推荐你一个基于Koin, Ktor & Paging等组件的KMM Compose Multiplatform项目
Kotlin Multiplatform Mobile(KMM)已经从一个雄心勃勃的想法发展成为一个稳定而强大的框架,为开发人员提供了在多个平台上无缝共享代码的能力。通过最近的稳定版本里程碑,KMM已成为跨平台开发领域的改变者。
环境设置
带有Kotlin Multiplatform插件的Android Studio
Kotlin版本:1.9.10 Gradle版本:8.1.1 用于演示的任何开放API - 在这里,我使用Internshala列表API来填充UI。 注意:使用Internshala的开放列表API,并没有使用不当的做法:)
设置Koin和Ktor
- 将两个依赖项添加到:shared模块中,我使用gradlelibrary目录作为依赖项。
io.insert-koin:koin-core:3.5.0
- 对于Ktor,根据您的API,有多个依赖项各自具有自己的目的。
- 由于注入将在平台级别进行,因此我们需要将初始化部分暴露给两个平台。为此,我们将创建一个带有initKoin()方法的Helper类,并从iOS和Android的应用程序类调用此方法。
- initKoin() → 我们在这里定义我们稍后需要的所有依赖项。
kt
//shared/src/commonMain/kotlin/di/Koin.kt
fun initKoin() = startKoin {
modules(networkModule)
}
private val networkModule = module {
single {
HttpClient {
defaultRequest {
url.takeFrom(URLBuilder().takeFrom("https://internshala.com/"))
}
install(HttpTimeout) {
requestTimeoutMillis = 15_000
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
install(Logging) {
level = LogLevel.ALL
logger = object : Logger {
override fun log(message: String) {
println(message)
}
}
}
}
}
}
- 从两个平台调用此方法
- • iOS:我们需要在2个不同的文件中进行更改。(共享iOS文件和特定于平台的文件)
kt
// :shared/src/iosMain/kotlin/Koin.ios.kt
class Koin {
fun initKoin() {
di.initKoin()
}
}
kt
// iosApp/iosApp/iOSApp.swift
import SwiftUI
import shared
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
init() {
KoinKt.doInitKoin()
}
- • Android:与iOS不同,对于Android,我们可以直接调用:shared initKoin()方法并初始化Koin。
kt
class App : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
}
将API响应与Result类进行映射
我们将在Ktor的HttpClient上编写一个小的扩展,用于执行所有API调用并将响应包装在Result(成功/错误)中,并附带适当的消息。
kt
suspend inline fun <reified T> HttpClient.fetch(
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 R> {
class Success<out R>(val value: R) : Result<R>
data object Loading : Result<Nothing>
class Error(val throwable: Throwable) : Result<Nothing>
}
设置分页库
在实施Koin和Ktor之后,接下来是分页库。我们将使用来自cashapp的开源库。 由于Paging提供了多种方法来处理错误和API响应,我们将创建一个组合,以处理这种行为。
kt
@Composable
fun <T : Any> PagingListUI(
data: LazyPagingItems<T>,
content: @Composable (T) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(data.itemCount) { index ->
val item = data[index]
item?.let { content(it) }
Divider(
color = UiColor.background,
thickness = 10.dp,
modifier = Modifier.border(border = BorderStroke(0.5.dp, Color.LightGray))
)
}
data.loadState.apply {
when {
refresh is LoadStateNotLoading && data.itemCount < 1 -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "No Items",
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
}
}
}
refresh is LoadStateLoading -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = UiColor.primary
)
}
}
}
append is LoadStateLoading -> {
item {
CircularProgressIndicator(
color = UiColor.primary,
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
refresh is LoadStateError -> {
item {
ErrorView(
message = "No Internet Connection.",
onClickRetry = { data.retry() },
modifier = Modifier.fillParentMaxSize()
)
}
}
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 = androidx.compose.ui.graphics.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 = androidx.compose.ui.graphics.Color.Red
)
OutlinedButton(
onClick = onClickRetry, modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
) {
Text(text = "Try again")
}
}
}
设置App和Internship屏幕的UI部分
我们的应用程序入口点 - App.kt
kt
// :shared/src/commonMain/kotlin/App.kt
@Composable
fun App() {
MaterialTheme {
val screens = Screen.values()
var selectedScreen by remember { mutableStateOf(screens.first()) }
Scaffold(
bottomBar = {
BottomNavigation(
backgroundColor = Color.White,
modifier = Modifier.height(64.dp)
) {
screens.forEach { screen ->
BottomNavigationItem(
modifier = Modifier.background(Color.White),
selectedContentColor = ui.theme.Color.textOnPrimary,
unselectedContentColor = Color.Gray,
icon = {
Icon(
imageVector = getIconForScreen(screen),
contentDescription = screen.textValue
)
},
label = { Text(screen.textValue) },
selected = screen == selectedScreen,
onClick = { selectedScreen = screen },
)
}
}
},
content = { getScreen(selectedScreen) }
)
}
}
@Composable
fun getIconForScreen(screen: Screen): ImageVector {
return when (screen) {
Screen.INTERNSHIPS -> Icons.Default.AccountBox
Screen.JOBS -> Icons.Default.Add
Screen.COURSES -> Icons.Default.Notifications
else -> Icons.Default.Home
}
}
@Composable
fun getScreen(selectedScreen: Screen) = when (selectedScreen) {
Screen.INTERNSHIPS -> InternshipsScreen().content()
Screen.JOBS -> JobsScreen()
Screen.COURSES -> CoursesScreen()
else -> HomeScreen()
}
获取分页数据的实习界面
ini
class InternshipsScreen : KoinComponent {
private val viewModel: InternshipViewModel by inject()
@Composable
fun content() {
val result by rememberUpdatedState(viewModel.internships.collectAsLazyPagingItems())
return Scaffold(
topBar = {
TopAppBar(
title = { Text("Internships") },
elevation = 0.dp,
navigationIcon = {
IconButton(onClick = { println("Drawer clicked") }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu")
}
},
actions = {
IconButton(onClick = { println("Search Internships!") }) {
Icon(imageVector = Icons.Default.Search, contentDescription = "Search")
}
},
backgroundColor = Color.White
)
},
drawerContent = { /*Drawer content*/ },
content = { PagingListUI(data = result, content = { InternshipCard(it) }) },
)
}
}