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

Posted 上马定江山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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的这些特性有进一步的了解。

小例子 —— 看猫咪

引入依赖

在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编写,内部使用协程实现异步。

我们运行一下吧!

总结一下

是不是很简单捏!看起来好像很多,其实核心用法就三个

  • 实例HttpClient

  • 在HttpClient中配置插件

  • 调用get或者post方法

由于内部使用了协程来进行异步,因此不用担心主线程阻塞!令我觉得比较香的是数据转换插件,可以再也不用担心数据转换了。并且支持例如XML、CBOR、Json等等,也不会担心后端会给我们发来什么数据格式了。

还有一个文中没有用到的是Logging插件,可以在logcat打印给服务端发了什么,服务端给客户端发了什么,调试API起来也很方便,跟后端拉扯起来也很有底气!

另外,Android插件不支持WebSocket,但是Okhttp和CIO支持!实际使用中可以用后者创建httpClient!

服务端

创建项目

服务端不是重点就简单提一下,贴一下代码,使用IntelliJ IDEA Ultimate可以直接创建Ktor工程,要是用社区版就去ktor.io/create/创建。

  1. 工程名字。

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

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()
        
    
    
    
  • 放入图片资源,我放了七只猫咪图片。

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

又总结一次

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

参考

ktor.io/

作者:米奇律师
链接:https://juejin.cn/post/7136829279903416333
更多Android学习资料可点击下方卡片~

以上是关于Ktor挖坑日记还在用Retrofit网络请求吗?试试Ktor吧的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Ktor 中设置类似于 Retrofit 的`Retrofit.Builder().baseUrl(baseUrl) 的 basePath?

liveDataCallAdapter库 用来实现Retrofit+LiveData具有生命周期的网络请求

我们真的需要使用RxJava+Retrofit吗?

android之Retrofit使用

Android开发 retrofit入门讲解

Retrofit的浅析 —— 针对面试