Compose Multiplatform, Paging, Koin, Ktor

社区移动开发Android
Compose Multiplatform, Paging, Koin, Ktor

picture.image

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

picture.image

就是这样了!

感兴趣的可以查看我的Github上的完整实现.

今天的内容就分享到这里啦!

一家之言, 欢迎拍砖!

Happy Coding! Stay GOLDEN!

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