iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
Mobile Development

30天,從0開始用Kotlin寫APP系列 第 15

Day 15 | Kotlin 中用 Retrofit 和 Moshi 捕捉神奇寶貝回來 - Part 1(起手式)

Retrofit

Retrofit 是 Android 和 Java 中 處理 HTTP 請求的 Thired party library ,他是基於 OKHttpRestful HTTP Network 框架,並且支持 GsonJsonXml 等序列化數據 EncodeDecode

Android App->Retrofit: 發起 Request
Note right of Retrofit: 封裝 Message 、Header 、Url 等訊息成封包
Retrofit -> OkHttp: 封包交給 OkHttp
OkHttp -> Server: 將封包透過 get 或 Post 丟給 Server
Server -> OkHttp: Response 數據封包
OkHttp -> Retrofit: 交給 Retrofit
Note right of Retrofit: 解析封包,並回原程序列化數據
Retrofit -> Android App: 返回序列化數據

導入 Retrofit

1. 將 Retrofit 加入 dependencies

Retrofit2OkHttp3 加到 app/build.gradledependencies 區塊中

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'

// OkHttp3
implementation 'com.squareup.okhttp3:okhttp:3.12.1'

2. 加入 Internet 權限到 <uses-permission>

Android 因為資安的原因,因此會將一些功能不加入預設的 App 中,而是當開發者有需要時,開發者要自己手動加入其中,而開發者就是透過 <uses-permission> 向 OS 要求要哪些 Service ,那 <uses-permission> 會寫在 AndroidManifest.xml 之中,而有些更私密的權限請求,甚至需要使用者同意, App 才能夠使用,例如: 訪問外部儲存空間( External Storage )

不同版本的 Android ,需要要求的 Service 也不盡相同,因此可以到下面的網址去看看目前服務的把本有哪些是需要要求服務權限的

那網路的 Service 也是需要和 Android OS 要求的,如果沒有加上去的話, App 執行時就沒辦法打 Request 出去,因此需要把

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

也加到 AndroidManifest 之中


HTTP Restful API 接口

那麼我們首要工作就是要用 Retrofit 實作下面兩個 Url ,讓 Retrofit 可以丟請求到這個個 Url 去

  1. https://pokeapi.co/api/v2/pokemon?offset=0&limit=10
    從 1 號開始,取出 10 隻神奇寶貝
  2. https://pokeapi.co/api/v2/pokemon/1
    取出第 1 號神奇寶貝的詳細資訊

我們可以先到這兩個 Url 中,看看裏面的資料格式大概長怎樣,以及我們需要取出哪些資料格式

接下來的都會先以第 1 個 Url : https://pokeapi.co/api/v2/pokemon?offset=0&limit=10 當作例子

1. 解析 Json 格式

那因為回傳資料都是 Json ,因此我們會需要有工具幫助我們從中解析 Key-Value ,那要解析 Json 很直覺的就會想到 Gson ,但這次鐵人賽就是要玩玩看新東西,因此我決定嘗試 Moshi ,另一款 Json 的 encode 、decode 框架,根據一些人的評測,效能上比 Gson 更佳,並且能力也不會比 Gson 遜色

因此我們這邊一樣要將 Moshi 加到 app/build.gradledependencies 區塊中

// moshi
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2'

因為有用到 kapt ,因此要將 kapt 加到最上面的 plugin

apply plugin: 'kotlin-kapt'

2. 建立 Data Class

那我們就可以建立之前有講解過的 data class 搭配 moshi 去解析回傳資料和將資料以結構化儲存起來
因此在 data/model 下,先建立 Priate.ktPirateResponse.kt ,兩者都是 data class

  • PirateResponse.kt
@JsonClass(generateAdapter = true)
data class PirateResponse(
    @field:Json(name = "count") val count: Int,
    @field:Json(name = "next") val next: String?,
    @field:Json(name = "previous") val previous: String?,
    @field:Json(name = "results") val results: List<Pirate>
)
  • Pirate.kt
@JsonClass(generateAdapter = true)
data class Pirate(
    @field:Json(name = "name") val name: String,
    @field:Json(name = "url") val url: String
)

可以注意到 PirateResponse 中有用到 Pirate ,這兩個 data class 是有關聯性的,會這樣設計的原因是因為 https://pokeapi.co/api/v2/pokemon?offset=0&limit=10 回傳 Json 格式長這樣

  • object 中的內容會對應到 PirateResponse
  • object 中的 results 的內容則會對應到 Pirate

3. Retrofit 建立 Url 的 Restful API 接口

建立完 data class 後就要開始設計接口,正常 Url 會具備 scheme://host+path?query 這樣的格式

https://pokeapi.co/api/v2/pokemon?offset=0&limit=10 為例

scheme -> https
host -> pokeapi.co
path -> api/v2/pokemon
query -> offset=0&limit=10

