如何做网站超链接,网站建设时关键词要怎么选呢,在线制作图片代码,安徽华力建设集团网站使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile (KMM) 开发
文章中探讨了 Google 提供的应用架构指南在多平台上的实现。通过共享视图模型#xff08;View Models#xff09;和共享 UI 状态#xff08;UI States#xff09;#xff0c;我们可以专注于在原生端…
使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile (KMM) 开发
文章中探讨了 Google 提供的应用架构指南在多平台上的实现。通过共享视图模型View Models和共享 UI 状态UI States我们可以专注于在原生端实现 UI 部分。 使用了简单的自定义抽象层包括 KmmViewModel 和 KmmStateFlow使得我们可以将共享的业务逻辑连接到原生 UI而无需依赖于复杂的第三方库。这种方法有助于简化 KMM 开发提高开发效率。 Google官方应用架构指南 https://developer.android.com/topic/architecture?hlzh-cn 架构指南概览 androidApp本地应用 视图可以使用 XML 或 Jetpack Compose 实现。 iosApp本地应用 视图可以使用 UIKit 或 SwiftUI 实现。 sharedKMM 共享层 View Models 处理呈现逻辑并向本地 UI 发送 UI State。View Models 使用 Repositories 和 Use Cases 获取数据并执行业务逻辑。Use Cases 处理一些可重用的业务逻辑可以应用于不同的 View Models。Repositories 处理数据逻辑。它们公开了用于返回或更新数据的 CRUD 操作。Repositories 访问不同的数据源以在本地或远程获取或存储数据。
实现案例 https://github.com/Maruchin1/kmm-shared-mvi https://github.com/touchlab/KaMPKit KMM 抽象
为了实现这一架构我们需要引入两个简单的 KMM 抽象。一个用于 ViewModel另一个用于 StateFlow。
KmmViewModel
// commonMain
expect abstract class KmmViewModel constructor() {protected val scope: CoroutineScope
}// androidMain
actual abstract class KmmViewModel : ViewModel() {protected actual val scope: CoroutineScopeget() viewModelScope
}// iosMain
actual abstract class KmmViewModel {protected actual val scope CoroutineScope(SupervisorJob() Dispatchers.Main)fun onCleared() {scope.cancel()}
}在 Android 端我们只需使用 androidx.lifecycle.ViewModel使其像本地 ViewModel 一样运行。我们还将 viewModelScope 关联起来以便在 KmmViewModel 中启动异步操作。
在 iOS 端我们有一个自定义实现它使用 MainDispatcher 实例化 CoroutineScope。它还公开了一个额外的 onCleared 方法可以在本地端用于取消正在进行的异步操作。
KmmStateFlow
// commonMain
expect class KmmStateFlowT(source: StateFlowT) : StateFlowT// androidMain
actual class KmmStateFlowT actual constructor(source: StateFlowT
) : StateFlowT by source// iosMain
fun interface KmmSubscription {fun unsubscribe()
}actual class KmmStateFlowT actual constructor(private val source: StateFlowT
) : StateFlowT by source {fun subscribe(onEach: (T) - Unit, onCompletion: (Throwable?) - Unit): KmmSubscription {val scope CoroutineScope(Job() Dispatchers.Main)source.onEach { onEach(it) }.catch { onCompletion(it) }.onCompletion { onCompletion(null) }.launchIn(scope)return KmmSubscription { scope.cancel() }}
}在 Android 端我们只需将实现委托给标准的 StateFlow因此它的工作方式完全相同。在 iOS 端由于无法访问 CoroutineScope我们无法像标准方式一样收集 StateFlow。解决这个问题的方法是采用基于订阅的方式这在 RxJava 和其他 Rx* 库中很常见。我们添加了一个带有两个回调的 subscribe 方法它返回一个 KmmSubscription 实例。iOS 应用程序可以取消订阅从而取消 CoroutineScope。
IOS端实现
要在 iOS 应用程序中正确集成 KmmViewModel最简单且最灵活的方法是依赖委托模式。首先可以使用 ObjCName 注解专门为 iOS 应用程序更改共享的 View Model 名称。
ObjCName(LoginViewModelDelegate)
class LoginViewModel : KmmViewModel() {val uiState: KmmStateFlowLoginUiState ...fun login() {...}
}然后在本机 iOS 应用中我们创建一个视图模型包装器它在底层使用共享委托。
最重要的部分是 deinit 块。它通知视图模型委托应取消所有异步工作并关闭 UI State 订阅。这样当屏幕从导航堆栈中移除时就不会发生内存泄漏。
class LoginViewModel: ObservableObject {Published var state: LoginUiState LoginUiState.companion.default()private let viewModelDelegate: LoginViewModelDelegateprivate var stateSubscription: KmmSubscription!init(viewModelDelegate: LoginViewModelDelegate) {self.viewModelDelegate viewModelDelegatesubscribeState()}// Remember to clear and unscubscribe when no more neededdeinit {viewModelDelegate.onCleared()stateSubscription.unsubscribe()}func login() {viewModelDelegate.login()}private func subscribeState() {stateSubscription viewModelDelegate.uiState.subscribe(onEach: { state inself.state state!},onCompletion: { error inif let error error {print(error)}})}
}关键规则
1. 视图模型与屏幕一一对应 视图模型是屏幕级别的状态持有者。本地屏幕和共享视图模型之间存在一对一的关系。当我们在共享部分拥有 HomeViewModel 时我们应该在 Android 中拥有 HomeScreen / HomeFragment而在 iOS 中拥有 HomeView / HomeController。
2. 视图模型发出单一数据流 MVVM 和 MVI 之间的主要区别在于在 MVI 中对于每个屏幕我们有一个单一的不可变状态。当视图模型需要向本地 UI 发出一些数据时它应该定义一个不可变的*UiState数据类并使用 KmmStateFlow 发出它。 https://developer.android.com/topic/architecture/ui-layer https://developer.android.com/topic/architecture/ui-layer/stateholders 不推荐的MVI View Model
class HomeViewModel : KmmViewModel() {val userName: KmmStateFlowString ...val articles: KmmStateFlowListArticle ...val isLoading: KmmStateFlowBoolean ...
}推荐的MVI View Model
data class HomeUiState(val userName: String,val articles: ListArticleUiState,val isLoading: Boolean,
)class HomeViewModel : KmmViewModel() {val uiState: KmmStateFlowHomeUiState ...
}3. UI事件可触发UI状态更新 View Models使用命名方法例如fun login()处理UI事件如OnClick。方法执行业务逻辑后不返回值或触发事件而是更新UI状态以传递相关数据。 https://developer.android.com/topic/architecture/ui-layer/events data class LoginUiState(val isLoggedIn: Boolean,val errorMessage: String?)class LoginViewModel : KmmViewModel() {private val _uiState MutableStateFlow(LoginUiState.default())val uiState: KmmStateFlowLoginUiState _uiState.asKmmStateFlow()fun login() viewModelScope.launch {runCatching {loginUserUseCase()}.onSuccess {_uiState.update { // It can be consumed by the UI to navigate to HomeScreenit.copy(isLoggedIn true)}}.onFailure {_uiState.update { error -// It can be consumed by the UI to display a Toastit.copy(errorMessage getErrorMessage(error))}}}}4. 使用案例是可选的 并非每个应用都需要使用案例。当应用简单时直接在视图模型中访问存储库是可以的。但当应用引入更多逻辑需要转换、分组或执行复杂操作时应该考虑使用案例来封装这些逻辑以便在不同的视图模型中重用。 https://developer.android.com/topic/architecture/domain-layer https://medium.com/androiddevelopers/adding-a-domain-layer-bc5a708a96da 5. 使用案例是无状态的 使用案例负责执行一些逻辑操作可能涉及不同的存储库和不同类型的数据。然而使用案例本身不应保留任何内部状态。如果需要持久化或临时存储某些数据应该委托给存储库。
6. 一个数据类型对应一个存储库 每个存储库都代表一个数据类型的集合。如果我们有用户实体我们创建 UsersRepository。而对于文章我们创建 ArticlesRepository。存储库不应依赖于其他存储库。
在Android文档中我们可以找到关于构建多层存储库的信息。请记住这个更高级别的存储库有不同的目的。它不是使用不同的数据源来管理单一类型的数据而是使用其他存储库来管理某种聚合类型的数据。这就是它们有时被称为管理器的原因。
在MVI架构中我们首先应该使用使用案例来从不同的存储库中聚合数据。只有在我们的需求非常复杂使用使用案例不足以满足时我们才可以考虑引入多层存储库。
7. 存储库隐藏数据持久化细节 每个存储库都充当一个外观隐藏了数据持久化的详细信息。存储库的所有公共方法都应该接受并返回领域模型。在内部它们将领域模型映射到相应的远程API或本地数据库模型。 https://developer.android.com/topic/architecture/data-layer 结论
该架构适用于Android和iOS平台具有相同的演示逻辑的情况。它遵循Google的应用程序架构指南无需使用重型第三方库支持不可变UI状态和单向数据流代码共享比例高但需要注意iOS端的额外代码以避免内存泄漏。
参考 google应用架构指南 https://developer.android.com/topic/architecture/intro mvi框架 https://github.com/icerockdev/moko-mvvm https://arkivanov.github.io/Decompose/