Kotlin Multiplatform Mobile(KMM)已經從一個雄心勃勃的想法發展成為一個穩定而強大的框架,為開發人員提供了在多個平臺上無縫共享代碼的能力。通過最近的穩定版本里程碑,KMM已成為跨平臺開發領域的改變者。
環境設置
帶有Kotlin Multiplatform插件的Android Studio
設置Koin和Ktor
- 將兩個依賴項添加到:shared模塊中,我使用gradlelibrary目錄作為依賴項。 io.insert-koin:koin-core:3.5.0
- 對于Ktor,根據您的API,有多個依賴項各自具有自己的目的。
- 由于注入將在平臺級別進行,因此我們需要將初始化部分暴露給兩個平臺。為此,我們將創建一個帶有initKoin()方法的Helper類,并從iOS和Android的應用程序類調用此方法。
- initKoin() → 我們在這里定義我們稍后需要的所有依賴項。
//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 = trueprettyPrint = true})}install(Logging) {level = LogLevel.ALLlogger = object : Logger {override fun log(message: String) {println(message)}}}}}
}
2個平臺調用此方法:
- iOS:我們需要在2個不同的文件中進行更改。(共享iOS文件和特定于平臺的文件)
// :shared/src/iosMain/kotlin/Koin.ios.kt
class Koin {fun initKoin() {di.initKoin()}
}
// 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。
class App : Application() {override fun onCreate() {super.onCreate()initKoin()}
}
將API響應與Result類進行映射
我們將在Ktor的HttpClient上編寫一個小的擴展,用于執行所有API調用并將響應包裝在Result(成功/錯誤)中,并附帶適當的消息。
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())elseResult.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響應,我們將創建一個組合,以處理這種行為。
@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
// :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.AccountBoxScreen.JOBS -> Icons.Default.AddScreen.COURSES -> Icons.Default.Notificationselse -> Icons.Default.Home}
}@Composable
fun getScreen(selectedScreen: Screen) = when (selectedScreen) {Screen.INTERNSHIPS -> InternshipsScreen().content()Screen.JOBS -> JobsScreen()Screen.COURSES -> CoursesScreen()else -> HomeScreen()
}
獲取分頁數據的實習界面
class InternshipsScreen : KoinComponent {private val viewModel: InternshipViewModel by inject()@Composablefun 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) }) },)}
}
總結:
KMM是一個在多個平臺上無縫共享代碼的能力的框架。已成為跨平臺開發領域的改變者。后續我會自己實戰一個KMM項目,包括Ktor的詳細講解,敬請期待。