推荐你一个基于Koin, Ktor & Paging等组件的KMM Compose Multiplatform项目

推荐你一个基于Koin, Ktor & Paging等组件的KMM Compose Multiplatform项目

Kotlin Multiplatform Mobile(KMM)已经从一个雄心勃勃的想法发展成为一个稳定而强大的框架,为开发人员提供了在多个平台上无缝共享代码的能力。通过最近的稳定版本里程碑,KMM已成为跨平台开发领域的改变者。

环境设置

带有Kotlin Multiplatform插件的Android Studio

https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile

Kotlin版本:1.9.10
Gradle版本:8.1.1
用于演示的任何开放API - 在这里,我使用Internshala列表API来填充UI。
注意:使用Internshala的开放列表API,并没有使用不当的做法:)

设置Koin和Ktor

  1. 将两个依赖项添加到:shared模块中,我使用gradlelibrary目录作为依赖项。
    io.insert-koin:koin-core:3.5.0
  2. 对于Ktor,根据您的API,有多个依赖项各自具有自己的目的。
  3. 由于注入将在平台级别进行,因此我们需要将初始化部分暴露给两个平台。为此,我们将创建一个带有initKoin()方法的Helper类,并从iOS和Android的应用程序类调用此方法。
  4. 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 = true
                    prettyPrint = true
                })
            }
            install(Logging) {
    
    
                level = LogLevel.ALL
                logger = object : Logger {
    
    
                    override fun log(message: String) {
    
    
                        println(message)
                    }
                }
            }
        }
    }
}
  1. 从两个平台调用此方法
  • 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())
    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响应,我们将创建一个组合,以处理这种行为。

@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.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()
}

获取分页数据的实习界面

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) }) },
        )
    }
}

输出的预览

GitHub

https://github.com/Prashant-123/kmm-ktor-koin
https://github.com/Kamel-Media/Kamel

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/134641833