jetpack compose 开发架构选择探讨(一)

2,229 阅读6分钟

最近jetpack compose发布了正式版本,在jetpack compose刚出来的时候就一直有在关注这个全新的ui框架,但是一直没有基于它去做一个完整的项目,只是去了解这个框架的原理、特性等,最近基于jetpack compose做了一个webrtc的视频通话项目webrtc_compose,在项目开发过程中进一步加深了对jetpack compose的理解,尤其思考了这个全新的ui框架到底适合哪种开发架构。

开发架构选择

虽然jetpack compose已经放出了1.0.0的正式版本,但是它离实际大规模应用可能还有一大段路要走,因为一个全新的框架需要时间来完善相关的周边库,以及需要大型项目来验证选择合适的开发架构。今天我就来简单的讲讲目前主流的开发架构和jetpack compose的结合以及优缺点和目前需要解决的问题。

本文提到的例子具体代码都在compose_architecture中,有需要的可以自取,同时也可以帮忙点点赞

原有安卓开发架构的选择

在安卓原有view体系中,比较流行的开发架构有MVC、MVP、MVVM、MVI、CLEAN等,由于jetpack compose是声明式ui框架,对于需要持有view引用的mvc mvp等显然无法适用,同时由于clean的重点在于数据以及逻辑的分层,在ui层可以选用MVVM和MVI等,所以本文也不会分析。因此我们主要来分析下MVVM和MVI和jetpack compose的结合

MVVM

说到MVVM开发架构,其实对于原有的安卓view体系中的MVVM并不是完全的MVVM,因为MVVM最初就是为声明式ui来设计的,而原有的安卓view体系并不是声明式ui,因此使用起来总有些不伦不类,不过现在jetpack compose的出现完美的解决这个问题。由于jetpack compose和jetpack viewmodel完美兼容,因此在jetpack compose中实现MVVM和原有view体系中差别并不大,区别就是可以用声明式的方式更方便的完成ui开发。 MVVM

我们来看个简单的例子,我们用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

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通信问题

对于一个应用来说,通常不可能只会有一个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中的实现