Ktor是一个在国外挺火的开源网络框架,它既是一个服务端框架、也能用作客户端。它背靠JetBrains,即出品IntelliJ IDEA的公司,一起Kotlin也是Jetbrains开发的,Ktor也算是背景十分雄厚了 。截止到写文章的时分,Ktor在github拥有10k的star。

github.com/ktorio/ktor

Ktor的特性能够先看看我的第一篇入坑文章。简而言之,它是全Kotlin的网络框架,运用协程在底层完成异步,具有可插拔插件的特性,扩展性十分强,本文就简略带我们入坑Ktor的客户端(Desktop、Android)。

多渠道客户端

为了展现这个网络框架的多渠道特性,我决议还是用一个例子来解说,此处创立一个多渠道项目。当然假如我们的项目是单渠道的(Destop or Android),Android渠道引进依靠办法见第一篇入坑文章,也能够在单渠道上运用。

【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 —— 客户端

进入common/build.gradle.kts加入Ktor依靠

kotlin {
    ....
    sourceSets {
        val commonMain by getting {
            dependencies {
                ...
                // Coroutine
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
                // Ktor-core
                implementation("io.ktor:ktor-client-core:${extra["ktor.version"]}")
                // Logging
                implementation("io.ktor:ktor-client-logging:${extra["ktor.version"]}")
                implementation("ch.qos.logback:logback-classic:1.2.11")
                // Negotiation
                implementation("io.ktor:ktor-client-content-negotiation:${extra["ktor.version"]}")
                implementation("io.ktor:ktor-serialization-kotlinx-json:${extra["ktor.version"]}")
            }
        }
        val androidMain by getting {
            dependencies {
                ...
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
                implementation("io.ktor:ktor-client-okhttp:${extra["ktor.version"]}")
            }
        }
        val desktopMain by getting {
            dependencies {
                ...
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
                implementation("io.ktor:ktor-client-cio:${extra["ktor.version"]}")
            }
        }
    }
}

在commonMain中加入四个依靠,分别是:

  • Kotlin协程核心组件

  • Ktor客户端核心库

  • Ktor打印插件

  • Ktor数据序列化插件 —— 本文引进Json可将Json转化成对应数据类

在androidMain中加入两个依靠,分别是:

  • coroutines-android协程组件

  • Ktor的Okhttp客户端插件,好用的Okhttp!

    当然也能够运用android特有的。

    implementation("io.ktor:ktor-client-android:${extra["ktor.version"]}")
    

在desktopMain加入两个依靠,分别是

  • coroutines-core-jvm协程组件

  • Ktor的CIO客户端插件,这儿也能够引进Okhttp。

为什么像Okhttp、CIO这些客户端插件需要在不同的渠道上分别引证呢?

因为核心库是通用的,而其他客户端插件是有渠道特性的,例如ktor-android只能在Android渠道上运用不能在Desktop渠道上运用,所以需要在不同渠道依靠。

协程组件同上。

创立客户端实例

翻开common/commonMain/kotlin/包名/platform.kt加入以下一行代码声明一个httpClient expect函数。该函数的效果简略来说类似于函数的接口,在通用代码库中声明,在不同的渠道有不同的完成。

expect fun getPlatformName(): String
expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient

翻开common/androidMain/kotlin/包名/platform.kt完成上面声明的httpClient函数。因为翻开的Android渠道的文件,因而运用上面依靠的OkHttp插件。(此处能够看到也完成了一个getPlatfromName函数,返回”Android”)

actual fun getPlatformName(): String {
    return "Android"
}
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
    config(this)
}

翻开common/desktopMain/kotlin/包名/platform.kt完成httpClient函数,运用CIO

actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(CIO) {
    config(this)
}

到这儿,多渠道的代码适配就告一段落了!

common/commonMain/kotlin/包名/创立一个network文件夹,并在network中创立一个HttpClient文件。声明一个httpClient

val httpClient = httpClient {
    install(DefaultRequest) {
         url {
             protocol = URLProtocol.HTTP  // HTTP协议
             host = "192.168.31.229"      // 本地IP
             port = 8088                  // 本地端口
         }
    }
    install(Logging) {
        level = LogLevel.ALL
    }
    install(HttpCookies) {
        storage = AcceptAllCookiesStorage()
    }
    install(ContentNegotiation) {
        json()
    }
}

设置插件的时分遵循DSL语法,而且每个插件都是可插拔的,介绍一下运用的插件:

DefaultRequest

是一个主动弥补默认恳求base URL的插件,对应Retrofit便是baseUrl办法。

Retrofit.Builder().baseUrl(BASE_URL)

它能够独自设置协议、host和port,可读性比较强。

Logging

是一个主动打印恳求和呼应的插件,十分好用。有5个等级能够选择:ALL、HEADER、BODY、INFO、NONE,依据名字能够知道对应的打印内容,为了方便展示,此处设置ALL挂号。打印结果能够看下文。

HttpCookies

是一个寄存Cookies的插件,默认运用AcceptAllCookiesStorage在内存寄存服务端下发的Cookies,点进AcceptAllCookieStorage能够看到它默认接纳一切Cookies,而且对发放了Cookies的网站运用同样的Cookies。

不过建议自己完成CookiesStorage接口来寄存Cookies,并运用自己的办法来持久化Cookies。

ContentNegotiation

数据序列化插件,类似于Retrofit的addConverterFactory,能够将数据序列化成对应数据类。

Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create()) //运用GSON转化库解析JSON数据

发送Http恳求

创立一个CatSource object,先看一下最常用的Get、Post恳求,HttpClient建议网络恳求一般是挂起函数,在内部切换成子线程建议网络恳求,因而它是异步的,在实际运用中直接调用函数即可,是十分方便的。

