在Kotlin Multiplatform Mobile(iOS和Android)中共享视图模型
#kotlin #android #ios #kmm

Kotlin Multiplatform Mobile(KMM)脱颖而出,是一种独特的跨平台技术,与市场上可用的其他解决方案区分开来。与许多其他框架不同,KMM可以仅共享平台之间的业务逻辑,而用户界面仍然是本机的完全优势。这种方法通常涉及组建三个不同的团队来开发您的应用程序:iOS,Android和共享开发人员。 iOS团队专注于iOS平台的本机UI,Android团队负责Android UI,共享的开发人员专注于制定业务逻辑。这种协作模型促进了有效的开发,尤其是当您努力最大化代码共享时。 KMM简化了共享存储库,服务和数据库,但您的技术堆栈与其原理保持一致。

KMM提出的有趣的挑战之一是iOS和Android之间的视图模型共享,这项任务带来了增加的复杂性,但带来了可观的回报。

共享视图模型的情况

我在Kotlin Multiplatform移动设备上进行了我的Side Project,Wolfie.app的过程,这是一个专为iOS和Android平台设计的创新宠物伴侣应用程序。考虑到我的应用程序的最佳技术时,我权衡了颤抖,React Native和Kotlin Multiplatform手机的优点。选择扑朔迷离或反应的本地人会损害本地用户体验的影响 - 我渴望避免的结果。我的愿景是为iOS和Android用户提供无缝的本地体验。此外,鉴于我是一个单人物处理网络,后端和移动开发的团队,因此采取完整的本地开发途径可能会消耗我所有的可用时间。这种上下文使Kotlin MultipLatform移动移动成为理想的选择。

作为iOS开发人员,我发现采用KMM的最初步骤比对于Android开发人员的陡峭得多。对于那些更倾向于Android开发的人,清单将相对较短。这是一些要考虑的关键组件:

  • kotlin :基本要求。即使您在iOS开发方面有背景,Kotlin也很容易,即使在您的就职项目上也可以很容易地学习。
  • Swift :iOS开发的显而易见的必要性。如果您缺乏Mac,那么收购一个就必须。
  • Swiftui和JetPack组成:声明性开发范式,为UI设计带来了新的精致水平。
  • coroutines :此并发框架起着关键作用,因为您的大多数逻辑,存储库和数据库操作都将依靠它。如果您拥有反应性编程的经验,那么这将成为宝贵的基础。值得注意的是,Coroutines还可以在iOS开发中找到应用。
  • KTOR:您对任何API连接的坚定盟友。

除了这些必需品之外,很多图书馆都在等待您的考虑。但是,本文将专门研究共享视图模型的主题,探索两个值得注意的库和自己动手的方法。

## moko-mvvm github

tl; dr
许可证:Apache 2.0
首次公开发行是2019年10月。

Github上的760星
34公开问题
14个贡献者

在Kotlin Multiplatform Mobile(KMM)中共享视图模型时, Moko-MVVM 库(GitHub)引起了极大的关注。该图书馆拥有Apache 2.0许可,于2019年10月发布了首次公开发布。在Github上,Moko-MVVM在Github上占据了760颗星的惊人数量,展示了其在开发人员社区中的知名度。在一个多年来一直关注KMM的团队的支持下,该图书馆将自己作为有力的竞争者展示。但是,尽管它具有巨大的希望,但在采用之前有一些考虑因素。

Moko-MVVM的关键优势是致力于推进KMM生态系统的公司的强大支持。然而,存在固有的权衡,以图书馆与声明性UI实施的一致性为中心。重要的是要注意,Moko-MVVM更多地倾向于Uikit而不是Swiftui,可能会影响其与最新技术的兼容性。在我的写作时,似乎没有与Xcode 14.3.1兼容的库,或者至少在文档之后无法无缝实现兼容性。鉴于这些情况,我选择了暂时推迟对Moko-MVVM的使用。

值得注意的是,Moko-MVVM与来自同一开发人员组的另一个值得注意的库找到了协同作用: kswift GitHub)。这个互补的图书馆提供了大量的Kotlin/本机API翻译,以增强跨平台开发。此外,它促进了对iOS的Coroutine支持,进一步简化了您的开发工作流程。

