【Ktor挖坑日記】還在用Retrofit網路請求嗎?試試Ktor吧!

語言: CN / TW / HK

Ktor官方對Ktor的描述是:

Create asynchronous client and server applications. Anything from microservices to multiplatform HTTP client apps in a simple way. Open Source, free, and fun!

建立非同步客戶端和和伺服器應用,從微服務到多平臺HTTP客戶端應用程式都可以用一種簡單的方式完成。開源、免費、有趣!

它具有輕量級+可擴充套件性強+多平臺+非同步的特性。

  • 輕量級和可擴充套件性是因為它的核心比較簡單,並且當需要一些功能的時候可以加入別的外掛到專案中,並不會造成功能冗餘。並且Ktor的擴充套件是使用插拔的方式,使用起來非常簡單!

  • 非同步,Ktor內部是使用Kotlin協程來實現非同步,這對於熟悉Kotlin的Android開發非常友好。

看到這裡可能一頭霧水,下面將用一個比較簡單的例子來帶大家入坑Ktor!等看完這篇文章之後就會對Ktor的這些特性有進一步的瞭解。

小例子 —— 看貓咪

序列 01.gif

引入依賴

在app模組的gradle中引入依賴

```kotlin plugins { ... id 'org.jetbrains.kotlin.plugin.serialization' version "1.7.10" // 跟Kotlin版本一致 }

dependencies { ... // Ktor def ktor_version = "2.1.0" implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-android:$ktor_version" implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" } ```

稍微解釋一下這兩個依賴

  1. Ktor的客戶端核心

  2. 由於本APP是部署在Android上的,因此需要引入一個Android依賴,Android平臺和其他平臺的不同點在於Android具有主執行緒的概念,Android不允許在主執行緒傳送網路請求,而在Kotlin協程中就是主排程器的概念,其內部是post任務到主執行緒Handler中,這裡就不展開太多。當然如果要使用OkHttp也是可以的!

    kotlin implementation "io.ktor:ktor-client-okhttp:$ktor_version"

    如果想應用到其他客戶端平臺可以使用CIO

  3. 第三個簡單來說就是資料轉換的外掛,例如將遠端傳送來的資料(可以是CBOR、Json、Protobuf)轉換成一個個資料類。

  4. 而第四個就是第三個的衍生外掛,相信用過kotlin-serialization的人會比較熟悉,是Kotlin序列化外掛,本次引用的是json,類似於Gson,可以將json字串轉換成資料類。

當然,如果需要其他外掛可以到官網上看看,例如列印日誌Logging

kotlin implementation "ch.qos.logback:logback-classic:$logback_version" implementation "io.ktor:ktor-client-logging:$ktor_version"

建立HttpClient

首先建立一個HttpClient例項

kotlin val httpClient = HttpClient(Android) { defaultRequest { url { protocol = URLProtocol.HTTP host = 你的host port = 你的埠 } } install(ContentNegotiation) { json() } }

建立的時候是使用DSL語法的,這裡解釋一下其中使用的兩個配置

  • defaultRequest:給每個HTTP請求加上BaseUrl

    例如請求"/get-cat"就會向"http://${你的host}:${你的埠}/get-cat"發起HTTP請求。

  • ContentNegotiation:引入資料轉換外掛。

  • json:引入自動將json轉換資料類的外掛。

定義資料類

```kotlin @Serializable data class Cat( val name: String, val description: String, val imageUrl: String )

```

此處給貓咪定義名字、描述和圖片url,需要注意的是需要加上@Serializable註解,這是使用kotlin-serialization的前提條件,而需要正常使用kotlin-serialization,需要在app模組的build.gradle加上以下plugin

