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

2023-11-09

使用共享 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/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 数据结构串的基本操作及KMP算法

    将串的基本操作C语言实现 xff0c 实现KMP算法算出NEXT函数和NEXTVAL的值 SqString h的基本内容 span class hljs keyword typedef span span class hljs keywor
  • 从头到尾彻底理解KMP(2014年8月22日版)

    从头到尾彻底理解KMP 作者 xff1a July 时间 xff1a 最初写于2011年12月 xff0c 2014年7月21日晚10点 全部删除重写成此文 xff0c 随后的半个多月不断反复改进 后收录于新书 编程之法 xff1a 面试和
  • ZJM 与纸条(KMP算法)

    问题描述 ZJM 的女朋友是一个书法家 xff0c 喜欢写一些好看的英文书法 有一天 ZJM 拿到了她写的纸条 xff0c 纸条上的字暗示了 ZJM 的女朋友 想给 ZJM 送生日礼物 ZJM 想知道自己收到的礼物是不是就是她送的 xff0
  • 【Week15作业 C】ZJM与纸条【KMP】

    题意 xff1a ZJM 的女朋友是一个书法家 xff0c 喜欢写一些好看的英文书法 有一天 ZJM 拿到了她写的纸条 xff0c 纸条上的字暗示了 ZJM 的女朋友 想给 ZJM 送生日礼物 ZJM 想知道自己收到的礼物是不是就是她送的
  • Cyclic Nacklace 【HDU - 3746】【KMP补周期】

    KMP算法的讲解 自己的领悟可随时提问 题目链接 题意 有一个字符序列 现在问你 序列后面最少补充几个元素使其恰能成为几个重复循环的序列 题目还是很良心的 让我们求字符串后面放几个字符可以使其变成周期字符串 所以还是可以想到用KMP的nex
  • KMP算法详解

    一 什么是KMP算法 KMP主要应用在字符串匹配上 KMP的主要思想是当出现字符串不匹配时 通过已知一部分之前已经匹配的内容 避免从头再去做匹配 所以KMP算法的重点就是如何记录已经匹配的信息 也就是next 数组的实现 二 什么是next
  • 超详细超全超好理解的KMP算法

    定义 KMP算法是一种字符串匹配算法 用于在一个主串中查找一个模式串的出现位置 先看这个视频 再看下边的代码实现 油管阿三哥讲KMP查找算法 中英文字幕 人工翻译 简单易懂 https www bilibili com video BV18
  • 剪花布条 【HDU - 2087】【KMP模板题】

    KMP教学链接 不懂的可以在线问 题意 2个字符串A B 问A中有多少个字符串B Input 输入中含有一些数据 分别是成对出现A B A和B不会超过1000个字符 如果遇见 字符 则表示测试结束 Output 输出B的个数 每个结果之间应
  • KMP算法之基础思想篇

    KMP算法是快速求字符串P 是不是字符串S的子串的一个算法 具体案例呢 可以看力扣的28题 实现 strStr 题意也很简单 就是找出P在S中出现的第一个位置 实际上就是找子串 这种最简单的方法就是暴力 直接两层for循环 O n m 的复
  • 求字符串可匹配的最大长度

    如 text abcdlijkfgd query abcdefg 最大匹配为 abcd 为4 编写一个函数 求字符串可匹配的最大长度 如果是完全匹配 则用很多种方法 如BF KMP sunday等字符串匹配算法 KMP是比较常见的 其思想也
  • kmp算法(最简单最直观的理解,看完包会)

    本文将以特殊的方式来让人们更好地理解kmp算法 不包括kmp算法的推导 接下来 我们将从朴素算法出发 在这之前 我们先设主串为S 模式串为T 我们要解决的询问是主串中是否包含模式串 即T是否为S的子串 版权声明 本文为原创文章 转载请标明出
  • 数据结构中Java实现KMP与BF算法对比

    public class KMPANDBF public int indexBfCount SeqString s SeqString t int begin int slen tlen i begin j 0 int count 0 sl
  • leetcode 028.实现strStr(),即查找重复字符串(KMP算法)

    前言 本题是经典的字符串单模匹配的模型 因此可以使用字符串匹配算法解决 常见的字符串匹配算法包括暴力匹配 Knuth Morris Pratt 算法 Boyer Moore 算法 Sunday 算法等 本文 前言 本题是经典的字符串单模匹配
  • BF,KMP,BM三种字符串匹配算法性能比较

    三种最基本的字符串匹配算法是BF KMP以及BM BF算法是最简单直接的匹配算法 就是逐个比较 一旦匹配不上 就往后移动一位 继续比较 所以比较次数很都 关于KMP和BM的详细介绍可以参考下面的两个link 是讲得比较好的 KMP http
  • 【扩展KMP】POJ_3450| HDU_2328 Corporate Identity

    原题直通车 POJ 3450 Corporate Identity HDU 2328 Corporate Identity 题意概述 找出N个串中最长公共子串 分析 一 可以直接枚举其中一个串的所有字串 跟所有串进行匹配找到结果 二 用其中
  • Oulipo

    点击打开链接 Problem Description The French author Georges Perec 1936 1982 once wrote a book La disparition without the letter
  • SDUTOJ KMP简单应用 【KMP】

    KMP简单应用 Time Limit 1000MS Memory limit 65536K 题目描述 给定两个字符串string1和string2 判断string2是否为string1的子串 输入 输入包含多组数据 每组测试数据包含两行
  • KMP比较简单的讲法。

    转载链接 http blog csdn net yearn520 article details 6729426 我们在一个母字符串中查找一个子字符串有很多方法 KMP是一种最常见的改进算法 它可以在匹配过程中失配的情况下 有效地多往后面跳
  • KMM 与公共(共享)模块中的 Java 源

    由于平台限制 我们无法将 Java 源代码与 Kotlin Multiplatform Mobile 一起使用 但如果 Kotlin 与 Java 100 兼容 为什么我们不能将 Java 与 Kotlin Multiplatform Mo
  • 如何在 Kotlin 多平台项目的共享模块中使用 @Parcelize 注解

    我正在开发一个 Kotlin 多平台应用程序 我想在我的模型类中使用 Parcelize 注释 但在 Kotlin Multiplatform 插件中 我使用的 kotlin 版本中的 Parcelize 注释位于 android exte

