在开始使用 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!