Moko-MVVM持有的承诺是不可否认的,并且与专门针对KMM的公司保持一致,这增加了其信誉。但是,对于那些投资于Swiftui并寻求与最新iOS技术的无缝兼容性的人,有必要仔细考虑其以Uikit为中心的方法。随着KMM景观的不断发展,Moko-Mvvm和Kswift等库之间的协同作用可能变得越来越引人注目。

kmm-viewModel github

tl; dr
许可证:麻省理工学院
首次公开发行于2022年12月。
353颗恒星
4个公开问题
2个贡献者

在与Kotlin Multiplatform Mobile(KMM)跨平台共享视图模型的领域中, kmm-viewModel library 库(GitHub)成为一个显着的竞争者。 KMM-ViewModel凭借MIT许可证及其在2022年12月的首次公开发布,是该领域的一个相对较新的球员,但它已经在Github上引起了353颗星的关注。虽然处于Alpha阶段,但它与Swiftui的兼容性将其与盒子隔开。此外,其麻省理工学院许可在潜在的维护工作方面具有有利的灵活性,尤其是在支持的情况下。

尽管当前具有开发阶段,KMM-ViewModel提供了一个非凡的功能:与Swiftui无缝集成,Swiftui是现代iOS应用程序开发中的关键框架。对于那些优先考虑Swiftui的声明性和直观方法来设计用户界面设计的人来说,此属性可能特别诱人。通过在KMM-ViewModel库和Swiftui之间提供直接的桥梁,开发人员可以在平台之间实现和谐的协同作用,为iOS和Android的用户提供一致且类似的本地体验。

值得注意的是,KMM-ViewModel是一个不断发展的库,仍在穿越其alpha阶段。但是,它与Swiftui的一致性强调了其成为跨平台开发的强大工具的潜力。鉴于其相对较新的进入景观,随着进一步的成熟,监测其进展是谨慎的。

kmm-viewModel的一个有趣的方面在于它依赖 kmp-nativecoroutines 库(GitHub)。这个辅助库通过为iOS提供了现代移动应用程序开发的关键组成部分来补充KMM-ViewModel。 Coroutines对于异步编程和管理并发是关键的,而KMP-NativeCoroutines的包含则强调了对跨平台的强大开发体验的承诺。

虽然KMM-ViewModel保留在Alpha中,但它与SwiftUI的兼容性和对Coroutine支持的强调信号是无缝跨平台视图模型共享的潜在突破。随着它继续成熟并收集开发人员社区的反馈,它可能成为KMM工具包中的重要资产。

自定义解决方案:制作Coroutines支持

在您的Kotlin Multiplatform Mobile(KMM)项目中追求对iOS和Android平台的协调支持时,自定义解决方案提供了一种多功能方法。通过制定共享功能和类,您可以弥合差距并确保异步操作的无缝集成。

让我们逐步分解自定义解决方案:

1. StateFlowClass:共享状态流

// shared/src/commonMain/kotlin/me/blanik/sample/Couritines.kt

package me.blanik.sample

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class StateFlowClass<T>(private val delegate: StateFlow<T>) : StateFlow<T> by delegate {
    fun subscribe(block: (T) -> Unit) = GlobalScope.launch(Dispatchers.IO) {
        delegate.collect {
            block(it)
        }
    }
}

fun <T> StateFlow<T>.asStateFlowClass(): StateFlowClass<T> = StateFlowClass(this)

此片段建立了一个StateFlowClass,该StateFlowClass包裹StateFlow并提供subscribe函数。它采用Coroutines来弥合平台之间的差距,确保数据收集在适当的线程上发生。

2.共享调度员

// shared/src/commonMain/kotlin/me/blanik/sample/Dispatchers.kt
package me.blanik.sample.database

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

expect val Dispatchers.IO: CoroutineDispatcher

在本节中,为两个平台设置了共享调度程序。在Android上,我们对Android的Dispatchers.IO进行了直接的映射。对于iOS,我们需要更多上下文:

