The future of apps:Declarative UIs with Kotlin MultiPlatform (D-KMP) — Part 3/3
作者:Sahil Sharma
译者:不想翻身的鱼
基于声明式UI,Kotlin跨平台和MVI模式,分三篇文章来讲述新的D-KMP架构。
第三篇:D-KMP的分层和团队组织
最近更新:2021年5月18日
D-KMP架构下的ViewModel类
在我们的D-KMP架构下,ViewModel是一个Kotlin跨平台的类,它有5个组件:
class DKMPViewModel(repo: Repository) {
val stateFlow: StateFlow<AppState>
get() = stateManager.mutableStateFlow
private val stateManager by lazy { StateManager(repo) }
val navigation by lazy { Navigation(stateManager) }
val stateProvider by lazy { StateProvider(stateManager) }
val events by lazy { Events(stateManager) }
}
简单的描述一下这几个组件。
StateFlow
ViewModel里面的StateFlow是负责触发UI层进行重组的组件,每当它的值发生改变就会触发UI重组。
从上面的定义你可以发现,它的类型是一个叫AppState的数据类,定义的非常简单只持有了一个recompositionIndex
的属性:
data class AppState (
val recompositionIndex : Int = 0
)
StateFlow是一个只读组件,从读/写版本中读取自己的值(通过一个getter的属性计算),MutableStateFlow
被定义为StateManager组件的一个属性。
StateManager
StateManager是我们ViewModel的核心类。它管理者屏幕的状态和协程的scope。它还持有了MutableStateFlow
这个负责修改AppState这个StateFlow的值然后触发UI的重组。
class StateManager(repo: Repository) {
internal val mutableStateFlow = MutableStateFlow(AppState())
val screenStatesMap : MutableMap<ScreenIdentifier, ScreenState> = mutableMapOf()
val screenScopesMap : MutableMap<ScreenIdentifier,CoroutineScope> = mutableMapOf()
internal val dataRepository by lazy { repo }
fun triggerRecomposition() {
mutableStateFlow.value = AppState(mutableStateFlow.value.recompositionIndex+1)
}
}
Navigation
Navigation被所有平台共享,很好的保证了多平台的一致性。 也正是因为这个,不同平台的页面可以用几乎相同的语法来定义。下面贴上了我们实例工程maste/detail里面的的页面定义(github仓库的链接在本文最后)。 Compose上面(Kotlin):
SwiftUI上面(Swift):
StateProvider
StateProvider给页面提供状态。UI重组的时候会调用,并且从StateManager的screenStatesMap
里面获取数据。
...
Screen.CountriesList -> CountriesListScreen (
countriesListState = stateProviders.get(screenIdentifier)
...
)
...
Events
Events是定义在共享代码里面的函数,可以被各自平台的UI层调用。它们通常是执行一些能盖面APP状态的操作,然后触发新UI的重组。
...
Screen.CountriesList -> CountriesListScreen (
...
onFavoriteIconClick = { events.selectFavorite(countryName = it) }
)
...
在文章的最后我贴了github仓库链接,可以把代码拉下来自己跑一下可能更容易理解这些组件。
ViewModel 文件的组织
平台特性代码
关于平台特性的代码,首先我们需要通过平台特性的工厂方法创建一个DKMPViewModel
的实例。
在Android侧,需要把applicationContext作为参数传进去。因为在Android的框架下很多东西没有应用上下文无法工作,比如sqlite数据库。
DKMPViewModel.Factory.getAndroidInstance(context)
在iOS侧,我们不需要任何参数。
DKMPViewModel.Factory.getIosInstance()
收集数据流(StateFlow)
我们架构里面非常重要的一个一部分就是StateFlow(跟平台无关的观察者),它可以通过监听AppState状态的变化,来触发UI层的重组(recomposition)。
在我们的框架里面你需要记住一点,AppState就是一个recompositionIndex
的值,每次StateManager调用triggerRecomposition()
会让它的值加+1。
每次进行重组,UI都能通过StateProvider拿到一个新的状态。
接下来我们看一下如何在各个平台上配置StateFlow。
Android侧
在Android上📱StateFlow非常的直接,一行代码就能搞定。是因为Android团队已经实现了StateFlow里面的collectAsState()
方法作为Jetpack Compose这个库的一部分。
lass DKMPApp : Application() {
lateinit var model: DKMPViewModel
override fun onCreate() {
super.onCreate()
model = DKMPViewModel.Factory.getAndroidInstance(this)
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val model = (application as DKMPApp).model
setContent {
MyTheme {
MainComposable(model)
}
}
}
}
@Composable
fun MainComposable(model: DKMPViewModel) {
val appState by model.stateFlow.collectAsState()
val dkmpNav = appState.getNavigation(model)
dkmpNav.Router()
}
iOS侧
目前在iOS上收集数据流还需要写一些模板代码,但是我们希望JetBrains 能尽快优化一下。我们已经提了issue,你可以投个票。 同时我们需要初始化一个“listener”在平台特性的ViewModel上:
class AppObservableObject: ObservableObject {
let model : DKMPViewModel = DKMPViewModel.Factory().getIosInstance()
var dkmpNav : Navigation {
return self.appState.getNavigation(model: self.model)
}
@Published var appState : AppState = AppState()
init() {
model.onChange { newState in
self.appState = newState
}
}
}
@main
struct iosApp: App {
@StateObject var appObj = AppObservableObject()
var body: some Scene {
WindowGroup {
MainView(appObj: appObj)
}
}
}
struct MainView: View {
@ObservedObject var appObj: AppObservableObject
var body: some View {
let dkmpNav = appObj.dkmpNav
dkmpNav.router()
}
}
onChange
是作为一个扩展函数加到ViewModel上面,只有iOS需要。
fun DKMPViewModel.onChange(provideNewState: ((AppState) -> Unit)) : Closeable {
val job = Job()
stateFlow.onEach {
provideNewState(it)
}.launchIn(
CoroutineScope(Dispatchers.Main + job)
)
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
D-KMP数据层
在我们的D-KMP架构下,我们希望ViewModel和数据层是完全分离的。
数据层由Repository组成,Repository从各种数据源获取数据,比如webservice,运行时对象,平台配置,平台服务,sqlite数据库,实时数据库,本地文件等等。
Repository也负责管理本地的缓存机制,这样也能保证更好的做到只写一次,在各个平台上都能运行。不需要再各自平台再实现一遍。
Repository的角色是负责处理数据然后将未经格式化的数据给ViewModel,在ViewModel里面对数据进行格式化。
ViewModel不需要关心数据源,数据源或者缓存都是由Repository来负责管理。
KMP库
在KMP里面,我们当然不能用平台特有的库。但是也不用担心,因为新的KMP库会比旧的库做的更好。有的完全是用Kotlin重写了,有的可能是底层对原生库的封装。不管怎么样,你可以不用关心它具体是怎么实现的。
下面这个面,总结一些重要库目前的KMP支持情况:
对于声明式UI,StateFlow和Coroutines。我们快速额过一下其他几个吧:
- Ktor Http Client 由JetBrains开发,可以说是目前最好的KMP网路库。它在给各自平台封装了原生的Http client。
- Serialization 由JetB开发,是一个非常简单的序列化数据的库。经常和Ktor Http client 一起使用,用来解析JSON数据。
- SqlDelight,由Square开发。提供跨平台的本地SQLite数据库解决方案。
- 跨平台设置,由Russell Wolf (TouchLab)开发。Android侧是对SharedPreferences的封装,iOS是NSUserDefaults,JS是Storage。
除了已经存在的KMP库,任何人都可以自己开发一个KMP的库。用expect/actual 的特性来实现,这样就能将各自平台特性给封装起来。
有一些KMP库可能马上会发布:
- Location,对Android和iOS定位服务API的封装。
- Bluetooth,对Android和iOS蓝牙服务API的封装(更新:本文发布的时候,BLE的KMP库已经发布了)
- In-App-Purchases,包装Google Play和appStore的In-App-PurchasesAPI
- Firebase,官方的KMP实现比如Analytics,FireStore,Authentication等。不过也不用担心已经有三方对整个Firebase做了封装。
现在让我们看一下D-KMP架构下怎么来组织开发团队,其实跟传统的APP开发还是有比较大的不同的。 我们有4个主要的角色:
- 声明式UI 开发(JetpackCompose和SwiftUI)
- ViewModel开发(KMP)
- 数据层开发(KMP)
- 后端开发(Kotlin/JVM? Gloang?)
声明式UI开发
我们坚信在D-KMP的团队里面,UI开发应该是跨平台的,负责实现Jetpack Compose和SwiftUI的实现。考虑从到声明式UI框架的简单特点,同一个开发者来负责两端完全是可行的(也是有趣的)。聚焦到两个框架上,可以让开发者能更好的理解UI的发展趋势,以及实现更好的用户体验。
ViewModel 开发
这个角色非常重要,某种程度上需要承担一些管理的任务。是整个开发过程中的中心位置,需要对整个工程很了解。ViewModel的开发者需要同UI开发者一起定义所有页面状态的对象,还要和数据层开发者一起定义需要的数据。 同时ViewModel开发者还需要组织国际化的问题。
数据层(DataLayer)开发
这是一个对技术要求非常高的角色。数据层开发需要处理所有跟数据有关的东西,包括数据的缓存机制等等。数据开发需要组织好所有的数据,有时候甚至包括平台特性的数据源,比如定位或者蓝牙服务。这个角色需要对Kotlin跨平台非常熟悉,有时候设置需要写一些自定义的跨平台库。
后端(Backend)开发
在D-KMP的团队里面,这个角色仍然重要,因为需要和所有的app团队进行合作(Android,iOS,Web),但是又没有平台特性开发那么重要。 在D-KMP架构下,后端开关直接和数据层开发打交道。数据层开发把APP需要哪些数据定好。后端开发不需要了解具体APP层发生了什么。Webservice可以用任意的语言开发,比如Golang。如果你对Kotlin全栈感兴趣的话,也可以用Ktor这个框架,用的Kotlin/JVM技术。在服务端用Kotlin来写还有另外一个好处,可以让APP的数据层和服务端共用数据类的定义。
文章到此就结束了,感谢大家的阅读。 示例工程