注册

【Ktor挖坑日记】还在用Retrofit网络请求吗?试试Ktor吧!

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中引入依赖


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也是可以的!


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

    如果想应用到其他客户端平台可以使用CIO




  3. 第三个简单来说就是数据转换的插件,例如将远端发送来的数据(可以是CBOR、Json、Protobuf)转换成一个个数据类。




  4. 而第四个就是第三个的衍生插件,相信用过kotlin-serialization的人会比较熟悉,是Kotlin序列化插件,本次引用的是json,类似于Gson,可以将json字符串转换成数据类。




当然,如果需要其他插件可以到官网上看看,例如打印日志Logging


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

创建HttpClient


首先创建一个HttpClient实例


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转换数据类的插件。




定义数据类


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

此处给猫咪定义名字、描述和图片url,需要注意的是需要加上@Serializable注解,这是使用kotlin-serialization的前提条件,而需要正常使用kotlin-serialization,需要在app模块的build.gradle加上以下plugin


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

创建API


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


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



  • body:获取返回结果,由于内部协程实现,因此不用担心阻塞主线程的问题,由于引入了ContentNegotiation,因此获取到结果之后可以对其进行转换,转换成实际数据类。




展示


ViewModel


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<out T> {
object Loading: UiState<Nothing>()
data class Success<T>(val value: T): UiState<T>()
data class Failure(val exc: Throwable): UiState<Nothing>()
}

inline fun <T> UiState<T>.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实现


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工程,要是用社区版就去ktor.io/create/创建。



  1. 工程名字。

image2.png


2. 配置插件,官方很多插件,不用想着一下子就添加完,需要用的时候再像客户端一样引入依赖就好。


image3.png


3. 创建项目,下载打开。


编写代码


到Application.kt看一下主函数


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



  • 配置Routing插件


    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插件


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



  • 放入图片资源,我放了七只猫咪图片。




image4.png


然后跑起来就好啦!去手机上看看效果吧!


又总结一次


客户端和服务端使用方式是比较相似的,这也非常友好,由于也是使用Kotlin作为后端,那很多代码都可以拷贝了,例如文中的数据类Cat甚至可以直接拷贝过来。Ktor用起来非常方便,由于其Okhttp插件的存在,在全Kotlin的Android项目中甚至可以考虑Ktor而不是Retrofit(当然Retrofit也是非常优秀的网络请求库)。关于Ktor的坑先开到这啦!


参考


ktor.io/


作者:米奇律师
链接:https://juejin.cn/post/7136829279903416333
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册