3. iOS的自定义Coroutine调度员

// shared/src/iosMain/kotlin/me/blanik/sample/Disaptchers.kt
package me.blanik.sample

import kotlinx.coroutines.*
import platform.darwin.*

actual val Dispatchers.IO: CoroutineDispatcher
    get() = IODispatcher

@OptIn(InternalCoroutinesApi::class)
private object IODispatcher : CoroutineDispatcher(), Delay {
    // Implementation details...
}

在此代码块中,我们为iOS定义了一个自定义的Coroutine调度程序。该调度员利用iOS的调度机制来确保Coroutines在主队列上运行并适当处理延迟。

4.流动公用事业

// shared/src/commonMain/kotlin/me/blanik/sample/FlowUtils.kt
package me.blanik.sample.database

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
    // Implementation details...
}

// Helper extension
internal fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)

// Remove when Kotlin's Closeable is supported in K/N
interface Closeable {
    fun close()
}

此片段引入了CFlow,该类别可以消耗Swift/Objective-C的基于流的API。它提供了一种处理这些语言订阅的干净方法。

5.特定于平台的实现

安卓:

// shared/src/androidMain/kotlin/me/blanik/sample/Dispatchers.kt
package me.blanik.sample

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

actual val Dispatchers.IO: CoroutineDispatcher
    get() = Dispatchers.IO

ios:

// shared/src/iosMain/kotlin/me/blanik/sample/Disaptchers.kt
package me.blanik.sample

import kotlinx.coroutines.*
import platform.darwin.*

actual val Dispatchers.IO: CoroutineDispatcher
    get() = IODispatcher

@OptIn(InternalCoroutinesApi::class)
private object IODispatcher : CoroutineDispatcher(), Delay {
    // Implementation details...
}

在这里,我们通过为调度员提供特定于平台的实现来最终确定设置。对于Android,我们只需映射到Android的内置调度程序即可。对于iOS,我们深入研究了自定义调度程序,该调度器弥合了Kotlin Coroutines和iOS的调度机制之间的差距。

有了这些组件,您已经创建了一个全面的解决方案,用于管理KMM项目中的Coroutines和异步操作。这种方法使您的项目能够在iOS和Android平台上无缝处理并发性,从而促进一致的功能和性能。

实施KMM-ViewModel:逐步指南

将KMM-ViewModel纳入您的Kotlin Multiplatform Mobile(KMM)项目为跨平台的无缝视图模型打开了新的途径。让我们逐步研究实施过程:

  1. 将您的项目升级到Kotlin 1.9:

在您的build.gradle.kts文件中,将kotlin版本更新为1.9.0:

   plugins {
       id("com.android.application").version("8.1.0").apply(false)
       id("com.android.library").version("8.1.0").apply(false)
       kotlin("android").version("1.9.0").apply(false)
       kotlin("multiplatform").version("1.9.0").apply(false)
   }
  1. 将所需库添加到您的共享模块:

更新您的shared/build.gradle.kts文件以包括必要的依赖项:

   sourceSets {
       val multiplatformSettingsVersion = "1.0.0"
       val kmmViewModelVersion = "1.0.0-ALPHA-12"

       all {
           languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
       }

       val commonMain by getting {
           dependencies {
               implementation("com.russhwolf:multiplatform-settings-no-arg:$multiplatformSettingsVersion")
               implementation("com.russhwolf:multiplatform-settings-serialization:$multiplatformSettingsVersion")
               implementation("com.russhwolf:multiplatform-settings-coroutines:$multiplatformSettingsVersion")
               implementation("com.rickclephas.kmm:kmm-viewmodel-core:$kmmViewModelVersion")
           }
       }
   }
  1. 同步您的gradle文件:

添加新依赖项后确保同步您的gradle文件。

  1. 更新iOS的Podfile