Retrofit 採用註解方式去描述 HTTP 的請求,例如常見的 @Get@Post@Put 等等,那我們這次打的都是 @GET,將上面的 Url 轉換到 Retrofit ,就會長的像這樣

@GET("pokemon")
fun fetchPirateList(
    @Query("limit") limit: Int = 10,
    @Query("offset") offset: Int = 0
): Call<PirateResponse>

這邊可以注意到 functionreturn 值是 Call<PirateResponse>,其中 Call 是負責傳輸資料到 Server 和接收 Server 回傳的資料,透過他就可以取得 Url 的資料,並且 Call 的泛型是帶 PirateResponse ,因此回傳資料會被放到 PirateResponse data class

上面的 function 會寫在 PokedexService.kt 中,這是一個在 data/api 資料夾下新建出來的 Interface


實作 Retrofit 及 OkHttp Client

完成Restful API 接口後,接下來就是實作 Retrofit 和 OkHttp Client

首先先到 utils/ 下面新建 NetworkManager.kt 並且他是一個 object ,裡面會再有兩個 Function ,分別是 provideOkHttpClientprovideRetrofit

  • provideOkHttpClient 是在處理相關 Http 設定,這邊可以注意到第 12 行,有調用一個 addInterceptor,除了這個方法也有 addNetworkInterceptor ,具體差別可以參考 Okhttp-wiki 之 Interceptors 拦截器
  • provideRetrofit 是在做封裝 provideOkHttpClient 的工作,因為 Json decode 上使採用 Moshi ,因此這邊的 addConverterFactory 就會放 MoshiConverterFactory.create()

把資料拿回來吧!

最後我在 MainActivity 上面放了一個按鈕

然後幫按鈕寫了一個簡單的 onClickListener 事件

fetch_data.setOnClickListener {
    val apiService = NetworkManager.provideRetrofit(NetworkManager.provideOkHttpClient())
        .create(PirateService::class.java)

    apiService.fetchPirateList().enqueue(object : Callback<PirateResponse> {
        override fun onResponse(
            call: Call<PirateResponse>,
            response: Response<PirateResponse>
        ) {
            Log.d(TAG, "response: ${response.body().toString()}")
        }
        override fun onFailure(call: Call<PirateResponse>, t: Throwable) {
            Log.d(TAG, "error: ${t.message}" ?: "Get some error")
        }
    })
}
  • 第 2 行實作了 provideRetrofit() 並且把 provideOkHttpClient() 傳進去做封裝,而 .create() 裏面就是塞入剛剛定義的 Restful API 接口介面
  • 第 5 行就呼叫 fetchPirateList() 取得 API Response ,並採用 enqueue function 進行異步操作
  • 當 Response 是成功的時候,會進到 onResponse() ,在這邊我會把 PirateResponse 的內容印出來。但如果失敗的話,會進到 onFailure() ,而這邊就會印出 Error message

全部程式碼會長的像這樣,完成之後就可以把 App 執行起來

那按下按鈕後可以在 Logcat 中看到成功把資料取回來了,大功告成!

2020-09-16 02:42:19.313 26970-26970/com.example.piratehegemony D/Companion: response: PirateResponse(count=1050, next=https://pokeapi.co/api/v2/pokemon?offset=10&limit=10, previous=null, results=[Pirate(name=bulbasaur, url=https://pokeapi.co/api/v2/pokemon/1/), Pirate(name=ivysaur, url=https://pokeapi.co/api/v2/pokemon/2/), Pirate(name=venusaur, url=https://pokeapi.co/api/v2/pokemon/3/), Pirate(name=charmander, url=https://pokeapi.co/api/v2/pokemon/4/), Pirate(name=charmeleon, url=https://pokeapi.co/api/v2/pokemon/5/), Pirate(name=charizard, url=https://pokeapi.co/api/v2/pokemon/6/), Pirate(name=squirtle, url=https://pokeapi.co/api/v2/pokemon/7/), Pirate(name=wartortle, url=https://pokeapi.co/api/v2/pokemon/8/), Pirate(name=blastoise, url=https://pokeapi.co/api/v2/pokemon/9/), Pirate(name=caterpie, url=https://pokeapi.co/api/v2/pokemon/10/)])

結語

那我在執行的時候有碰到一個 Error

java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType; ......

那他的解法就是把下面這段 code 加到 app/build.gradleandroid 區塊中

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

具體為什麼會這樣我還沒去探究,或許知道的大大可以留言開導小弟

那明天會繼續去把 Retrofit 這快做的更完整,希望明天可以準時出文章/images/emoticon/emoticon13.gif

Reference


上一篇
Day 14 | 用 Kotlin 實作 BottomNavigationView 與 FragNav
下一篇
Day 16 | 在 Sandwish 中夾入 Retrofit - Part 2(半完結)
系列文
30天,從0開始用Kotlin寫APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言