使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile (KMM) 开发

使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile (KMM) 开发

文章中探讨了 Google 提供的应用架构指南在多平台上的实现。通过共享视图模型(View Models)和共享 UI 状态(UI States),我们可以专注于在原生端实现 UI 部分。
使用了简单的自定义抽象层,包括 KmmViewModel 和 KmmStateFlow,使得我们可以将共享的业务逻辑连接到原生 UI,而无需依赖于复杂的第三方库。这种方法有助于简化 KMM 开发,提高开发效率。
Google官方应用架构指南

https://developer.android.com/topic/architecture?hl=zh-cn

架构指南概览

  • androidApp(本地应用)
    • 视图可以使用 XML 或 Jetpack Compose 实现。
  • iosApp(本地应用)
    • 视图可以使用 UIKit 或 SwiftUI 实现。
  • shared(KMM 共享层)
    • 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 KmmStateFlow<T>(source: StateFlow<T>) : StateFlow<T>// androidMain
actual class KmmStateFlow<T> actual constructor(source: StateFlow<T>
) : StateFlow<T> by source// iosMain
fun interface KmmSubscription {fun unsubscribe()
}actual class KmmStateFlow<T> actual constructor(private val source: StateFlow<T>
) : StateFlow<T> 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: KmmStateFlow<LoginUiState> = ...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: KmmStateFlow<String> ...val articles: KmmStateFlow<List<Article>> ...val isLoading: KmmStateFlow<Boolean> ...
}

推荐的MVI View Model

data class HomeUiState(val userName: String,val articles: List<ArticleUiState>,val isLoading: Boolean,
)class HomeViewModel : KmmViewModel() {val uiState: KmmStateFlow<HomeUiState> ...
}

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: KmmStateFlow<LoginUiState> = _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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/102039.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络编程day5作业

1. 根据select TCP服务器流程图编写服务器&#xff08;上交&#xff09; #include <myhead.h> #define ERR_MSG(msg) do{\fprintf(stderr,"__%d__",__LINE__);\perror(msg);\ }while(0) #define PORT 6666 #define IP "192.168.114.50" int main(in…

内网渗透之凭据收集的各种方式

凭据收集是什么&#xff1f; 凭据收集是获取用户和系统凭据访问权限的术语。 这是一种查找或窃取存储的凭据的技术&#xff0c;包括网络嗅探&#xff0c;攻击者可以在网络嗅探中捕获传输的凭据。 凭证可以有多种不同的形式&#xff0c;例如&#xff1a; 帐户详细信息&#xf…

ESXI主机扩容(VCSA)

原因分析SCSI扩容 VMware为ESXI虚拟机硬盘扩容(需要先关闭ESXI) ESXI扩容前ESXI扩容 https://blog.csdn.net/tongxin_tongmeng/article/details/132652423 ESXI扩容后

2023年8大在线渗透测试工具介绍与分析

随着企业参与数字化运动&#xff0c;网络安全已成为大多数董事会讨论的一个重要方面。事实上&#xff0c;最近的一份报告显示&#xff0c;2022 年网络犯罪造成的损失总额达到惊人的 103 亿美元。 这就是在线渗透测试工具在网络安全中受到关注的地方。 今天&#xff0c;我们希…

安卓核心板的不同核心规格及架构介绍

安卓核心板是将核心功能封装的一块电子主板&#xff0c;集成芯片、存储器和功放器件等&#xff0c;并提供标准接口的芯片。 其特点&#xff1a; ● 能跑 Android 等操作系统 强大的功能及丰富的接口 支持 LCD/TP&#xff0c;Audio&#xff0c;Camera&#xff0c;Video&#…

回归预测 | MATLAB实现PCA-BP主成分降维结合BP神经网络多输入单输出回归预测

回归预测 | MATLAB实现PCA-BP主成分降维结合BP神经网络多输入单输出回归预测 目录 回归预测 | MATLAB实现PCA-BP主成分降维结合BP神经网络多输入单输出回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 MATLAB实现PCA-BP主成分降维算法结合BP神经网络多输入单输出回…

Docker部署(5)——使用docker run命令部署运行jar项目

对于一些简单的单体项目&#xff0c;可以使用 docker run 命令可以直接在命令行中运行容器&#xff0c;无需事先构建镜像。这相较于之前使用的 dockerfile 文件来运行部署项目相当于是另外一种简单的部署方法&#xff0c;关于之前使用dockerfile 文件来运行部署这种方法&#x…

uView实现全屏选项卡

// 直接复制粘贴即可使用 <template><view><view class"tabsBox"><u-tabs-swiper ref"uTabs" :list"list":current"current"change"tabsChange":is-scroll"false"></u-tabs-swiper&g…

【高危】Apache Airflow Spark Provider 反序列化漏洞 (CVE-2023-40195)

zhi.oscs1024.com​​​​​ 漏洞类型反序列化发现时间2023-08-29漏洞等级高危MPS编号MPS-qkdx-17bcCVE编号CVE-2023-40195漏洞影响广度广 漏洞危害 OSCS 描述Apache Airflow Spark Provider是Apache Airflow项目的一个插件&#xff0c;用于在Airflow中管理和调度Apache Spar…

stm32之30.DMA

DMA&#xff08;硬件加速方法&#xff09;一般用于帮运比较大的数据&#xff08;如&#xff1a;摄像头数据图像传输&#xff09;&#xff0c;寄存器-》DMA-》RAM 或者 RAM-》DMA-》寄存器提高CPU的工作效率 源码-- #include "myhead.h" #include "adc.h"#…

【LeetCode - 每日一题】2594. 修车的最少时间(23.09.07)

2594. 修车的最少时间 题意 给定每个师傅修车的时间和需要修的车辆总数&#xff0c;计算修理所有汽车需要的最少时间。师傅可以同时修车。 解法 二分 看到题目没有任何头绪&#xff0c;直接查看题解。 至于为什么用二分做呢&#xff0c;讨论区有个友友这么说到&#xff1a…

Web安全——穷举爆破下篇(仅供学习)

Web安全 一、常见的端口服务穷举1、hydra 密码穷举工具的使用2、使用 hydra 穷举 ssh 服务3、使用 hydra 穷举 ftp 服务4、使用 hydra 穷举 mysql 服务5、使用 hydra 穷举 smb 服务6、使用 hydra 穷举 http 服务7、使用 hydra 穷举 pop3 服务8、使用 hydra 穷举 rdp 服务9、使用…