最近jetpack compose发布了正式版本,在jetpack compose刚出来的时候就一直有在关注这个全新的ui框架,但是一直没有基于它去做一个完整的项目,只是去了解这个框架的原理、特性等,最近基于jetpack compose做了一个webrtc的视频通话项目( webrtc_compose ),在项目开发过程中进一步加深了对jetpack compose的理解,尤其思考了这个全新的ui框架到底适合哪种开发架构。
虽然jetpack compose已经放出了1.0.0的正式版本,但是它离实际大规模应用可能还有一大段路要走,因为一个全新的框架需要时间来完善相关的周边库,以及需要大型项目来验证选择合适的开发架构。今天我就来简单的讲讲目前主流的开发架构和jetpack compose的结合以及优缺点和目前需要解决的问题。
在安卓原有view体系中,比较流行的开发架构有MVC、MVP、MVVM、MVI、CLEAN等,由于jetpack compose是声明式ui框架,对于需要持有view引用的mvc mvp等显然无法适用,同时由于clean的重点在于数据以及逻辑的分层,在ui层可以选用MVVM和MVI等,所以本文也不会分析。因此我们主要来分析下MVVM和MVI和jetpack compose的结合
本文提到的例子具体代码都在compose_architecture中,有需要的可以自取,同时也可以帮忙点点赞
说到MVVM开发架构,其实对于原有的安卓view体系中的MVVM并不是完全的MVVM,因为MVVM最初就是为声明式ui来设计的,而原有的安卓view体系并不是声明式ui,因此使用起来总有些不伦不类,不过现在jetpack compose的出现完美的解决这个问题。由于jetpack compose和jetpack viewmodel完美兼容,因此在jetpack compose中实现MVVM和原有view体系中差别并不大,区别就是可以用声明式的方式更方便的完成ui开发。
我们来看个简单的例子,我们用MVVM实现一个简单的add count
首先先实现下Content(jetpack compose提倡单向数据流,即将状态提升到Screen中,Content不包含状态,只是单纯的ui界面,便于测试,具体参考官方教程 (jetpack compose 状态)
@Composable
fun Content1(count: Int, click: () -> Unit, click1: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "The count is $count")
Button(onClick = click) {
Text(text = "goto screen2")
}
Button(onClick = click1) {
Text(text = "add")
}
}
}
然后我们分析下这个例子只有一个count 状态和add 操作,因此这样来实现viewModel,这里基于jetpack viewmodel和livedata组件来实现
class MvvmViewModel : ViewModel() {
val countState = MutableLiveData(1)
fun add(num: Int) {
countState.postValue(countState.value as Int + num)
}
fun reduce(num: Int) {
countState.postValue(countState.value as Int - num)
}
}
接下来我们就需要实现Screen,在Screen中将Content和ViewModel结合起来
@Composable
fun Screen1(
navController: NavController
) {
val viewModel: MvvmViewModel = viewModel()
val count by viewModel.countState.observeAsState(0)
Content1(count = count,
{ navController.navigate("screen2") }
) {
viewModel.add(1)
}
}
我们可以看到借助于viewModel()方法我们可以在jetpack compose中很方便快捷的创建viewModel,同时也可以将livedata方便的转换为compose state,当state发生变化时,界面就会自动重组并显示,比原有安卓view体系使用起来方便很多。
MVI架构大多数人可能不是很了解,不过其实也不是很难,它把mvvm的双向绑定变成单向数据流,强调数据的单向流动和数据源唯一性,state不可变,view通过state渲染数据,viewmodel通过action改变state
Content代码和MVVM一样
ViewModel代码如下
class MVIViewModel : ViewModel() {
val viewState = MutableLiveData(ViewState())
val userIntent = Channel<UiAction>(Channel.UNLIMITED)
init {
handleAction()
}
private fun add(num: Int) {
viewState.value?.let {
viewState.postValue(it.copy(count = it.count + 1))
}
}
private fun reduce(num: Int) {
viewState.value?.let {
viewState.postValue(it.copy(count = it.count - 1))
}
}
private fun handleAction() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is UiAction.AddAction -> add(it.num)
is UiAction.ReduceAction -> reduce(it.num)
}
}
}
}
data class ViewState(val count: Int = 1)
sealed class UiAction {
class AddAction(val num: Int) : UiAction()
class ReduceAction(val num: Int) : UiAction()
}
}
Screen代码如下
@Composable
fun Screen1(
navController: NavController
) {
val viewModel: MVIViewModel = viewModel(navController = navController)
val viewState by viewModel.viewState.observeAsState(MVIViewModel.ViewState())
val coroutine = rememberCoroutineScope()
Content1(count = viewState.count,
{ navController.navigate("screen2") }
) {
coroutine.launch {
viewModel.userIntent.send(MVIViewModel.UiAction.AddAction(1))
}
}
}
通过上诉代码我们应该可以体会出mvvm和mvi之间的区别
对于一个应用来说,通常不可能只会有一个page,由于mvvm和mvi的viewmodel都是和page绑定的,对于多个page来说,要想实现跨page通信可能比较麻烦,这也是目前mvvm和mvi的一个大问题。不过这个问题也很好解决,我们可以定义一个方法可以获取其他page的viewmodel或者全局的viewmodel即可。不过在compose中该如何实现,首先我们要了解compose中的viewmodel是如何保存的
通常compose都是和navigation来实现page跳转的,对于上面的viewModel()方法,我们分析源码可以发现,每跳转一个新的page,它都会新建一个新的ViewModelStoreOwner(即NavBackStackEntry),所以如果我们不指定ViewModelStoreOwner的话我们是获取不到上一个page和全局的viewmodel的,因此我们可以提供一个创建viewModel的方法,在创建时候先去获取当前路由栈和全局中存在的viewModel,获取不到的话再新建或者抛一个异常出去,这样就可以在page中获取到其他page的viewModel,实现page间的通信了
代码也非常简单,如下
@Suppress("MissingJvmstatic")
@Composable
inline fun <reified VM : ViewModel> viewModelOfNav(
navController: NavController,
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM {
val javaClass = VM::class.java
var viewModelStoreOwner: ViewModelStoreOwner? = null
navController.backQueue.forEach {
if (it.existViewModel(javaClass, key = key)) {
viewModelStoreOwner = it
return@forEach
}
}
if (viewModelStoreOwner == null) {
val context = LocalContext.current
if (context is ViewModelStoreOwner && context.existViewModel(javaClass, key = key)) {
viewModelStoreOwner = context
}
}
return viewModel(
javaClass, viewModelStoreOwner = viewModelStoreOwner ?: checkNotNull(
LocalViewModelStoreOwner.current
) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}, key = key, factory = factory
)
}
class NotExistException : Exception("not exist")
class ExistFactory : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
throw NotExistException()
}
}
fun <VM : ViewModel> ViewModelStoreOwner.existViewModel(
modelClass: Class<VM>,
key: String? = null
): Boolean {
var isExist = true
val provider = ViewModelProvider(this, ExistFactory())
try {
if (key != null) {
provider.get(key, modelClass)
} else {
provider.get(modelClass)
}
} catch (e: NotExistException) {
isExist = false
}
return isExist
}
其实到这里我们会发现,我们的MVI架构加上多page通信,有点类似于flutter的bloc架构了,同样是单向数据流,只不过一个是sink和stream ,这里是action和state,bloc同样可以通过provideof实现page间通信,所以我们分析下来,很多架构都是类似的
今天我们简单分析了原有的安卓开发架构MVVM和MVI和jetpack compose的结合以及适配性,我们发现两者可以完美结合,甚至比原有安卓的view开发更加便捷高效,其实这个就是声明式ui框架的好处,接下来,我还会继续分析其他开发架构比如redux和bloc等和jetpack compose的结合,以及在compose中的实现