kotlin plugins { ... id 'org.jetbrains.kotlin.plugin.serialization' version "1.7.10" // 跟Kotlin版本一致 }

建立API

```kotlin interface CatSource {

suspend fun getRandomCat(): Result<Cat>

companion object {
    val instance = CatSourceImpl(httpClient)
}

}

class CatSourceImpl( private val client: HttpClient ) : CatSource {

override suspend fun getRandomCat(): Result<Cat> = runCatching {
    client.get("random-cat").body()
}

}

```

此處宣告一個CatSource介面,介面中宣告一個獲取隨機小貓咪的函式,並且對該介面進行實現。

  • suspend:HttpClient的方法大多數為suspend函式,例如例子中的get為suspend函式,因此介面也要定義成suspend函式。

  • Result:Result為Kotlin官方包裝類,具有successfailure兩個方法,可以包裝成功和失敗兩種資料,可以簡單使用runCatching來返回Result

    ```kotlin @InlineOnly @SinceKotlin("1.3") public inline fun T.runCatching(block: T.() -> R): Result { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) } }

    ```

  • body:獲取返回結果,由於內部協程實現,因此不用擔心阻塞主執行緒的問題,由於引入了ContentNegotiation,因此獲取到結果之後可以對其進行轉換,轉換成實際資料類。

展示

ViewModel

```kotlin class MainViewModel : ViewModel() {

private val catSource = CatSource.instance

private val _catState = MutableStateFlow<UiState<Cat>>(UiState.Loading)
val catState = _catState.asStateFlow()

init {
    getRandomCat()
}

fun getRandomCat() {
    viewModelScope.launch {
        _catState.value = UiState.Loading
        // fold 方法可以用來對 Result 的結果分情況處理
        catSource.getRandomCat().fold(
            onSuccess = {
                _catState.value = UiState.Success(it)
            }, onFailure = {
                _catState.value = UiState.Failure(it)
            }
        )
    }
}

}

sealed class UiState { object Loading: UiState() data class Success(val value: T): UiState() data class Failure(val exc: Throwable): UiState() }

inline fun UiState.onState( onSuccess: (T) -> Unit, onFailure: (Throwable) -> Unit = {}, onLoading: () -> Unit = {} ) { when(this) { is UiState.Failure -> onFailure(this.exc) UiState.Loading -> onLoading() is UiState.Success -> onSuccess(this.value) } }

```

Activity

介面比較簡單,因此用Compose實現

```kotlin class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        KittyTheme {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(32.dp)
            ) {
                val viewModel: MainViewModel = viewModel()
                val catState by viewModel.catState.collectAsState()
                catState.onState(
                    onSuccess = { cat ->
                        AsyncImage(model = cat.imageUrl, contentDescription = cat.name)
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(
                            text = cat.name,
                            fontWeight = FontWeight.SemiBold,
                            fontSize = 20.sp
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(text = cat.description)
                    },
                    onFailure = {
                        Text(text = "Loading Failure!")
                    },
                    onLoading = {
                        CircularProgressIndicator()
                    }
                )

                Button(
                    onClick = viewModel::getRandomCat,
                    modifier = Modifier.align(Alignment.End)
                ) { Text(text = "Next Cat!") }

                Spacer(modifier = Modifier.height(8.dp))
            }
        }
    }
}

} ```

  • 對state分情況展示

    • 載入中就展示轉圈圈。

    • 成功就展示貓咪圖片、貓咪名字、貓咪描述。

    • 失敗就展示載入失敗。

  • 展示圖片的AsyncImage來自於Coil展示庫,傳入imageUrl就好啦,使用Kotlin編寫,內部使用協程實現非同步。

我們執行一下吧!

image.png

總結一下

是不是很簡單捏!看起來好像很多,其實核心用法就三個

  • 例項HttpClient

  • 在HttpClient中配置外掛

  • 呼叫get或者post方法

由於內部使用了協程來進行非同步,因此不用擔心主執行緒阻塞!令我覺得比較香的是資料轉換外掛,可以再也不用擔心資料轉換了。並且支援例如XML、CBOR、Json等等,也不會擔心後端會給我們發來什麼資料格式了。

還有一個文中沒有用到的是Logging外掛,可以在logcat列印給服務端發了什麼,服務端給客戶端發了什麼,除錯API起來也很方便,跟後端拉扯起來也很有底氣!

另外,Android外掛不支援WebSocket,但是Okhttp和CIO支援!實際使用中可以用後者建立httpClient!

服務端

建立專案

服務端不是重點就簡單提一下,貼一下程式碼,使用IntelliJ IDEA Ultimate可以直接建立Ktor工程,要是用社群版就去https://ktor.io/create/建立。

  1. 工程名字。

image2.png

2. 配置外掛,官方很多外掛,不用想著一下子就新增完,需要用的時候再像客戶端一樣引入依賴就好。

image3.png

3. 建立專案,下載開啟。

編寫程式碼

到Application.kt看一下主函式

kotlin fun main() { embeddedServer(Netty, port = 你的埠, host = "0.0.0.0") { configureRouting() configureSerialization() }.start(wait = true) }

  • 配置Routing外掛

    ```kotlin fun Application.configureRouting() { routing { randomCat() static { resources("static") } } }

    fun Route.randomCat() { get("/random-cat") { // 隨便回一直貓咪給客戶端 call.respond(cats.random()) } }

    //本地IPV4地址 private const val BASE_URL = "http://${你的host}:${你的埠}"

    private val cats = listOf( Cat("奪寶1號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat1.jpg"), Cat("奪寶2號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat2.jpg"), Cat("奪寶3號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat3.jpg"), Cat("奪寶4號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat4.jpg"), Cat("奪寶5號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat5.jpg"), Cat("奪寶6號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat6.jpg"), Cat("奪寶7號", "這是一隻可愛的小貓咪", "$BASE_URL/cats/cat7.jpg"), )

    @Serializable data class Cat( val name: String, val description: String, val imageUrl: String )

    ```

  • 配置Serialization外掛

    ```kotlin fun Application.configureSerialization() { install(ContentNegotiation) { json() } }

    ```

  • 放入圖片資源,我放了七隻貓咪圖片。

image4.png

然後跑起來就好啦!去手機上看看效果吧!

又總結一次

客戶端和服務端使用方式是比較相似的,這也非常友好,由於也是使用Kotlin作為後端,那很多程式碼都可以拷貝了,例如文中的資料類Cat甚至可以直接拷貝過來。Ktor用起來非常方便,由於其Okhttp外掛的存在,在全Kotlin的Android專案中甚至可以考慮Ktor而不是Retrofit(當然Retrofit也是非常優秀的網路請求庫)。關於Ktor的坑先開到這啦!

參考

https://ktor.io/