本次IO大会上曝出了Compose1。0即将发布的消息,虽然API层面已趋于稳定,但真正要在项目中落地还少不了一套合理的应用架构。传统Android开发中的MVP、MVVM等架构在声明式UI这一新事物中是否还依旧可用呢? 本文以一个简单的业务场景为例,试图找出一种与Compose最契合的架构模式Sample:WanandroidSearch App基本功能:用户输入关键字,在wanandroid网站中搜索出相关内容并展示 功能虽然简单,但是集合了数据请求、UI展示等常见业务场景,可用来做UI层与逻辑层的解耦实验。前期准备:Model层 其实无论MVX中X如何变化,Model都可以用同一套实现。我们先定义一个DataRepository,用于从wanandroid获取搜索结果。后文Sample中的Model层都基于此Repo实现ViewModelScopedclassDataRepositoryInjectconstructor(){privatevalokhttpClientbylazy{OkHttpClient。Builder()。build()}privatevalapiServicebylazy{Retrofit。Builder()。baseUrl(https:www。wanandroid。com)。client(okhttpClient)。addConverterFactory(GsonConverterFactory。create())。build()。create(ApiService::class。java)}suspendfungetArticlesList(key:String)apiService。getArticlesList(key)}Compose为什么需要架构? 首先,先看看不借助任何架构的Compose代码是怎样的? 不使用架构的情况下,逻辑代码将与UI代码耦合在一起,在Compose中这种弊端显得尤为明显。常规Android开发默认引入了MVC思想,XML的布局方式使得UI层与逻辑层有了初步的解耦。但是Compose中,布局和逻辑同样都使用Kotlin实现,当布局中夹了杂逻辑,界限变得更加模糊。 此外,ComposeUI中混入逻辑代码会带来更多的潜在隐患。由于Composable会频繁重组,逻辑代码中如果涉及IO就必须当做SideEffect{}处理、一些不能随重组频繁创建的对象也必须使用remember{}保存,当这些逻辑散落在UI中时,无形中增加了开发者的心智负担,很容易发生遗漏。 Sample的业务场景特别简单,UI中出现少许remember{}、LaunchedEffect{}似乎也没什么不妥,对于一些相对简单的业务场景出现下面这样的代码没有问题:ComposablefunNoArchitectureResultScreen(answer:String){valisLoadingremember{mutableStateOf(false)}valdataRepositoryremember{DataRepository()}varresult:Listbyremember{mutableStateOf(emptyList())}LaunchedEffect(Unit){isLoading。valuetrueresultwithContext(Dispatchers。IO){dataRepository。getArticlesList(answer)。data。datas}isLoading。valuefalse}SearchResultScreen(result,isLoading。value,answer)} 但是,当业务足够复杂时,你会发现这样的代码是难以忍受的。这正如在React前端开发中,虽然Hooks提供了处理逻辑的能力,但却依然无法取代Redux。Android中的常见架构模式 MVP、MVVM、MVI是Android中的而一些常见架构模式,它们的目的都是服务于UI层与逻辑层的解耦,只是在解耦方式上有所不同,如何选择取决于使用者的喜好以及项目的特点 没有最好的架构,只有最合适的架构。 那么在Compose项目中何种架构最合适呢?MVP MVP主要特点是Presenter与View之间通过接口通信,Presenter通过调用View的方法实现UI的更新。 这要求Presenter需要持有一个View层对象的引用,但是Compose显然无法获得这种引用,因为用来创建UI的Composable必须要求返回Unit,如下:ComposablefunHomeScreen(){Column{Text(HelloWorld!)}} 官方文档中对无返回值的要求也进行了明确约束: Thefunctiondoesn’treturnanything。ComposefunctionsthatemitUIdonotneedtoreturnanything,becausetheydescribethedesiredscreenstateinsteadofconstructingUIwidgets。https:developer。android。comjetpackcomposementalmodel ComposeUI既然存在于Android体系中,必定需要有一个与Android世界连接的起点,起点处可能是一个Activity或者Fragment,用他们做UI层的引用句柄不可以吗? 理论上可以,但是当Activity接收Presenter通知后,仍然无法在内部获取局部引用,只能设法触发整体Recomposition,这完全丧失了MVP的优势,即通过获取局部引用进行精准刷新。 通过分析可以得到结论:MVP这种依赖接口通信的解耦方式无法在Compose项目中使用MVVM(WithoutJetpack) 相对于MVP的接口通信,MVVM基于观察者模式进行通信,当UI观察到来自ViewModle的数据变化时自我更新。UI层是否能返回引用句柄已不再重要,这与Compose的工作方式非常契合。 自从Android用ViewModel命名了某Jetpack组件后,在很多人心里,Jetpack似乎就与MVVM画上了等号。这确实客观推动了MVVM的普及,但是Jetpack的ViewModel并非只能用在MVVM中(比如如后文介绍的MVI也可以使用);反之,没有Jetpack,照样可以实现MVVM。 先来看看不借助Jetpack的情况下,MVVM如何实现?Activity中创建ViewModel 首先View层创建ViewModel用于订阅classMvvmActivity:AppCompatActivity(){privatevalmvvmViewModelMvvmViewModel(DataRepository())overridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)setContent{ComposePlaygroundTheme{MvvmApp(mvvmViewModel)将vm传给Composable}}}} Compose项目一般使用单Activity结构,Activity作为全局入口非常适合创建全局ViewModel。子Compoable之间需要基于ViewModel通信,所以构建Composable时将ViewModel作为参数传入。 Sample中我们在Activity中创建的ViewModel仅仅是为了传递给MvvmApp使用,这种情况下也可以通过传递Lazy,将创建延迟到真正需要使用的时候以提高性能。定义NavGraph 当涉及到Compose页面切换时,navigationcompose是一个不错选择,Sample中也特意设计了SearchBarScreen和SearchResultScreen的切换场景build。gradleimplementationandroidx。navigation:navigationcompose:latestversionComposablefunMvvmApp(mvvmViewModel:MvvmViewModel){valnavControllerrememberNavController()LaunchedEffect(Unit){mvvmViewModel。navigateToResults。collect{navController。navigate(result)订阅VM路由事件通知,处理路由跳转}}NavHost(navController,startDestinationsearchBar){composable(searchBar){MvvmSearchBarScreen(mvvmViewModel,)}composable(result){MvvmSearchResultScreen(mvvmViewModel,)}}}在rootlevel的MvvmApp中定义NavGraph,composable(destid){}中构造路由节点的各个子Screen,构造时传入ViewModel用于Screen之间的通信每个Composable都有一个CoroutineScope与其Lifecycle绑定,LaunchedEffect{}可以在这个Scope中启动协程处理副作用。代码中使用了一个只执行一次的Effect订阅ViewModel的路由事件通知当然我们可以将navConroller也传给MvvmSearchBarScreen,在其内部直接发起路由跳转。但在较复杂的项目中,跳转逻辑与页面定义应该尽量保持解耦,这更利于页面的复用和测试。我们也可以在Composeable中直接mutableStateOf()创建state来处理路由跳转,但是既然选择使用ViewModel了,那就应该尽可能将所有state集中到ViewModle管理。 注意:上面例子中的处理路由跳转的navigateToResults是一个事件而非状态,关于这部分区别,在后文在详细阐述定义子Screen 接下来看一下两个Screen的具体实现ComposablefunMvvmSearchBarScreen(mvvmViewModel:MvvmViewModel,){SearchBarScreen{mvvmViewModel。searchKeyword(it)}}ComposablefunMvvmSearchResultScreen(mvvmViewModel:MvvmViewModel){valresultbymvvmViewModel。result。collectAsState()valisLoadingbymvvmViewModel。isLoading。collectAsState()SearchResultScreen(result,isLoading,mvvmViewModel。key。value)} 大量逻辑都抽象到ViewModel中,所以Screen非常简洁SearchBarScreen接受用户输入,将搜索关键词发送给ViewModelMvvmSearchResultScreen作为结果页显示ViewModel发送的数据,包括Loading状态和搜索结果等。collectAsState用来将Flow转化为Compose的state,每当Flow接收到新数据时会触发Composable重组。Compose同时支持LiveData、RxJava等其他响应式库的collectAsState UI层的更多内容可以查阅SearchBarScreen和SearchResultScreen的源码。经过逻辑抽离后,这两个Composable只剩余布局相关的代码,可以在任何一种MVX中实现复用。ViewModel实现 最后看一下ViewModel的实现classMvvmViewModel(privatevalsearchService:DataRepository,){privatevalcoroutineScopeMainScope()privatevalisLoading:MutableStateFlowBooleanMutableStateFlow(false)valisLoadingisLoading。asStateFlow()privatevalresult:MutableStateFlowListMutableStateFlow(emptyList())valresultresult。asStateFlow()privatevalkeyMutableStateFlow()valkeykey。asStateFlow()使用Channel定义事件privatevalnavigateToResultsChannelBoolean(Channel。BUFFERED)valnavigateToResultsnavigateToResults。receiveAsFlow()funsearchKeyword(input:String){coroutineScope。launch{isLoading。valuetruenavigateToResults。send(true)key。valueinputvalresultwithContext(Dispatchers。IO){searchService。getArticlesList(input)}result。emit(result。data。datas)isLoading。valuefalse}}}接收到用户输入后,通过DataRepository发起搜索请求搜索过程中依次更新loading(loading显示状态)、navigateToResult(页面跳转事件)、key(搜索关键词)、result(搜索结果)等内容,不断驱动UI刷新 所有状态集中在ViewModel管理,甚至页面跳转、Toast弹出等事件也由ViewModel负责通知,这对单元测试非常友好,在单测中无需再mock各种UI相关的上下文。JetpackMVVM Jeptack的意义在于降低MVVM在Android平台的落地成本。 引入Jetpack后的代码变化不大,主要变动在于ViewModel的创建。 Jetpack提供了多个组件,降低了ViewModel的使用成本:通过hilt的DI降低ViewModel构造成本,无需手动传入DataRepository等依赖任意Composable都可以从最近的Scope中获取ViewModel,无需层层传参。HiltViewModelclassJetpackMvvmViewModelInjectconstructor(privatevalsearchService:DataRepositoryDataRepository依靠DI注入):ViewModel(){。。。}ComposablefunJetpackMvvmApp(){valnavControllerrememberNavController()NavHost(navController,startDestinationsearchBar,routeroot){composable(searchBar){JetpackMvvmSearchBarScreen(viewModel(navController,root)viewModel可以在需要时再获取,无需实现创建好并通过参数传进来)}composable(result){JetpackMvvmSearchResultScreen(viewModel(navController,root)可以获取跟同一个ViewModel实例)}}}ComposableinlinefunreifiedVM:ViewModelviewModel(navController:NavController,graphId:String):VM在NavGraph全局范围使用Hilt创建ViewModelhiltNavGraphViewModel(backStackEntrynavController。getBackStackEntry(graphId)) Jetpack甚至提供了hiltnavigationcompose库,可以在Composable中获取NavGraphScope或DestinationScope的ViewModel,并自动依赖Hilt构建。DestinationScope的ViewModel会跟随BackStack的弹出自动Clear,避免泄露。build。gradleimplementationandroidx。hilt:hiltnavigationcompose:latestversioin 未来Jetpack各组件之间协同效应会变得越来越强。参考https:developer。android。comjetpackcomposelibrarieshiltMVI MVI与MVVM很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,可以看做是MVVMRedux的结合。 MVI的I指Intent,这里不是启动Activity那个Intent,而是一种对用户操作的封装形式,为避免混淆,也可唤做Action等其他称呼。用户操作以Action的形式送给Model层进行处理。代码中,我们可以用Jetpack的ViewModel负责Intent的接受和处理,因为ViewModel可以在Composable中方便获取。 在SearchBarScreen用户输入关键词后通过Action通知ViewModel进行搜索ComposablefunMviSearchBarScreen(mviViewModel:MviViewModel,onConfirm:()Unit){SearchBarScreen{mviViewModel。onAction(MviViewModel。UiAction。SearchInput(it))}} 通过Action通信,有利于View与ViewModel之间的进一步解耦,同时所有调用以Action的形式汇总到一处,也有利于对行为的集中分析和监控ComposablefunMviSearchResultScreen(mviViewModel:MviViewModel){valviewStatebymviViewModel。viewState。collectAsState()SearchResultScreen(viewState。result,viewState。isLoading,viewState。key)} MVVM的ViewModle中分散定义了多个State,MVI使用ViewState对State集中管理,只需要订阅一个ViewState便可获取页面的所有状态,相对MVVM减少了不少模板代码。 相对于MVVM,ViewModel也有一些变化classMviViewModel(privatevalsearchService:DataRepository,){privatevalcoroutineScopeMainScope()privatevalviewState:MutableStateFlowViewStateMutableStateFlow(ViewState())valviewStateviewState。asStateFlow()privatevalnavigateToResultsChannelOneShotEvent(Channel。BUFFERED)valnavigateToResultsnavigateToResults。receiveAsFlow()funonAction(uiAction:UiAction){when(uiAction){isUiAction。SearchInput{coroutineScope。launch{viewState。valueviewState。value。copy(isLoadingtrue)valresultwithContext(Dispatchers。IO){searchService。getArticlesList(uiAction。input)}viewState。valueviewState。value。copy(resultresult。data。datas,keyuiAction。input)navigateToResults。send(OneShotEvent。NavigateToResults)viewState。valueviewState。value。copy(isLoadingfalse)}}}}dataclassViewState(valisLoading:Booleanfalse,valresult:ListemptyList(),valkey:String)sealedclassOneShotEvent{objectNavigateToResults:OneShotEvent()}sealedclassUiAction{classSearchInput(valinput:String):UiAction()}}页面所有的状态都定义在ViewState这个dataclass中,状态的修改只能在onAction中进行,其余场所都是immutable的,保证了数据流只能单向修改。反观MVVM,MutableStateFlow对外暴露时转成immutable才能保证这种安全性,需要增加不少模板代码且仍然容易遗漏。事件则统一定义在OneShotEvent中。Event不同于State,同一类型的事件允许响应多次,因此定义事件使用Channel而不是StateFlow。 Compose鼓励多使用State少使用Event,Event只适合用在弹Toast等少数场景中 通过浏览ViewModel的ViewState和Aciton定义就可以理清ViewModel的职责,可以直接拿来作为接口文档使用。页面路由 Sample中之所以使用事件而非状态来处理路由跳转,一个主要原因是由于使用了Navigation。Navigation有自己的backstack管理,当点击back键时会自动帮助我们返回前一页面。倘若我们使用状态来描述当前页面,当点击back时,没有机会更新状态,这将造成ViewState与UI的不一致。 关于路由方案的建议:简单项目使用事件控制页面跳转没有问题,但是对于复杂项目,推荐使用状态进行页面管理,有利于逻辑层时刻感知到当前的UI状态。 我们可以将NavController的backstack状态与ViewModel的状态建立同步:classMvvmViewModel(privatevalsearchService:DataRepository,){。。。使用StateFlow描述页面privatevaldestinationMutableStateFlow(DestSearchBar)valdestinationdestination。asStateFlow()funsearchKeyword(input:String){coroutineScope。launch{。。。destination。valueDestSearchResult。。。}}funbindNavStack(navController:NavController){navigation的状态时刻同步到viewModelnavController。addOnDestinationChangedListener{,,argumentsrun{destination。valuerequireNotNull(arguments?。getString(KEYROUTE))}}}} 如上,当navigation状态变化时,会及时同步到ViewModel,这样就可以使用StateFlow而非Channel来描述页面状态了。ComposablefunMvvmApp(mvvmViewModel:MvvmViewModel){valnavControllerrememberNavController()LaunchedEffect(Unit){with(mvvmViewModel){bindNavStack(navController)建立同步destination。collect{navController。navigate(it)}}}} 在入口处,为NavController和ViewModel建立同步绑定即可。CleanArchitecture 更大型的项目中,会引入CleanArchitecture,通过UseCase将ViewModel内的逻辑进一步分解。Compose只是个UI框架,对于ViewModle以下的逻辑层的治理方式与传统的Andorid开发没有区别。所以CleanArchitecture这样的复杂架构仍然可以在Compose项目中使用总结 比较了这么多种架构,那种与Compose最契合呢? Compose的声明式UI思想来自React,所以同样来自Redux思想的MVI应该是Compose的最佳伴侣。当然MVI只是在MVVM的基础上做了一些改良,如果你已经有了一个MVVM的项目,只是想将UI部分改造成Compose,那么没必要为了改造成MVI而进行重构,MVVM也可以很好地配合Compose使用的。但是如果你想将一个MVP项目改造成Compose可能成本就有点大了。 关于Jetpack,如果你的项目只用于Android,那么Jetpack无疑是一个好工具。但是Compose未来的应用场景将会很广泛,如果你有预期未来会配合KMP开发跨平台应用,那么就需要学会不依赖Jetpack的开发方式,这也是本文为什么要介绍非Jetpack下的MVVM的一个初衷。最后 在这里我分享一份由多位大佬亲自收录整理的Android学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。 当然,你也可以拿去查漏补缺,提升自身的竞争力。 真心希望可以帮助到大家,Android路漫漫,共勉! 如果你有需要的话,只需私信我【进阶】即可获取