在您的iosApp目录中,更新Podfile以包括所需的豆荚:

   target 'iosApp' do
     use_frameworks!
     platform :ios, '14.1'
     pod 'shared', :path => '../shared'
     pod 'KMPNativeCoroutinesAsync', '1.0.0-ALPHA-13'
     pod 'KMPNativeCoroutinesCombine', '1.0.0-ALPHA-13'
     pod 'KMPNativeCoroutinesRxSwift', '1.0.0-ALPHA-13'
     pod 'KMMViewModelSwiftUI', '1.0.0-ALPHA-12'
   end

在您的iosApp目录中运行pod install

  1. 创建您的第一个共享视图模型:

在您的共享模块中创建一个新的Kotlin文件SignInViewModel.kt

   package me.blanik.sample

   import com.rickclephas.kmm.viewmodel.*
   import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
   import kotlinx.coroutines.flow.*

   open class SignInViewModel: KMMViewModel() {
       private val _email = MutableStateFlow(viewModelScope, "")
       private val _password = MutableStateFlow(viewModelScope, "")

       @NativeCoroutinesState
       val email = _email.asStateFlow()
       @NativeCoroutinesState
       val password = _password.asStateFlow()

       fun setEmail(email: String) {
           _email.value = email
       }

       fun setPassword(password: String) {
           _password.value = password
       }
   }
  1. 添加iOS实现:

在您的iosApp目录中,创建一个名为KMMViewModel.swift的swift文件:

   import KMMViewModelCore
   import shared

   extension Kmm_viewmodel_coreKMMViewModel: KMMViewModel { }

您还需要更新ContentView.swift文件:

   import SwiftUI
   import KMMViewModelSwiftUI
   import shared

   extension ContentView {
       class ViewModel: shared.SignInViewModel {}
   }

   struct ContentView: View {
       @StateViewModel var viewModel = ViewModel()

       var body: some View {
           VStack {
               List {
                   Section(header: Text("Input")) {
                       HStack {
                           Text("Email")

                           TextField("Email here", text: Binding(get: {
                               viewModel.email
                           }, set: { value in
                               viewModel.setEmail(email: value)
                           }))
                       }

                       HStack {
                           Text("Password")

                           SecureField("Type here", text: Binding(get: {
                               viewModel.password
                           }, set: { value in
                               viewModel.setPassword(password: value)
                           }))
                       }
                   }

                   Section(header: Text("Output")) {
                       HStack {
                           Text("Email")

                           Text(viewModel.email)
                       }
                       HStack {
                           Text("Password")

                           Text(viewModel.password)
                       }
                   }
               }
           }
       }
   }

   struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
   }

将科特林的企业绑定到swiftui TextField组件:

方向是一种方式沟通。您需要将它们作为@State(或@Published)值附加,并由iOS进行双向通信和本机支持。要存档,您可以使用创建自定义绑定作为Getter和setter。

   Binding(get: {
       
   }, set: { value in
       viewModel.()
   })
  1. 在屏幕上创建标志:

在您的Android模块中,为登录屏幕创建一个可组合功能:

   @Composable
   fun SignInScreen(
       signInViewModel: SignInViewModel = SignInViewModel()
   ) {
       val emailState by signInViewModel.email.collectAsState()
       val passwordState by signInViewModel.password.collectAsState()

       Column {
           Text("Input")
           OutlinedTextField(
               label = { Text(text = "Email") },
               value = emailState,
               onValueChange = signInViewModel::setEmail
           )
           OutlinedTextField(
               label = { Text(text = "Password") },
               value = passwordState,
               onValueChange = signInViewModel::setPassword,
               visualTransformation = PasswordVisualTransformation(),
               keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
           )
           Text("Output")
           Text(text = emailState)
           Text(text = passwordState)
       }
   }
  1. 在MainActivity中实现屏幕上的符号:

在您的android MainActivity中,使用SignInScreen合并功能:

   class MainActivity : ComponentActivity() {
       private val viewModel: SignInViewModel by viewModels()

       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)

           setContent {
               MyApplicationTheme {
                   Surface(
                       modifier = Modifier.fillMaxSize(),
                       color = MaterialTheme.colors.background
                   ) {
                       Column {
                           SignInScreen(viewModel)
                       }
                   }
               }
           }
       }
   }

