使用 Ktor 和 Koin 的 Compose Multiplatform 网络

社区移动开发Android
使用 Ktor 和 Koin 完成的 Compose Multiplatform 网络部分

picture.image

在开始使用 Compose Multiplatform 联网之前, 我想先回顾一下我之前的博客(第一部分), 在这篇博客中, 我们介绍了Compose Multiplatform如何在 KMM 支持下工作, 以及如何在不同平台间共享UI. 还介绍了依赖注入Koin如何工作, 在这个演示中, 我们使用Ktor客户端通过网络调用创建了一个简单的LazyverticalGrid.

以下是第一部分的链接:

使用 KMP 构建 Compose Multiplatform 共享 UI

什么是 Ktor?

Ktor 用于异步客户端和服务器应用, 这里我们将使用 Ktor 客户端与后端交互.

让我们从快速实现开始.

第一步是在版本目录文件 libs.versions.toml 中定义版本.

ktor = "2.3.7"
koin="1.1.0"

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-cio= {module ="io.ktor:ktor-client-cio", version.ref = "ktor"}
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
kotlin-serialization = {module = "io.ktor:ktor-serialization-kotlinx-json", version.ref="ktor"}
media-kamel = {module="media.kamel:kamel-image", version.ref="kamel"}
koin-compose = {module="io.insert-koin:koin-compose", version.ref = "koin"}
ktor-client-content-negotiation = {module = "io.ktor:ktor-client-content-negotiation", version.ref= "ktor"}

[bundles]
ktor = ["ktor-client-core", "ktor-client-content-negotiation"]

让我们将其包含在 build.gradle.kts 中.

sourceSets {
        val desktopMain by getting
        
        androidMain.dependencies {
            implementation(libs.compose.ui.tooling.preview)
            implementation(libs.androidx.activity.compose)
            implementation(libs.ktor.client.android)
        }

        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material)
            implementation(compose.ui)
            @OptIn(ExperimentalComposeLibrary::class)
            implementation(compose.components.resources)
            implementation(libs.bundles.ktor)
            implementation(libs.ktor.client.content.negotiation)
            implementation(libs.kotlin.serialization)
            implementation(libs.media.kamel)
            implementation(libs.koin.compose)
        }

        desktopMain.dependencies {
            implementation(compose.desktop.currentOs)
            implementation(libs.ktor.client.cio)
        }

        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
        }
    }

在上一篇博客中, 我们看到了如何为 koin 声明不同的模块:

NetworkModule.kt

val providehttpClientModule = module {
    single {
        HttpClient {
            install(ContentNegotiation) {
                json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
            }
        }
    }
}

这里的 HttpClient 是配置异步客户端的客户端:

RepositoryModule.kt

val provideRepositoryModule = module {
    single<NetworkRepository> { NetworkRepository(get()) }
}

ViewModelModule.kt

val provideviewModelModule = module {
    single {
        HomeViewModel(get())
    }
}

合并所有模块列表:

AppModule.kt

fun appModule() = listOf(providehttpClientModule, provideRepositoryModule, provideviewModelModule)

通过插入应用级别来启动 koin. koin 应用用于在组成上下文中启动新的 koin 应用:

@Composable
fun App() {
    KoinApplication(application = {
        modules(appModule())
    }) {
        MaterialTheme {
            Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
                HomeScreen()
            }
        }
    }

}

Kotlin 序列化包括一个编译器插件(可生成可序列化类的访客代码), 包含核心序列化 API 的运行时库以及包含各种序列化格式的支持库.

  • 支持标记为 @Serializable 的 Kotlin 类和标准集合.

Data Model 类

@Serializable
data class ApiResponse(
    @SerialName("products")
    var list: List<Products>
)
@Serializable
data class Products (
    @SerialName("id")
    var id: Int=0,
    @SerialName("title")
    var title: String="",
    @SerialName("description")
    val description: String="",
    @SerialName("price")
    val price: Double=0.0,
    @SerialName("discountPercentage")
    val discountPercentage: Double=0.0,
    @SerialName("category")
    val category: String="",
    @SerialName("thumbnail")
    val thumbnail: String="",
)

Repository类

class NetworkRepository(private val httpClient: HttpClient) {

     fun getProductList(): Flow<NetWorkResult<ApiResponse?>> {
        return toResultFlow {
                val response = httpClient.get("api url").body<ApiResponse>()
                 NetWorkResult.Success(response)
        }
    }
}

用于UI状态管理的ViewModel:

class HomeViewModel(private val networkRepository: NetworkRepository) {

    private val _homeState = MutableStateFlow(HomeState())
    private val _homeViewState: MutableStateFlow<HomeScreenState> = MutableStateFlow(HomeScreenState.Loading)
    val homeViewState = _homeViewState.asStateFlow()

    suspend fun getProducts() {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                networkRepository.getProductList().collect{response ->
                    when(response.status){
                        ApiStatus.LOADING->{
                            _homeState.update { it.copy(isLoading = true) }
                        }
                        ApiStatus.SUCCESS->{
                            _homeState.update { it.copy(isLoading = false, errorMessage = "", response.data) }
                        }
                        ApiStatus.ERROR->{
                            _homeState.update { it.copy(isLoading = false,errorMessage = response.message) }
                        }
                    }
                    _homeViewState.value = _homeState.value.toUiState()
                }
            } catch (e: Exception) {
                _homeState.update { it.copy(isLoading = false,errorMessage ="Failed to fetch data") }
            }
        }
    }
    sealed class HomeScreenState {
        data object Loading: HomeScreenState()
        data class Error(val errorMessage: String):HomeScreenState()
        data class Success(val responseData: ApiResponse):HomeScreenState()
    }
    private data class HomeState(
        val isLoading:Boolean = false,
        val errorMessage: String?=null,
        val responseData: ApiResponse?=null
    ) {
        fun toUiState(): HomeScreenState {
            return if (isLoading) {
                HomeScreenState.Loading
            } else if(errorMessage?.isNotEmpty()==true) {
                HomeScreenState.Error(errorMessage)
            } else {
                HomeScreenState.Success(responseData!!)
            }
        }
    }
}

Main Compose 类

@Composable
fun HomeScreen(){
    val viewModel: HomeViewModel= getKoin().get()
    val homeScreenState by viewModel.homeViewState.collectAsState()
    LaunchedEffect(Unit) {
        viewModel.getProducts()
    }
    when (homeScreenState) {
        is HomeViewModel.HomeScreenState.Loading -> {
            PiProgressIndicator()
        }
        is HomeViewModel.HomeScreenState.Success -> {
            val products = (homeScreenState as HomeViewModel.HomeScreenState.Success).responseData.list
            ProductCard(products)
        }
        is HomeViewModel.HomeScreenState.Error -> {
           //show Error
        }
    }
}

不要忘记在 Android Manifest 文件中添加 Internet 权限:

androidMain(AndroidManifest.xml)

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

就是这样 请参见这个Github上的完整实现! !

好了, 今天的内容就分享到这儿了啦!

一家之言, 欢迎拍砖!

Happy Coding! Stay GOLDEN!

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论