随机推荐

  • 【面向对象编程 C++】笔记(完结)

    前言 是为复习做的笔记 内容来自课本和老师的课件 不全面 第10章 类和对象 面向对象 注重过程 把事件分成小模块 类和对象的定义与访问 注意 类定义结束处有分号 类是一种类型 该类型的变量成为对象 类成员的访问特性 成员函数的定义 类内声
  • OpenCV的copyTo()函数讲解及应用

    Index 目录索引 写在前面 函数介绍 案例演示 参考文章 写在前面 继前文的setTo 函数讲解后 本文对和该函数用法类似的OpenCV中的copyTo 函数进行讲解 函数介绍 可以直接在 OpenCV参考文档 中查阅 该函数的用法为
  • MySQL高可用工具heartbeat简介

    MySQL高可用工具heartbeat简介 官网 http www linux ha org wiki Heartbeat 一 HeartBeat的作用 通过HeartBeat 可以将资源 IP以及程序服务等资源 从一台已经故障的计算机快速
  • ETL基础认知

    1 ETL基础认知 了解 问题1 如何将零散的数据 集中输入到数据仓库 ETL E 数据抽取 抽取的是其他数据源中的数据 T 数据转换 将数据转换为统一的格式 消除异常值 缺失值 对于错误的逻辑进行修改 L 数据加载 将不同数据源的数据处理
  • OpenGL GLFW入门篇 - 画矩形

    效果图 主体代码 void DrawRectangle void GLfloat xl yt xr yb w h glPushMatrix glLoadIdentity glTranslatef 0 0 0 0 0 f w 1 2 h 1
  • osgEarth中opengl版本的确定

    osgEarth VirtualProgram if defined OSG GLES2 AVAILABLE define GLSL VERSION 100 define GLSL VERSION STR 100 define GLSL D
  • 聚类算法总结

    最近整理一下聚类相关的东西 数据说明 凸集 在欧氏空间中 凸集是对于集合内的每一对点 连接该对点的直线段上的每个点也在该集合内 非凸 non convex 数据 类比上述可知 距离 相似度 首先我们要了解衡量对象间差异的方法对象可能是一个值
  • 关于带MinGW版本的codeblocks

    MinGW就是Windows移植版的GCC编译器 Codeblocks是IDE 这个软件的特点是可以让你自由选择想要使用的编译器 Code Blocks是一个免费 开源 跨平台的C C IDE 支持Windows Linux MacOSX
  • Boost电路的结构及工作原理

    Boost电路定义 Boost升压电路的英文名称为 theboostconverter 或者叫 step upconverter 是一种开关直流升压电路 它能够将直流电变为另一固定电压或可调电压的直流电 也称为直流 直流变换器 DC DCC
  • COPU陆首群教授应邀在开放原子全球开源峰会上做主旨演讲

    各位领导 各位专家 同志们 朋友们 大家下午好 祝贺开放原子开源基金会首届全球开源峰会成功举办 1970年是为人们称道的UNIX元年 也是开源在全球诞生之日 开源在全球流行至今已有52年了 自从1991年我国引进UNIX现代计算系统以来 中
  • DS内排—直插排序

    目录 题目描述 思路分析 AC代码 题目描述 给定一组数据 使用直插排序完成数据的升序排序 程序要求 若使用C 只能include一个头文件iostream 若使用C语言只能include一个头文件stdio 程序中若include多过一个
  • 在java中重复一个字符串n次的几种方法

    方法一 String format 0 n d 0 replace 0 s 方法二 new String new char n replace 0 s 方法三 JAVA 8 String join Collections nCopies n
  • (三)Unity开发Vision Pro——入门

    3 入门 1 入门 本节涵盖了几个重要主题 可帮助您加快visionOS 平台开发速度 在这里 您将找到构建第一个 Unity PolySpatial XR 应用程序的分步指南的链接 以及 PolySpatial XR 开发时的一些开发最佳
  • 目标检测数据集PASCAL VOC笔记

    PASCAL VOC 数据集的应用领域有Object Classification Object Detection Object Segmentation Human Layout Action Classification等 它的常用版
  • Acwing 116. 飞行员兄弟

    枚举所有开关的状态 0 2 16 1 16位二进制数 若某一位为1表示按一下 为0表示不按 按照该方案 对所有灯泡进行操作 所在行 所在列全部按一下 判断灯泡是否全亮 如果全亮的话 记录方案 include
  • 美团客户端技术团队招人啦

    非广告哈 帮好友发一则招聘 美团客户端团队在北京招人了 性能优化 基础组件相关的岗位都有 在看机会的或者想了解一下的 都可以通过文章最后面的联系方式进行联系 或者私信我 我拉个群你们细聊 想必大家都看过美团技术团队的博客 美团技术团队 1
  • SQL中DML语句(数据操作语言)

    表示数据操作语言 凡是对表当中的数据进行增删改的都是DML 目录 insert 插入数据 update 修改数据 delete 删除数据 insert 插入数据 语法格式 insert into 表名 字段名1 字段名2 字段名3 valu
  • 如何用burpsuite进行攻击

    一 使用Burpsuite进行攻击 1 第一步打开burpsuite 2 第二部点击Repeater 3 第三步点击粉笔形状的按钮 4 输入要攻击目标的ip地址与端口号 5 添加攻击报文 进行攻击 6 查看响应结果 完整界面展示如下 注意
  • 今日头条2017校招(出题数目)

    题目描述 头条的2017校招开始了 为了这次校招 我们组织了一个规模宏大的出题团队 每个出题人都出了一些有趣的题目 而我们现在想把这些题目组合成若干场考试出来 在选题之前 我们对题目进行了盲审 并定出了每道题的难度系数 一场考试包含3道开放
  • 使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile (KMM) 开发

    使用共享 MVI 架构实现高效的 Kotlin Multiplatform Mobile KMM 开发 文章中探讨了 Google 提供的应用架构指南在多平台上的实现 通过共享视图模型 View Models 和共享 UI 状态 UI St