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还是本地,都是没有任何异议的。当然,除此之外,还可以提供一些工具扩展类。

项目创建和配置

  1. 首先我们创建一个KMM项目,catalogs管理依赖库的方式很早就被推荐使用了,所以我们将准备好的libs.versions.toml文件copy到gradle目录下
    composite build同样也是比较推荐的一种做法,从nowinandroid项目中copy过来,并加以修改放到我们项目中,命名为build-logic,里面有插件,
    这些插件是为了减少的module下的gradle配置模板代码从而更易于管理。
  2. 配置好项目之后,确定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层是可有可无的)
  1. 根据配置精简各个模块的gradle 配置之后便可以专注于代码的编写了。在编写代码的时候,要考虑好哪些模块实现哪些功能,
    解耦之类的问题。

    Noted: 很多时候,我们在UI层需要实现一些数据的转换(KMM Compiler生成的类和Swift的类型之间),在Swift上实现起来比较麻烦。
    但是我们知道Kotlin中有Native和Object-C已经帮我们实现了很多数据的对应和方法,所以可以将这些转换放到iOSMain中实现。比如ByteArray和UIImage

    val 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上来考虑呢?

TODO-LIST-KMM