通过这些步骤,您已经成功地将KMM-ViewModel集成到了您的项目中,从而使iOS和Android平台之间的无缝视图模型共享。该综合指南应帮助您浏览实施过程,并利用Kotlin Multiplatform移动开发旅程中共享视图模型的力量。

KMM中的额外API连接

通过KTOR将API连接添加到Kotlin Multiplatform Mobile(KMM)项目可以大大增强您的应用程序的功能。让我们仔细研究将API调用集成到您现有的KMM项目中的步骤:

  1. 更新共享构建gradle配置:

在您的shared/build.gradle.kts文件中,将ktor依赖项添加到您的共同来源集:

   // shared/build.gradle.kts
   kotlin {
       // ...

       sourceSets {
           val ktorVersion = "2.3.3"

           val commonMain by getting {
               dependencies {
                   implementation("io.ktor:ktor-client-core:$ktorVersion")
                   implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
                   implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
                   implementation("io.ktor:ktor-client-serialization:$ktorVersion")
                   implementation("io.ktor:ktor-client-logging:$ktorVersion")
                   implementation("io.ktor:ktor-client-auth:$ktorVersion")
               }
           }

           // ...
       }
   }
  1. 创建API模型:

定义您的API响应模型和有效载荷。例如,在您的共享模块中创建ApiAuthSignInResponseApiAuthSignInPayload类:

   // shared/src/commonMain/kotlin/me/blanik/sample/network/ApiAuthSignIn.kt
   package me.blanik.sample.network

   import kotlinx.serialization.Serializable

   @Serializable
   data class ApiAuthSignInResponse(
       val accessToken: String,
       val refreshToken: String? = null
   )

   @Serializable
   data class ApiAuthSignInPayload(
       val username: String,
       val password: String,
       val keepSignIn: Boolean? = null,
       val device: String? = null
   )
  1. 创建API响应处理:

定义一个类以表示API错误响应和包含数据的响应。例如:

   // shared/src/commonMain/kotlin/me/blanik/sample/network/ApiResponse.kt
   package me.blanik.sample.network

   import io.ktor.util.date.GMTDate
   import kotlinx.serialization.Serializable

   @Serializable
   data class ApiError(
       val statusCode: Int = 400,
       val message: String = "",
       val timestamp: String = GMTDate().toString(),
       val errors: List<ApiErrorErrors>? = null
   )

   @Serializable
   data class ApiErrorErrors(
       val property: String = "",
       val children: List<ApiErrorErrors> = emptyList(),
       val constraints: Map<String, String> = emptyMap()
   )

   class ApiResponse<T>(success: T?, error: ApiError?) {
       var success: T? = success
       var failure: ApiError? = error
   }
  1. 创建API服务:

使用KTOR的HTTP客户端实现您的API服务。定义一个封装API端点并处理API请求的类。例如:

   // shared/src/commonMain/kotlin/me/blanik/sample/network/WolfieApi.kt
   package me.blanik.sample.network

   import io.ktor.client.*
   import io.ktor.client.call.receive
   import io.ktor.client.request.*
   import io.ktor.http.ContentType
   import io.ktor.http.HttpHeaders
   import io.ktor.http.contentType
   import io.ktor.http.isSuccess
   import io.ktor.serialization.Serializable
   import kotlinx.serialization.json.Json

   class WolfieApi {
       private val client by lazy {
           HttpClient {
               install(JsonFeature) {
                   serializer = KotlinxSerializer(Json {
                       ignoreUnknownKeys = true
                       prettyPrint = true
                   })
               }
           }
       }

       suspend fun authSignIn(payload: ApiAuthSignInPayload): ApiResponse<ApiAuthSignInResponse> {
           val response = client.post<HttpResponse>("$API_BASE_URL$AUTH_SIGN_IN") {
               contentType(ContentType.Application.Json)
               body = payload
           }

           return if (response.status.isSuccess()) {
               ApiResponse(response.receive(), null)
           } else {
               ApiResponse(null, response.receive())
           }
       }

       companion object {
           private const val API_BASE_URL = "https://api-x.wolfie.app/v2"
           private const val AUTH_SIGN_IN = "/auth/sign-in"
       }
   }

