Compose Multiplatform 之旅 — 网络请求(Ktor)

768 阅读3分钟

在app 开发过程中,网络请求是必不可少的,那Compose Multiplatform项目中,我们该如何进行网络请求呢?今天我们就来聊一聊CMP跨平台项目中的网络请求。

想更多了解Compose Multiplatform项目的小伙伴,也可以看看其他文章

对于框架的选择,我们还是使用之前推荐的 klibs.io,搜network 关键字,找到了一个破万star的开源项目ktor。它是由JetBrains打造的异步网络框架,100% Kotlin,完美契合CMP。

仓库地址:github.com/ktorio/ktor

依赖引入

注意需要引入serialization的plugins,不然不能生成对应的类,出现类找不到的问题。


//libs.versions.toml文件

ktor = "2.3.11"

[libraries]
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }  
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }  
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }  
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }  
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

[plugins]
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }



//shared build.gradle.kts 文件 
plugins {  
    ...
    alias(libs.plugins.kotlinx.serialization)  
}  
  
kotlin {  
  
    sourceSets {  
        commonMain.dependencies {  
            implementation(libs.ktor.client.core)  
            implementation(libs.ktor.client.content.negotiation)  
            implementation(libs.ktor.serialization.kotlinx.json)  
        }  
        androidMain.dependencies {  
            implementation(libs.ktor.client.android)  
        }  
        iosMain.dependencies {  
            implementation(libs.ktor.client.darwin)  
        }  
        jvmMain.dependencies {  
            implementation(libs.ktor.client.cio)  
        }  
    }
}

如果之前没有引入lifecycle-viewmodel-compose 也需要引入下,这个基于viewmodel 来更新UI,后续我们也会讲讲viewmodel

//libs.versions.toml
compose-lifecycle = "2.8.2"
lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "compose-lifecycle" }

implementation(libs.lifecycle.viewmodel.compose)

Android 还需要manifest里面声明一下权限

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

初始化ktorClient 实例

val ktorClient = HttpClient {  
    install(ContentNegotiation) {  
        json(kotlinx.serialization.json.Json {  
            ignoreUnknownKeys = true  
            isLenient = true  
        })  
    }  
}

编写对应数据类

这里我们基于wanandroid 的开放api 做一个数据的展示。

对应接口示例:www.wanandroid.com/article/lis…

根据对应的数据,可以找一个json to kotlin的网站,直接生成对应的data class,然后加上@Serializable 注解。

下面就是wanandroid的数据自动生成的data class 精简后的代码。

@Serializable  
data class ApiResponse<T>(  
    val data: T,  
    val errorCode: Int,  
    val errorMsg: String  
)  
  
@Serializable  
data class Article(  
    val id: Int,  
    val title: String,  
    val author: String,  
    val niceDate: String,  
    val superChapterName: String,  
    val chapterName: String,  
    val link: String  
)  
  
@Serializable  
data class ArticleList(  
    val curPage: Int,  
    val datas: List<Article>,  
    val offset: Int,  
    val over: Boolean,  
    val pageCount: Int,  
    val size: Int,  
    val total: Int  
)

创建 Repository

我们这里是只用到get请求,代码也是非常的简单,给一个url 拼接参数即可。post 请求可以看testPost方法,使用上也十分方便。

class WanAndroidRepository() {  
    private val baseUrl = "https://www.wanandroid.com"  

	//get 请求
    suspend fun getArticles(page: Int = 0): ApiResponse<ArticleList> {  
        return ktorClient.get("$baseUrl/article/list/$page/json").body()  
    }  

	//post 请求
    suspend fun testPost(page: Int = 0): ApiResponse<ArticleList> {  
        return ktorClient.post("$baseUrl/article/list/$page/json") {  
            headers {  
                append("X-Custom-Header", "value")  
                append(HttpHeaders.UserAgent, "Ktor Client")  
            }  
            contentType(ContentType.Application.Json)  
            setBody(xxx)  
        }.body()  
    }  
}

创建 ViewModel

继承了ViewModel,使用viewModelScope 使用协程调用接口请求

class ArticleViewModel(  
    private val repository: WanAndroidRepository  
) : ViewModel(){  
    private val _articles = mutableStateListOf<Article>()  
    val articles: List<Article> = _articles  
  
    private val _isLoading = mutableStateOf(false)  
    val isLoading: State<Boolean> = _isLoading  
  
    private val _error = mutableStateOf<String?>(null)  
    val error: State<String?> = _error  
  
    private var currentPage = 0  
  
    fun loadArticles() {  
        viewModelScope.launch(Dispatchers.Default) {  
            _isLoading.value = true  
            try {  
                val response = repository.getArticles(currentPage)  
                if (response.errorCode == 0) {  
                    _articles.addAll(response.data.datas)  
                    currentPage++  
                } else {  
                    _error.value = response.errorMsg  
                }  
            } catch (e: Exception) {  
                _error.value = e.message ?: "Unknown error"  
            } finally {  
                _isLoading.value = false  
            }  
        }  
    }  
}

UI展示

使用前面的导航的逻辑,在发现tab 展示创建对应的viewModel 和 ComposableUI

object FindTab: Tab {  
    override val options: TabOptions  
        @Composable  
        get() = TabOptions(  
            index = 0u,  
            title = "Find"  
        )  
  
    @Composable  
    override fun Content() {  
        val viewModel = viewModel {  
            ArticleViewModel(WanAndroidRepository())  
        }  
        ArticleScreen(viewModel = viewModel)  
    }  
}  

使用LazyColumn列表展示,每一个item卡片,封装成ArticleItem

@Composable  
fun ArticleScreen(viewModel: ArticleViewModel) {  
    val articles = viewModel.articles  
    val isLoading = viewModel.isLoading  
  
    Column(modifier = Modifier.fillMaxSize()) {  
  
        if (isLoading.value) {  
            CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))  
        }  
  
        LazyColumn {  
            items(articles.size) { index ->  
                ArticleItem(article = articles[index])  
            }  
        }    
    }  
    LaunchedEffect(Unit) {  
        viewModel.loadArticles()  
    }  
}  
  
@Composable  
fun ArticleItem(article: Article) {  
    Card(  
        modifier = Modifier  
            .fillMaxWidth()  
            .padding(8.dp),  
        elevation = 4.dp  
    ) {  
        Column(modifier = Modifier.padding(16.dp)) {  
            Text(  
                text = article.title,  
                style = MaterialTheme.typography.h6,  
                modifier = Modifier.padding(bottom = 8.dp)  
            )  
            Row(verticalAlignment = Alignment.CenterVertically) {  
                Text(  
                    text = "${article.author} · ",  
                    style = MaterialTheme.typography.caption,  
                    color = Color.Gray  
                )  
                Text(  
                    text = article.niceDate,  
                    style = MaterialTheme.typography.caption,  
                    color = Color.Gray  
                )  
                Spacer(modifier = Modifier.weight(1f))  
                Text(  
                    text = "${article.superChapterName}/${article.chapterName}",  
                    style = MaterialTheme.typography.caption,  
                    color = Color.Blue  
                )  
            }  
        }    
    }
}

最终效果

img_v3_02jj_26f7fc62-340a-46a6-807f-260f4cba0e6g.jpg

结语

ktor 是一个强大的库,不仅可以实现我们这里的Android、iOS、桌面端,还能写后端,感兴趣的伙伴,也可以去看看它的官方文档

目前已经提到了好几个三方库,能覆盖app开发大部分场景,有想了解跨平台其他的特性或者三方库的伙伴,可以提出来,我们一起学习,共同进步。