TODO List KMM 实践遇到的问题记录
前言
TODO List KMM是一个基于Kotlin Multiplatform Mobile实现的一个小项目,主要提供查看Todo task列表,增加/删除/编辑task功能,目前有加上sign in/sign up页面,但是没有整合后端,有时间可能会加上。当然,这并不重要,本篇文章主要为了介绍KMM项目结构和公共模块抽取的个人想法和学习实践过程的一个回顾。
项目地址在文末。
至今,虽然Kotlin Multiplatform可以直接使用Compose实现两端UI了,但是它还处于非稳定版本,
而且实际上在iOS上存在卡顿现象,所以TODO List还是使用SwiftUI/Jetpack Compose分别实现Android/iOS 界面
基本结构
作为一个KMM项目,Android/iOS 这两个模块是必不可少的,它们分别是用来实现Android/iOS页面和使用或构建数据逻辑相关的代码,而shared当然也很重要,shared是两平台的共用代码。在大多数项目中,我们的数据来源双端都是一致的,所以shared模块完全可以作为一个数据
提供者,不管是来源是后端api还是本地,都是没有任何异议的。当然,除此之外,还可以提供一些工具扩展类。
项目创建和配置
- 首先我们创建一个KMM项目,catalogs管理依赖库的方式很早就被推荐使用了,所以我们将准备好的
libs.versions.toml
文件copy到gradle
目录下composite build
同样也是比较推荐的一种做法,从nowinandroid项目中copy过来,并加以修改放到我们项目中,命名为build-logic
,里面有插件,
这些插件是为了减少的module下的gradle配置模板代码从而更易于管理。 - 配置好项目之后,确定shared模块主要需要实现哪些数据相关的功能,并将其分模块来实现,在该项目中有4个模块分别是core, data, database, model.
- 2.1 core 提供核心工具类或扩展方法
- 2.2 model 封装上层需要的数据结构,用于UI或者逻辑需要的过度Model类
- 2.3 database 提供本地存储和读取功能作用
- 2.4 data 数据源层,主要是combine本地或者远程api数据来源的组装和mapping等逻辑 除了以上,同样可以添加一个domain层用来处理复杂的用例(domain层是可有可无的)
- 根据配置精简各个模块的gradle 配置之后便可以专注于代码的编写了。在编写代码的时候,要考虑好哪些模块实现哪些功能,
解耦之类的问题。Noted: 很多时候,我们在UI层需要实现一些数据的转换(KMM Compiler生成的类和Swift的类型之间),在Swift上实现起来比较麻烦。
但是我们知道Kotlin中有Native和Object-C已经帮我们实现了很多数据的对应和方法,所以可以将这些转换放到iOSMain中实现。比如ByteArray和UIImageval data = bytes.usePinned { NSData.create( bytes = it.addressOf(0), length = bytes.size.toULong() ) } return UIImage(data)
其他
sqldelight 上的坑
虽然依赖库中的gradle 已经在lineropts中加上了-lsqlite3
,但是有时候在运行iOS平台项目还是会出现找不到的问题。
使用XCode打开iOS项目,在Build Settings中的Other linker flags上加上就好了
Koin
对于KMM项目,我个人觉得Koin是目前最舒服的一个依赖注入方式,其也只是作用于shared中的Kotlin代码,Swift还是需要通过调用init实例化操作。所以为了防止依赖注入的helper类随着项目扩大而mapping方法也增加到可怕的数量,建议一开始就管理分类。
Flow和协程
Swift的闭包很好用,对于suspend方法都有一个completionHandler
的闭包回调,对于Flow当然也可以使用闭包在相关的时候进行回调。
比如:
// onEach(it) onThrow(it) onComplete()是闭包参数
flow.onEach { onEach(it) }
.catch { onThrow(it) }
.onCompletion { onComplete() }
Note:需要注意的是,要清楚这个回调在哪个线程中,如果你需要在回调方法后实现dismiss当前界面或者其他UI操作,而协程又是在IO线程中执行,并没有切换会主线程再调用回调,这时候回调同样是在IO线程,而IO线程中的UI操作是无效果的(dismiss不会有效果)需要使用DispatchQueue.main切到主线程中
那么我们是应该注意在kotlin代码上主动切回主线程再进行回调还是让其在Swift上来考虑呢?