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

news2024/9/23 21:32:38

使用共享 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: CoroutineScope
    get() = 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: LoginViewModelDelegate
  private var stateSubscription: KmmSubscription!
  
  init(viewModelDelegate: LoginViewModelDelegate) {
    self.viewModelDelegate = viewModelDelegate
    subscribeState()
  }
  
  // Remember to clear and unscubscribe when no more needed
  deinit {
    viewModelDelegate.onCleared()
    stateSubscription.unsubscribe()
  }
  
  func login() {
    viewModelDelegate.login()
  }
  
  private func subscribeState() {
    stateSubscription = viewModelDelegate.uiState.subscribe(
      onEach: { state in
        self.state = state!
      },
      onCompletion: { error in
        if 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 HomeScreen
        it.copy(isLoggedIn = true)
      }
    }.onFailure {
      _uiState.update { error ->
        // It can be consumed by the UI to display a Toast
        it.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.coloradmin.cn/o/986271.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RHCSA-VMware Workstation Pro-Linux基础配置命令

1.代码命令 1.查看本机IP地址&#xff1a; ip addr 或者 ip a [foxbogon ~]$ ip addre [foxbogon ~]$ ip a 1&#xff1a;<Loopback,U,LOWER-UP> 为环回2网卡 2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP>为虚拟机自身网卡 2.测试网络联通性&#xff1a; [f…

【0907作业】写一个shell脚本,将以下内容放到脚本中

在家目录下创建目录文件&#xff0c;dir在dir下创建dir1和dir2把当前目录下的所有文件拷贝到dir1中&#xff0c;把当前目录下的所有脚本文件拷贝到dir2中把dir2打包并压缩为dir2.tar.xz再把dir2.tar.xz移动到dir1中解压dir1中的压缩包使用tree工具&#xff0c;查看dir下的文件 …

vue3:5、组合式API-reactive和ref函数

<script setup> /* reactive接收一个对象类型的数据&#xff0c;返回一个响应式的对象 *//*** ref:接收简单类型或复杂类型&#xff0c;返回一个响应式对象* 本质&#xff1a;是在原有传入数据的基础上&#xff0c;外层报了一层对象&#xff0c;包成了复杂类型* 底层&…

宇凡微YE09合封芯片,集成高性能32位mcu和2.4G芯片

合封芯片是指将主控芯片和外部器件合并封装的芯片&#xff0c;能大幅降低开发成本、采购成本、减少pcb面积等等。宇凡微YE09合封芯片&#xff0c;将技术领域推向新的高度。这款高度创新性的芯片融合了32位MCU和2.4G芯片&#xff0c;为各种应用场景提供卓越的功能和性能。 32位M…

CSDN: ABTest流量分层分桶机制

在互联网行业&#xff0c;无论是构建搜索推荐系统&#xff0c;还是智能营销等场景&#xff0c;都是围绕用户进行不同的实验&#xff0c;从各项指标上观察用户对不同交互、流程、策略、算法等反馈&#xff0c;进而对产品进行迭代改进。 本文的goal&#xff1a;在进行了模型的线下…

STM32 FreeRTOS 内存问题

1. STM32L151C8T6 内存&#xff0c;64Kb 的Flash&#xff08;代码就是烧录在这里面的&#xff09;&#xff0c;16Kb 的RAM&#xff0c;程序跑起来之后的内存&#xff0c;相当于我们高考时发的草稿纸&#xff0c;直接影响程序的运行速度&#xff0c;可以用STM32 CubeMx 软件直接…

JMeter压力测试 5分钟让你学会如何并发压测接口

文章目录 地址下载启动 使用 地址 JMeter官网下载&#xff1a;https://jmeter.apache.org/download_jmeter.cgi 下载 最新款的jmeter需要java8的支持&#xff0c;请自行安装jdk8或以上的版本 根据系统不同系统下载zip或者是tgz格式的压缩包&#xff0c;并解压&#xff0c;博…

Google 在 2023 开发者大会上的 AI 革命

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页 ——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

对线程池设置做压测

线程池代码 Configuration public class ThreadPoolConfig {// 核心线程池大小private int corePoolSize 24;// 最大可创建的线程数private int maxPoolSize 25;// 队列最大长度private int queueCapacity 100;// 线程池维护线程所允许的空闲时间private int keepAliveSeco…

qt作业day4

//clock_exercise.cpp#include "clock_timer.h" #include "ui_clock_timer.h"//时间事件处理函数 void Clock_Timer::timerEvent(QTimerEvent *event) {if(event->timerId() time_id){sys_tm QDateTime :: currentDateTime(); // int year sy…

docker-compose安装redis

基于docker-compose快速安装redis 目录 一、目录结构 1、docker-compose.yml 2、redis.conf 二、连接使用 一、目录结构 1、docker-compose.yml version: 3 services:redis:image: registry.cn-hangzhou.aliyuncs.com/zhengqing/redis:6.0.8 # 镜像red…

高通DSP架构和HVX指令介绍

1. Qualcomm Snapdragon™处理器 Qualcomm Snapdragon™是高通的移动平台处理器,是一种系统级芯片(SoC),包含了CPU、GPU、DSP、调制解调器、无线电、摄像头处理器、安全处理器等多种功能。Snapdragon处理器广泛应用于智能手机、平板电脑、智能手表、智能音箱等移动设备中。…

使用ChatGLMTokenizer处理json格式数据

我下载了一些中文wikipedia数据&#xff0c;准备采用ChatGLMTokenizer对齐进行清洗&#xff0c;整理为预训练语料。 import numpy as np import json from tqdm import tqdm from chatglm_tokenizer.tokenization_chatglm import ChatGLMTokenizertokenizer ChatGLMTokenizer…

【算法专题突破】双指针 - 三数之和(7)

目录 1. 题目解析 2. 算法原理 3. 代码编写 写在最后&#xff1a; 1. 题目解析 题目链接&#xff1a;15. 三数之和 - 力扣&#xff08;Leetcode&#xff09; 题目就是要找出和为0的不重复的三元组&#xff0c; 注意三元组的每个元素是得不同的位置&#xff0c;那不重复又…

pnpm 升级

1. 在以下路径下删除pnpm包 2. 执行which pnpm&#xff0c;在结果目录中删除pnpm 3. sudo npm install -g pnpm 重新安装&#xff0c;node默认使用16

总结982

时间记录&#xff1a; 7:00~7:50早读&#xff0c;13年tex2 8:00~8:30列日任务知识点回顾 8:35~11:00数学108 11:17~11:56计网 2&#xff1a;30~4:09计网网课50min&#xff0c;做笔记10道题 4:22~6:23数据结构二叉树两道代码题 7&#xff1a;12~7:57数学进步本回顾 8:00…

深入探讨Kubernetes(K8s)在云原生架构中的关键作用和应用

文章目录 1. 容器化的应用程序管理2. 自动化扩展和负载均衡3. 容器编排和调度4. 存储管理5. 自动化滚动更新6. 多云和混合云部署7. 监控和日志8. 安全9. 社区支持和生态系统10. 未来展望案例 &#x1f388;个人主页&#xff1a;程序员 小侯 &#x1f390;CSDN新晋作者 &#x1…

webGIS外包开发框架及特点

WebGIS&#xff08;Web地理信息系统&#xff09;是一种用于在Web浏览器中展示和交互地理信息数据的技术。WebGIS开发需要使用特定的框架和工具来构建交互式地图应用程序。以下是一些常见的WebGIS开发框架以及它们的特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公…

算法基础-数学知识-容斥原理、博弈论

容斥原理、博弈论 容斥原理890. 能被整除的数&#xff08;二进制状态压缩版本&#xff0c;复杂度多一个Om&#xff09;890. 能被整除的数&#xff08;dfs版本&#xff09; 博弈论无限制nim游戏AcWing 891. Nim游戏AcWing 892. 台阶-Nim游戏&#xff08;待补&#xff09; 集合版…

Spring Boot集成Elasticsearch实战

文章目录 一、简介二、安装与配置Elasticsearch三、集成Spring Boot与Elasticsearch1. 添加依赖与配置文件2. 创建Elasticsearch数据模型3. 定义Elasticsearch仓库接口4. 实现Elasticsearch数据操作 四、基本查询与索引操作1. 插入与更新数据2. 删除数据与索引3. 条件查询与分页…