Get

@Serializable
data class Cat(
    val name: String,
    val description: String,
    val imageUrl: String
)
object CatSource {
    suspend fun getRandomCat(): Result<Cat> {
        return runCatching {
            httpClient.get("random-cat").body()
        }
    }
}

运用HttpClient实例的get办法传入一个url字符串获取到一个HttpResponse实例,而经过该HttpResponse能够获取到服务端发回来的呼应,包括call、HTTP状况码、HTTP版本、恳求时刻和呼应时刻等等。

因为前面引进了默认恳求的IP地址和端口,因而在恳求中会主动拼接成一个完整的URL

public abstract class HttpResponse : HttpMessage, CoroutineScope {
    // call中包含恳求、呼应报文,能够从中获取到参数信息、内容
    public abstract val call: HttpClientCall
    // HTTP状况码
    public abstract val status: HttpStatusCode
    // HTTP版本,通常为1.1和2.0
    public abstract val version: HttpProtocolVersion
    // 恳求时刻
    public abstract val requestTime: GMTDate
    // 呼应时刻
    public abstract val responseTime: GMTDate
}

而一般运用body()办法获取服务器呼应内容,看一下body办法。

public suspend inline fun <reified T> HttpResponse.body(): T = call.bodyNullable(typeInfo<T>()) as T

其实只是经过从HttpResponse的call成员变量中获取呼应内容,点进发现还有反序列化的逻辑,而这个反序列化则需要引进上方依靠的反序列化的插件。因为是inline函数而且经过<reified T>将泛型类型传递进去,因而不会丢掉类型信息。

因而在运用的时分只需要一行函数即可建议网络恳求并转化成想要的数据类。

网络恳求和反序列化过程中,可能会抛出异常,因而在调用的时分需要try-catch

Post

@Serializable
data class SpecialCatRequest(
    val number: Int
)
object CatSource {
    suspend fun postSpecialCat(number: Int): Result<Cat> {
        return runCatching {
            httpClient.post {
                url("special-cat")
                setBody(SpecialCatRequest(number = number))
                contentType(ContentType.Application.Json)
            }.body()
        }
    }
}

经过HttpClient实例的post函数获取一个HttpResponse,这次并没有直接url参数进去,采用别的一个重载办法。而这个重载办法传入一个HttpRequestBuilder.() -> Unit参数,也便是说这个参数在HttpRequestBuilder效果域内,能够调用HttpRequestBuilder中的办法构建一个恳求。在实际运用中则能够经过DSL范式来构建报文。

  • 调用url办法传入urlString,传入的值会拼接之前设置的IP地址和端口,组合成一个完整的URL

  • 调用setBody设置恳求body内容,能够直接传入数据类,setBody办法是一个内联泛型实体化的函数,因而能够获取该数据类型信息。

  • 调用contentType为该body设置序列化类型,因为前方引证的Json依靠,因而此处设置Json类型,点进ContentType.Application会发现其支撑十分多种类型,Json、Cbor、Protobuf、ZIP等等,如需运用则分别引证依靠即可。

界面代码

翻开common/src/commomMain/kotlin/包名/App.kt,写下如下Compose代码。

@Composable
fun App() {
    val scope = rememberCoroutineScope()
    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Get恳求", style = TextStyle(fontSize = 24.sp))
        var catForGet by remember { mutableStateOf(Cat("没有猫咪", "快Get获取猫咪", "没有URL")) }
        Button(onClick = {
            scope.launch {
                CatSource.getRandomCat().onSuccess { catForGet = it }
            }
        }) {
            Text("Get随机猫咪")
        }
        Text(catForGet.toString())
        Spacer(Modifier.height(32.dp))
        Text("Post恳求", style = TextStyle(fontSize = 24.sp))
        var catForPost by remember { mutableStateOf(Cat("没有猫咪", "快Post获取猫咪", "没有URL")) }
        var catNumber by remember { mutableStateOf("1") }
        OutlinedTextField(
            value = catNumber,
            onValueChange = {
                catNumber = it
            },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number
            )
        )
        Button(onClick = {
            scope.launch {
                CatSource.postSpecialCat(catNumber.toIntOrNull() ?: 0).onSuccess {
                    catForPost = it
                }.onFailure {
                    println("获取猫咪失利!请查看参数。")
                }
            }
        }) {
            Text("Post特定猫咪")
        }
        Text(catForPost.toString())
    }
}

运转一下吧!

【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 —— 客户端

本次跑了桌面端和Android端,都能成功从本地建立的服务器中正确获取到相应内容!

本来想做图片展示猫咪的,因为自己比较懒就没做!

能够看到左下角打印日志的当地有打印恳求和呼应日志。而这个也是一个插件,在初始化HttpClient的时分设置了install(Logging)并将打印等级设置成了ALL,因而打印信息十分全,有看到get恳求返回的Json字符串,也能看到post恳求发送出去的{"number":4}

【Ktor入坑指南】全Kotlin编写的多平台异步网络框架 —— 客户端

代码

放一下代码仓库,想跑通文章的代码的同学能够clone下来跑。

服务端:github.com/MReP1/ktor-…

客户端:github.com/MReP1/Multi…

总结

不知道有没有人看到这儿呢?其实我是不太会写总结的,关于全Kotlin的项目我会比较倾向于运用Ktor作为网络恳求框架,对协程的支撑也比较好,扩展性也强。本文章下一篇Ktor的文章我会讲讲怎么运用Ktor建议WebSocket长衔接,同样是运用协程和Flow来完成的!