安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

JetpackCompose架构如何选?MVPMVVM还是M

8月8日 先锋客投稿
  本次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路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取
投诉 评论 转载

12GB256GB71跑分仅2299元,米粉可能都心动了以往一说起性价比,我们很多人都会联系到小米,但并不代表其它厂商就做不出高性价比的产品,比如我们现在要说的Realme(真我)这个品牌,做起性价比产品来,同样一点也不含糊,推出的……苹果6s现在越来越烫怎么回事?要换了吗?感谢您的阅读!iPhone手机为什么会出现越使用越烫的情况?iPhone6S这款手机它采用的是a9处理器整款手机的性能表现还是相对比较突出的。虽然经历过这么多年的发……犹如战车前脸设计太诱人,博越家族出新车,这车的配置有点香!近些年国产车越来越受大众喜爱,除了不断进步的汽车智能科技的体验之外,研制方面也是下足了功夫,前几天我们聊过的吉利星越L就是这样,颜值很受欢迎。新车是这样,那对于已经在市场混迹多……JetpackCompose架构如何选?MVPMVVM还是M本次IO大会上曝出了Compose1。0即将发布的消息,虽然API层面已趋于稳定,但真正要在项目中落地还少不了一套合理的应用架构。传统Android开发中的MVP、MVVM等架……被封杀后,川粉要发动美国政变?社死恐将掐断特朗普政治生命虽然脸书(Facebook)等美国互联网巨头已经在6月彻底封禁了特朗普的账号,但这显然阻止不了特朗普在互联网上的热度。最近,阴谋论团体QAnon与特朗普粉丝们突破重重困难,自己……iPhone14Pro渲染图创新高,标准版挤牙膏你多久换一次一、iPhone14Pro渲染图曝光每次距离苹果发布新机还有好几个月,网上各种爆料层出不穷。知名机构Letsgodigital基于各种消息,制作了iPhone14Pro的……后ofo和摩拜时代,城市出行还有哪些机会?微交通不死,那机会又会在哪里?他们又能如何实现这一盈利率提升的大目标呢?作者Lexie编辑Lu首图来源:Fordauthority城市,是我们生活的地方……美媒探析钠电池能否替代锂电池美国《科学日报》网站2月10日发表题为《用于制造更环保、更安全电池的新型电解质》的文章,文章认为,钠电池或可替代锂电池。全文摘编如下:电池技术的未来在于钠。相比目前为大多……微信又更新了!这次是朋友圈来源:新华网微信又更新了!iOS微信发布了8。0。18版本更新其中一项新增功能格外引人关注:微信发朋友圈现在可以支持选择20张图片!在……中国物流无人机的广泛应用近几年来,随着电商经济的蓬勃发展,对传统物流的挑战日益加剧。一方面,销售订单不断增加,可用的劳动力却越来越少,供求矛盾异常突出;另一方面,消费者对配送安全、效率、质量等方面的要……小白总结前端HTML基础知识点(1)元素总结展现在用户面前的WEB页面,内容无非就是文字、图片、视频、音频这四大方面。而这些内容要呈现在用户眼前,最最基础的一种互联网语言,就是HTML(HyperTextMarkupLa……LG发布1618办公显示器网友再也不用拼接屏了你是否还在因屏幕不够大而选择双屏或是多屏拼接进行办公?日前,LG发布了28MQ780可以解决这个问题。该显示器采用全球首创的16:18比例,相当于两块21寸16:9屏幕拼接而成……
蒂姆库克时代的苹果10年从最好到更好的进化安徽150余项科技成果科技大市场成功交易快手616实在购物节招商启动,激励升级助力商家爆发全场不足百元!便宜实用小家电推荐合集聚焦每一个小弄堂每一条小马路每一个小落脚,城市就是人工智能最小米手机如何关闭系统广告币圈那些事涨价潮下,混动的春天航天精神国潮耳机余音,仅售199的它你会喜欢么?7月新能源车市比亚迪家族持续爆发,特斯拉销量有蹊跷怎样把电脑C盘设置成禁止安装任何软件?求安利!华为Matepadpro有什么好用的记笔记和学习的软饮水机漏水怎么办?整容的危害有哪些(整容带来的危害)我有一个傻儿子兰亭注意!京城这些景点已经关闭,别白跑一趟关于护士节的征文奉献愿你永远是少年读后感100字牵手王俊凯后,小仙炖如何拯救屡屡违规的现实与消费者口碑?孩子没教养的表现,家长要及时纠正,帮助他们健康成长干性敏感色素和衰老性皮肤护理知识(全)实验室管理规定我这个人

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利