此类使用提供的有效负载定义了一个函数authSignIn,该功能将邮政请求发送到指定的API端点。它返回包含成功响应或错误响应的ApiResponse

  1. 在共享视图模型中使用API​​:

将API服务集成到您的共享视图模型中。使用WolfieApi类来拨打API调用,处理响应并相应地更新视图模型的状态。例如:

   // shared/src/commonMain/kotlin/me/blanik/sample/SignInViewModel.kt
   import me.blanik.sample.network.ApiAuthSignInPayload
   import me.blanik.sample.network.WolfieApi

   enum class ApiState {
       INIT,
       PENDING,
       SUCCESS,
       FAILURE
   }

   open class SignInViewModel : KMMViewModel() {
       private val wolfieApi = WolfieApi()

       // ...

       @NativeCoroutinesState
       val state = _state.asStateFlow()

       @NativeCoroutinesState
       val errorMessage = _errorMessage.asStateFlow()

       // ...

       suspend fun signIn() {
           _state.value = ApiState.PENDING

           val response = wolfieApi.authSignIn(
               ApiAuthSignInPayload(
                   username = _email.value,
                   password = _password.value
               )
           )

           if (response.success != null) {
               _state.value = ApiState.SUCCESS
           } else {
               _state.value = ApiState.FAILURE
               _errorMessage.value = response.failure?.message ?: null
           }
       }
   }
  1. 更新iOS实施:

在您的iOS实现(SWIFT)中,您可以显示API状态和错误消息:

   // iosApp/iosApp/ContentView.swift
   // ...
   struct ContentView: View {
       // ...

       var body: some View {
           VStack {
               // ...
               Section(header: Text("Output")) {
                   // ...
                   HStack {
                       Text("State")
                       Text(viewModel.state.rawValue)
                   }
                   HStack {
                       Text("Error message")
                       Text(viewModel.errorMessage ?? "—")
                   }
               }
               // ...
               Section(header: Text("Action")) {
                   Button("Sign in") {
                       Task {
                           try await viewModel.signIn()
                       }
                   }
               }
           }
       }
   }
  1. 更新Android实现:

在您的Android实施中,您还可以显示API状态和错误消息:

   // androidApp/src/main/java/me/blanik/sample/android/MainActivity.kt
   // ...
   @Composable
   fun SignInScreen(
       signInViewModel: SignInViewModel = SignInViewModel()
   ) {
       // ...
       val stateState by signInViewModel.state.collectAsState()
       val errorMessage by signInViewModel.errorMessage.collectAsState()

       Column {
           // ...
           Text("Output")
           Text(text = stateState.name)
           Text(text = errorMessage ?: "—")
           // ...
           Button(onClick = {
               GlobalScope.async(Dispatchers.Main) {
                   signInViewModel.signIn()
               }
           }) {
               Text(text = "Sign in")
           }
       }
   }

通过这些步骤,您使用KTOR成功地将API连接整合到了KMM项目中。您现在可以从共享视图模型中拨打API调用,并处理iOS和Android平台的响应。

概括

Kotlin Multiplatform Mobile(KMM)正在迅速发展,并且已成为生产应用程序的可行选择。社区正在增长,各种图书馆正在出现,以解决不同的用例,每个用例都有自己的优势和考虑因素。采用KMM时,仔细评估项目需求并选择正确的解决方案很重要。

在本指南中,我们探讨了如何使用KMM-ViewModel库在iOS和Android应用程序之间共享视图模型。通过创建共享的视图模型并使用KMM-ViewModel库,您可以确保您的业务逻辑在平台之间保持一致,同时允许本机开发人员专注于UI开发。请记住,共享模块开发人员需要在两个平台中进行精心处理,以有效地支持和维护共享代码库。

您可以在本指南GitHub上找到代码示例和实现详细信息。如果您还有其他问题或需要帮助,请随时通过LinkedIn与我联系。

kmm提供了一种有希望的方法来构建跨平台应用程序,随着生态系统的不断发展,这是一个令人兴奋的空间。