Compose Multiplatform, Paging, Koin, Ktor
在开始使用 Compose Multiplatform 进行 Paging 之前,我想先看看我之前的博客(第 2 部分),在这篇博客中,我们介绍了Compose Multiplatform如何与Ktor 和 Koin协同工作.
阅读本文(第 2 部分):
使用 Ktor 和 Koin 的 Compose Multiplatform 网络
阅读本文(第 1 部分): 使用 KMP 构建 Compose Multiplatform 共享 UI
让我们从快速实现Paging开始.
以下是版本目录文件:
ktor = "2.3.7"
koin="1.1.0"
pagingCommonVersion = "3.3.0-alpha02-0.4.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"}
paging-compose-common = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingCommonVersion" }
[bundles]
ktor = ["ktor-client-core", "ktor-client-content-negotiation"]
我在 KMM 的Paging中使用了这个第三方依赖项
这里是它的链接 !
在 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)
implementation(libs.paging.compose.common)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.ktor.client.cio)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
我们在上一篇博客中介绍了koin的设置,让我们快速回顾一下:
Koin 的实现
val providehttpClientModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
}
}
}
}
val provideRepositoryModule = module {
single<NetworkRepository> { NetworkRepository(get()) }
}
val provideviewModelModule = module {
single {
HomeViewModel(get())
}
}
fun appModule() = listOf(providehttpClientModule, provideRepositoryModule, provideviewModelModule)
通过插入Application来启动 koin .
koin 应用用于在 Compose Context中启动新的 koin 应用:
@Composable
fun App() {
KoinApplication(application = {
modules(appModule())
}) {
MaterialTheme {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
HomeScreen()
}
}
}
}
带有Result类的Api响应
suspend inline fun <reified T> HttpClient.getResults(
block: HttpRequestBuilder.() -> Unit
): Result<T> = try {
val response = request(block)
if (response.status == HttpStatusCode.OK) {
Result.Success(response.body())
} else {
Result.Error(Throwable("${response.status}: ${response.bodyAsText()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
sealed interface Result<out T> {
class Success<out T>(val value: T) : Result<T>
data object Loading : Result<Nothing>
class Error(val throwable: Throwable) : Result<Nothing>
}
inline fun <T, R> Result<T>.map(transform: (value: T) -> R): Result<R> =
when (this) {
is Result.Success -> Result.Success(transform(value))
is Result.Error -> Result.Error(throwable)
is Result.Loading -> Result.Loading
}
Ktor
关于 ktor 的实现,请参阅我之前的文章(第 2 部分).
class NetworkRepository(private val httpClient: HttpClient) {
fun getProducts(): Flow<PagingData<Products>> = Pager(
config = PagingConfig(pageSize = 10, initialLoadSize = 10, enablePlaceholders = false,),
pagingSourceFactory = {
ResultPagingSource { page, _ ->
httpClient.getProducts(page).map { it.list }
}
}
).flow
}
Paging
我们在这里使用的是 Gridview,根据你的要求,你可以使用 listview 的封装可组合函数,如下所示:
@Composable
fun <T : Any> PagingGrid(
data: LazyPagingItems<T>,
content: @Composable (T) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxSize()
){
items(data.itemCount) { index ->
val item = data[index]
item?.let { content(it) }
}
data.loadState.apply {
when {
refresh is LoadStateNotLoading && data.itemCount < 1 -> {
item {
Box(
modifier = Modifier.fillMaxWidth(1f),
contentAlignment = Alignment.Center
) {
Text(
text = "No Items",
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
}
}
}
refresh is LoadStateLoading -> {
item {
Box(
modifier = Modifier.fillMaxSize().padding(20.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = Color.Red
)
}
}
}
append is LoadStateLoading -> {
item {
CircularProgressIndicator(
color = Color.Red,
modifier = Modifier.fillMaxWidth(1f)
.padding(20.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
refresh is LoadStateError -> {
item {
ErrorView(
message = "No Internet Connection.",
onClickRetry = { data.retry() },
modifier = Modifier.fillMaxWidth(1f)
)
}
}
append is LoadStateError -> {
item {
ErrorItem(
message = "No Internet Connection",
onClickRetry = { data.retry() },
)
}
}
}
}
}
}
@Composable
private fun ErrorItem(
message: String,
modifier: Modifier = Modifier,
onClickRetry: () -> Unit
) {
Row(
modifier = modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = message,
maxLines = 1,
modifier = Modifier.weight(1f),
color = Color.Red
)
OutlinedButton(onClick = onClickRetry) {
Text(text = "Try again")
}
}
}
@Composable
private fun ErrorView(
message: String,
modifier: Modifier = Modifier,
onClickRetry: () -> Unit
) {
Column(
modifier = modifier.padding(16.dp).onPlaced { _ ->
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = message,
maxLines = 1,
modifier = Modifier.align(Alignment.CenterHorizontally),
color = Color.Red
)
OutlinedButton(
onClick = onClickRetry, modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
) {
Text(text = "Try again")
}
}
}
用于Paging结果的结果分页源类
open class ResultPagingSource<T : Any>(private val pagingData: suspend (page: Int, pageSize: Int) -> Result<List<T>>) :
PagingSource<Int, T>() {
override fun getRefreshKey(state: PagingState<Int, T>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> =
(params.key ?: 1).let { _page ->
try {
pagingData(_page, params.loadSize)
.run {
when (this) {
/* success */
is Result.Success -> {
LoadResult.Page(
data = value,
/* no previous pagination int as page */
prevKey = _page.takeIf { it > 1 }?.dec(),
/* no pagination if no results found else next page as +1 */
nextKey = _page.takeIf { value.size >= params.loadSize }?.inc()
)
}
/* error */
is Error -> LoadResult.Error(this)
else -> LoadResult.Error(IllegalStateException("$this type of [Result] is not allowed here"))
}
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
我们已经完成了Paging设置! 让我们从入口点应用类开始:
@Composable
fun App() {
KoinApplication(application = {
modules(appModule())
}) {
MaterialTheme {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
HomeScreen()
}
}
}
}
Homescreen:
@Composable
fun HomeScreen() {
val viewModel: HomeViewModel = getKoin().get()
val result by rememberUpdatedState(viewModel.products.collectAsLazyPagingItems())
Scaffold(
topBar = {
TopAppBar(
contentColor = Color.Black,
backgroundColor = Color.White,
title = {
Text(
"Home",
maxLines = 1,
)
},
navigationIcon = {
IconButton(onClick = { /* do something */ }) {
Icon(
imageVector = Icons.Filled.Menu,
tint = Color.Black,
contentDescription = "Localized description"
)
}
},
)
},
content = {
PagingGrid(data = result, content = { ProductCard(it)})
})
}
Output
就是这样了!
感兴趣的可以查看我的Github上的完整实现.
今天的内容就分享到这里啦!
一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!