作者:vivo 互联网客户端团队-Xu Jie
Android架构模式飞速演进,目前已经有MVC、MVP、MVVM、MVI。到底哪一个才是自己业务场景最需要的,不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍Compose为什么要结合MVI进行使用。希望知其然,然后找到适合自己业务的架构模式
一、前言
不得不感叹,近些年android的架构演进速度真的是飞快,拿笔者工作这几年接触的架构来说,就已经有了MVC、MVP、MVVM。正当笔者准备把MVVM应用到自己项目当中时,发现谷歌悄悄的更新了开发者文档(应用架构指南 | Android 开发者 | Android Developers (google.cn))。这是一篇指导如何使用MVI的文章。那么这个文章到底为什么更新,想要表达什么?里面提到的Compose又是什么?难道现在已经有的MVC、MVP、MVVM不够用吗?MVI跟已有的这些架构又有什么不同之处呢?
有人会说,不管什么架构,都是围绕着“ 解耦”来实现的,这种说法是正确的,但是耦合度高只是现象,采用什么手段降低耦合度?降低耦合度之后的程序方便单元测试吗?如果我在MVC、MVP、MVVM的基础上做解耦,可以做的很彻底吗?
先告诉你答案, MVC、MVP、MVVM无法做到彻底的解耦,但是MVI+Compose可以做到彻底的解耦,也就是本文的重点讲解部分。本文结合具体的代码和案例,复杂问题简单化,并且结合较多技术博客做了统一的总结,相信你读完会收获颇丰。
那么本篇文章编写的意义,就是为了能够深入浅出的讲解MVI+Compose,大家可以先试想下这样的业务场景,如果是你,你会选择哪种架构实现?
业务场景考虑
上面三个步骤是顺序执行的,手机号的登录、账号的验证、点赞都是与服务端进行交互之后,获取对应的返回结果,然后再做下一步。
在开始介绍MVI+Compose之前,需要循序渐进,了解每个架构模式的缺点,才知道为什么Google提出MVI+Compose。
正式开始前,按照架构模式的提出时间来看下是如何演变的,每个模式的提出往往不是基于android提出,而是基于服务端或者前端演进而来,这也说明设计思路上都是大同小异的:
二、架构模式过去式?
2.1 MVC已经存在很久了
MVC模式提出时间太久了,早在1978年就被提出,所以一定不是用于android,android的MVC架构主要还是源于服务端的SpringMVC,在2007年到2017年之间,MVC占据着主导地位,目前我们android中看到的MVC架构模式是这样的。
MVC架构这几个部分的含义如下,网上随便找找就有一堆说明。
MVC架构分为以下几个部分
(1)MVC代码示例
我们举个登录验证的例子来看下MVC架构一般怎么实现。
这个是controller
MVC架构实现登录流程-controller
publicclassMvcLoginActivityextendsAppCompatActivity{ privateEditText userNameEt; privateEditText passwordEt; privateUser user; @Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvc_login); user = newUser; userNameEt = findViewById(R.id.user_name_et); passwordEt = findViewById(R.id.password_et); Button loginBtn = findViewById(R.id.login_btn); loginBtn.setOnClickListener( newView.OnClickListener { @Override publicvoidonClick(View view){ LoginUtil.getInstance.doLogin(userNameEt.getText.toString, passwordEt.getText.toString, newLoginCallBack { @Override publicvoidloginResult(@NonNull com.example.mvcmvpmvvm.mvc.Model.User success){ if( null!= user) { // 这里免不了的,会有业务处理 //1、保存用户账号 //2、loading消失 //3、大量的变量判断 //4、再做进一步的其他网络请求 Toast.makeText(MvcLoginActivity. this, " Login Successful", Toast.LENGTH_SHORT) .show; } else{ Toast.makeText(MvcLoginActivity. this, "Login FAIled", Toast.LENGTH_SHORT) .show; } } }); } }); } }这个是model
MVC架构实现登录流程-model
publicclassLoginService{ publicstaticLoginUtil getInstance( ) { returnnewLoginUtil; } publicvoiddoLogin( String userName, String password, LoginCallBack loginCallBack) { User user = newUser; if(userName. equals( "123456") && password. equals( "123456")) { user.setUserName(userName); user.setPassword(password); loginCallBack.loginResult(user); } else{ loginCallBack.loginResult( null); } } }例子很简单,主要做了下面这些事情
(2)MVC优缺点
MVC在大部分简单业务场景下是够用的,主要优点如下:
但是随着时间的推移,你的MVC架构可能慢慢的演化成了下面的模式。拿上面的例子来说,你只做登录比较简单,但是当你的页面把登录账号校验、点赞都实现的时候,方法会比较多,共享一个view的时候,或者共同操作一个数据源的时候,就会出现变量满天飞,view四处被调用,相信大家也深有体会。
不可避免的,MVC就存在了下面的问题
归根究底,在android里面使用MVC的时候,对于Model、View、Controller的划分范围,总是那么的不明确,因为本身他们之间就有无法直接分割的依赖关系。所以总是避免不了这样的问题:
花了一定篇幅介绍MVC,是让大家对MVC中Model、View、Controller应该各自完成什么事情能深入理解,这样才有后面架构不断演进的意义。
2.2 MVP架构的由来
(1)MVP要解决什么问题?
2016年10月, Google官方提供了MVP架构的Sample代码来展示这种模式的用法,成为最流行的架构。
相对于MVC,MVP将Activity复杂的逻辑处理移至另外的一个类(Presenter)中,此时Activity就是MVP模式中的View,它负责UI元素的初始化,建立UI元素与Presenter的关联(Listener之类),同时自己也会处理一些简单的逻辑(复杂的逻辑交由 Presenter处理)。
那么MVP 同样将代码划分为三个部分:
结构说明
来看看MVP的架构图:
与MVC的最主要区别
View与Model并不直接交互,而是通过与Presenter交互来与Model间接交互。而在MVC中View可以与Model直接交互。
通常View与Presenter是一对一的,但复杂的View可能绑定多个Presenter来处理逻辑。而Controller回归本源,首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,它是基于行为的,并且可以被多个View共享,Controller可以负责决定显示哪个View。
Presenter与View的交互是通过接口来进行的,更有利于添加单元测试。
(2)MVP代码示意
① 先来看包结构图
② 建立Bean
MVP架构实现登录流程-model
publicclassUser { privateStringuserName; privateStringpassword; publicStringgetUserName { return... } publicvoidsetUserName( StringuserName) { ...; } }③ 建立Model接口 (处理业务逻辑,这里指数据读写),先写接口方法,后写实现
MVP架构实现登录流程-model
publicinterfaceIUserBiz { booleanlogin( StringuserName, Stringpassword); }④ 建立presenter(主导器,通过iView和iModel接口操作model和view),activity可以把所有逻辑给presenter处理,这样java逻辑就从activity中分离出来。
MVP架构实现登录流程-model
publicclassLoginPresenter{ privateUserBiz userBiz; privateIMvpLoginView iMvpLoginView; publicLoginPresenter(IMvpLoginView iMvpLoginView){ this.iMvpLoginView = iMvpLoginView; this.userBiz = newUserBiz; } publicvoidlogin{ String userName = iMvpLoginView.getUserName; String password = iMvpLoginView.getPassword; booleanisLoginSuccessful = userBiz.login(userName, password); iMvpLoginView.onLoginResult(isLoginSuccessful); } }⑤ View视图建立view,用于更新ui中的view状态,这里列出需要操作当前view的方法,也是接口IMvpLoginView
MVP架构实现登录流程-model
publicinterfaceIMvpLoginView{ String getUserName( ) ; String getPassword( ) ; voidonLoginResult( Boolean isLoginSuccess) ; }⑥ activity中实现IMvpLoginView接口,在其中操作view,实例化一个presenter变量。
MVP架构实现登录流程-model
publicclassMvpLoginActivityextendsAppCompatActivityimplementsIMvpLoginView{ privateEditText userNameEt; privateEditText passwordEt; privateLoginPresenter loginPresenter; @Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvp_login); userNameEt = findViewById(R.id.user_name_et); passwordEt = findViewById(R.id.password_et); Button loginBtn = findViewById(R.id.login_btn); loginPresenter = newLoginPresenter( this); loginBtn.setOnClickListener( newView.OnClickListener { @Override publicvoidonClick(View view){ loginPresenter.login; } }); } @Override publicString getUserName{ returnuserNameEt.getText.toString; } @Override publicString getPassword{ returnpasswordEt.getText.toString; } @Override publicvoidonLoginResult(Boolean isLoginSuccess){ if(isLoginSuccess) { Toast.makeText(MvpLoginActivity. this, getUserName + " Login Successful", Toast.LENGTH_SHORT) .show; } else{ Toast.makeText(MvpLoginActivity. this, "Login Failed", Toast.LENGTH_SHORT).show; } } }(3)MVP优缺点
因此,Activity及从MVC中的Controller中解放出来了,这会Activity主要做显示View的作用和用户交互。每个Activity可以根据自己显示View的不同实现View视图接口IUserView。
通过对比同一实例的MVC与MVP的代码,可以证实MVP模式的一些优点:
但还是存在一些缺点:
三、MVVM其实够用了
3.1MVVM思想存在很久了
MVVM最初是在2005年由微软提出的一个UI架构概念。后来在2015年的时候,开始应用于android中。
MVVM 模式改动在于中间的 Presenter 改为 ViewModel,MVVM 同样将代码划分为三个部分:
与MVP唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。
MVVM架构图如下所示:
可以看出MVVM与MVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP。
MVVM的双向数据绑定主要通过DataBinding实现,但是大部分人应该跟我一样,不使用DataBinding,那么大家最终使用的MVVM架构就变成了下面这样:
总结一下:
实际使用MVVM架构说明
3.2 MVVM代码示例
(1)建立viewModel,并且提供一个可供view调取的方法 login(String userName, String
password)
MVVM架构实现登录流程-model
publicclassLoginViewModelextendsViewModel{ privateUser user; privateMutableLiveData<Boolean> isLoginSuccessfulLD; publicLoginViewModel{ this.isLoginSuccessfulLD = newMutableLiveData<>; user = newUser; } publicMutableLiveData<Boolean> getIsLoginSuccessfulLD{ returnisLoginSuccessfulLD; } publicvoidsetIsLoginSuccessfulLD( booleanisLoginSuccessful) { isLoginSuccessfulLD.postValue(isLoginSuccessful); } publicvoidlogin(String userName, String password){ if(userName.equals( "123456") && password.equals( "123456")) { user.setUserName(userName); user.setPassword(password); setIsLoginSuccessfulLD( true); } else{ setIsLoginSuccessfulLD( false); } } publicString getUserName{ returnuser.getUserName; } }(2)在activity中声明viewModel,并建立观察。点击按钮,触发 login(String userName, String password)。持续作用的观察者loginObserver。只要LoginViewModel 中的isLoginSuccessfulLD变化,就会对应的有响应
MVVM架构实现登录流程-model
publicclassMvvmLoginActivityextendsAppCompatActivity{ privateLoginViewModel loginVM; privateEditText userNameEt; privateEditText passwordEt; @Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvvm_login); userNameEt = findViewById(R.id.user_name_et); passwordEt = findViewById(R.id.password_et); Button loginBtn = findViewById(R.id.login_btn); loginBtn.setOnClickListener( newView.OnClickListener { @Override publicvoidonClick(View view){ loginVM.login(userNameEt.getText.toString, passwordEt.getText.toString); } }); loginVM = newViewModelProvider( this).get(LoginViewModel.class); loginVM.getIsLoginSuccessfulLD.observe( this, loginObserver); } privateObserver<Boolean> loginObserver = newObserver<Boolean> { @Override publicvoidonChanged(@Nullable Boolean isLoginSuccessFul){ if(isLoginSuccessFul) { Toast.makeText(MvvmLoginActivity. this, "登录成功", Toast.LENGTH_SHORT) .show; } else{ Toast.makeText(MvvmLoginActivity. this, "登录失败", Toast.LENGTH_SHORT) .show; } } }; }3.3 MVVM优缺点
通过上面的代码,可以总结出MVVM的优点:
在实现细节上,View 和 Presenter 从双向依赖变成 View 可以向 ViewModel 发指令,但 ViewModel 不会直接向 View 回调,而是让 View 通过观察者的模式去监听数据的变化,有效规避了 MVP 双向依赖的缺点。
但 MVVM 在某些情况下,也存在一些缺点:
(1)关联性比较强的流程,liveData太多,并且理解成本较高
当业务比较复杂的时候,在viewModel中必然存在着比较多的LiveData去管理。当然,如果你去管理好这些LiveData,让他们去处理业务流程,问题也不大,只不过理解的成本会高些。
(2)不便于单元测试
viewModel里面一般都是对数据库和网络数据进行处理,包含了业务逻辑在里面,当要去对某一流程进行测试时,并没有办法完全剥离数据逻辑的处理流程,单元测试也就增加了难度。
那么我们来看看缺点对应的具体场景是什么,便于我们后续进一步探讨MVI架构。
(1)在上面登录之后,需要验证账号信息,然后再自动进行点赞。那么,viewModel里面对应的增加几个方法,每个方法对应一个LiveData
MVVM架构实现登录流程-model
publicclassLoginMultiViewModel extendsViewModel { privateUser user; // 是否登录成功 privateMutableLiveData< Boolean> isLoginSuccessfulLD; // 是否为指定账号 privateMutableLiveData< Boolean> isMyAccountLD; // 如果是指定账号,进行点赞 privateMutableLiveData< Boolean> goThumbUp; publicLoginMultiViewModel { this.isLoginSuccessfulLD = newMutableLiveData<>; this.isMyAccountLD = newMutableLiveData<>; this.goThumbUp = newMutableLiveData<>; user = newUser; } publicMutableLiveData< Boolean> getIsLoginSuccessfulLD { returnisLoginSuccessfulLD; } publicMutableLiveData< Boolean> getIsMyAccountLD { returnisMyAccountLD; } publicMutableLiveData< Boolean> getGoThumbUpLD { returngoThumbUp; } ... publicvoidlogin( StringuserName, Stringpassword) { if(userName.equals( "123456") && password.equals( "123456")) { user.setUserName(userName); user.setPassword(password); setIsLoginSuccessfulLD( true); } else{ setIsLoginSuccessfulLD( false); } } publicvoidisMyAccount( @NonNullStringuserName) { try{ Thread.sleep( 1000); } catch(Exception ex) { } if(userName.equals( "123456")) { setIsMyAccountSuccessfulLD( true); } else{ setIsMyAccountSuccessfulLD( false); } } publicvoidgoThumbUp( booleanisMyAccount) { setGoThumbUpLD(isMyAccount); } publicStringgetUserName { returnuser.getUserName; } }(2)再来看看你可能使用的一种处理逻辑,在判断登录成功之后,使用变量isLoginSuccessFul再去做 loginVM.isMyAccount(userNameEt.getText.toString);在账号验证成功之后,再去通过变量isMyAccount去做loginVM.goThumbUp(true);
MVVM架构实现登录流程-model
publicclassMvvmFaultLoginActivityextendsAppCompatActivity{ privateLoginMultiViewModel loginVM; privateEditText userNameEt; privateEditText passwordEt; @Override protectedvoid onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mvvm_fault_login); userNameEt = findViewById(R.id.user_name_et); passwordEt = findViewById(R.id.password_et); Button loginBtn = findViewById(R.id.login_btn); loginBtn.setOnClickListener(new View.OnClickListener { @Override publicvoid onClick(View view) { loginVM.login(userNameEt.getText.toString, passwordEt.getText.toString); } }); loginVM = new ViewModelProvider( this). get(LoginMultiViewModel. class); loginVM.getIsLoginSuccessfulLD.observe( this, loginObserver); loginVM.getIsMyAccountLD.observe( this, isMyAccountObserver); loginVM.getGoThumbUpLD.observe( this, goThumbUpObserver); } privateObserver< Boolean> loginObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisLoginSuccessFul) { if(isLoginSuccessFul) { Toast.makeText(MvvmFaultLoginActivity. this, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show; loginVM.isMyAccount(userNameEt.getText.toString); } else{ Toast.makeText(MvvmFaultLoginActivity. this, "登录失败", Toast.LENGTH_SHORT) .show; } } }; privateObserver< Boolean> isMyAccountObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisMyAccount) { if(isMyAccount) { Toast.makeText(MvvmFaultLoginActivity. this, "校验成功,开始点赞", Toast.LENGTH_SHORT).show; loginVM.goThumbUp( true); } } }; privateObserver< Boolean> goThumbUpObserver = new Observer< Boolean> { @Override publicvoid onChanged( @NullableBooleanisThumbUpSuccess) { if(isThumbUpSuccess) { Toast.makeText(MvvmFaultLoginActivity. this, "点赞成功", Toast.LENGTH_SHORT) .show; } else{ Toast.makeText(MvvmFaultLoginActivity. this, "点赞失败", Toast.LENGTH_SHORT) .show; } } }; }毫无疑问,这种交互在实际开发中是可能存在的,页面比较复杂的时候,这种变量也就滋生了。这种场景,就有必要聊聊MVI架构了。
四、MVI有存在的必要性吗?
4.1 MVI的由来
MVI 模式来源于2014年的 Cycle.js(一个 Java框架),并且在主流的 JS 框架 Redux 中大行其道,然后就被一些大佬移植到了 Android 上(比如最早期用Java写的 mosby)。
既然MVVM是目前android官方推荐的架构,又为什么要有MVI呢?其实应用架构指南中并没有提出MVI的概念,而是提到了单向数据流,唯一数据源,这也是区别MVVM的特性。
不过还是要说明一点,凡是MVI做到的,只要你使用MVVM去实现,基本上也能做得到。只是说在接下来要讲的内容里面,MVI具备的封装思路,是可以直接使用的,并且是便于单元测试的。
MVI的思想:靠数据驱动页面 (其实当你把这种思想应用在各个框架的时候,你的那个框架都会更加优雅)
MVI架构包括以下几个部分
看下交互流程图:
对流程图做下解释说明:
(1)用户操作以Intent的形式通知Model
(2)Model基于Intent更新State。这个里面包括使用ViewModel进行网络请求,更新State的操作
(3)View接收到State变化刷新UI。
4.2 MVI的代码示例
直接看代码吧
(1)先看下包结构
(2)用户点击按钮,发起登录流程
loginViewModel.loginActionIntent.send(LoginActionIntent.DoLogin(userNameEt.text.toString, passwordEt.text.toString))。
此处是发送了一个Intent出去
MVI架构代码-View
loginBtn.setOnClickListener { lifecycleScope.launch { loginViewModel.loginActionIntent.send( LoginActionIntent. DoLogin(userNameEt.text. toString, passwordEt.text. toString)) } }(3)ViewModel对Intent进行监听
initActionIntent。在这里可以把按钮点击事件的Intent消费掉
MVI架构代码-Model
classLoginViewModel: ViewModel{ companionobject{ constvalTAG = "LoginViewModel" } privateval_repository = LoginRepository valloginActionIntent = Channel<LoginActionIntent>(Channel.UNLIMITED) privateval_loginActionState = MutableSharedFlow<LoginActionState> valstate: SharedFlow<LoginActionState> get= _loginActionState init{ // 可以用来初始化一些页面或者参数 initActionIntent } privatefuninitActionIntent{ viewModelScope.launch { loginActionIntent.consumeAsFlow.collect { when(it) { isLoginActionIntent.DoLogin -> { doLogin(it.username, it.password) } else-> { } } } } } }(4)使用respository进行网络请求,更新state
MVI架构代码-Repository
classLoginRepository{ suspendfunrequestLoginData(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456"&& password == "123456") { returntrue } returnfalse } suspendfunrequestIsMyAccount(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456") { returntrue } returnfalse } suspendfunrequestThumbUp(username: String, password: String) : Boolean{ delay( 1000) if(username == "123456") { returntrue } returnfalse } }MVI架构代码-更新state
privatefundoLogin(username: String, password: String) { viewModelScope.launch { if(username.isEmpty || password.isEmpty) { return@launch } // 设置页面正在加载 _loginActionState.emit(LoginActionState.LoginLoading(username, password)) // 开始请求数据 valloginResult = _repository.requestLoginData(username, password) if(!loginResult) { //登录失败 _loginActionState.emit(LoginActionState.LoginFailed(username, password)) return@launch } _loginActionState.emit(LoginActionState.LoginSuccessful(username, password)) //登录成功继续往下 valisMyAccount = _repository.requestIsMyAccount(username, password) if(!isMyAccount) { //校验账号失败 _loginActionState.emit(LoginActionState.IsMyAccountFailed(username, password)) return@launch } _loginActionState.emit(LoginActionState.IsMyAccountSuccessful(username, password)) //校验账号成功继续往下 valisThumbUpSuccess = _repository.requestThumbUp(username, password) if(!isThumbUpSuccess) { //点赞失败 _loginActionState.emit(LoginActionState.GoThumbUpFailed(username, password)) return@launch } //点赞成功继续往下 _loginActionState.emit(LoginActionState.GoThumbUpSuccessful( true)) } }(5)在View中监听state的变化,做页面刷新
MVI架构代码-Repository
funobserveViewModel{ lifecycleScope.launch { loginViewModel.state.collect { when(it) { isLoginActionState.LoginLoading -> { Toast.makeText(baseContext, "登录中", Toast.LENGTH_SHORT).show } isLoginActionState.LoginFailed -> { Toast.makeText(baseContext, "登录失败", Toast.LENGTH_SHORT).show } isLoginActionState.LoginSuccessful -> { Toast.makeText(baseContext, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show } isLoginActionState.IsMyAccountSuccessful -> { Toast.makeText(baseContext, "校验成功,开始点赞", Toast.LENGTH_SHORT).show } isLoginActionState.GoThumbUpSuccessful -> { resultView.text = "点赞成功" Toast.makeText(baseContext, "点赞成功", Toast.LENGTH_SHORT).show } else-> {} } } } }通过这个流程,可以看到用户点击登录操作,一直到最后刷新页面,是一个串行的操作。在这种场景下,使用MVI架构,再合适不过
4.2 MVI的优缺点
(1)MVI的优点如下:
但MVI 本身也存在一些缺点:
更关键的一点,即使单向数据流封装的很多,仍然避免不了来一个新人,不遵守这个单向数据流的写法,随便去处理view。这时候就要去引用Compose了。
五、不妨利用Compose升级MVI
这一章节是本文的重点。
2021年,谷歌发布Jetpack Compose1.0,2022年,又更新了文章应用架构指南,在进行界面层的搭建时,建议方案如下:
为什么这里会提到Compose?
接下来就是本文与其他技术博客不一样的地方,把Compose如何使用,为什么这样使用做下说明,不要只看理论,最好实战。
5.1 Compose的主要作用
Compose可以做到界面view在一开始的时候就要绑定数据源,从而达到无法在其他地方被篡改的目的。
怎么理解?
当你有个TextView被声明之后,按照之前的架构,可以获取这个TextView,并且给它的text随意赋值,这就导致了TextView就有可能不止是在MVI架构里面使用,也可能在MVC架构里面使用。
5.2 MVI+Compose的代码示例
MVI+Compose架构代码
classMviComposeLoginActivity: ComponentActivity{ overridefunonCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { setContent { BoxWithConstraints( modifier = Modifier .background(colorResource(id = R.color.white)) .fillMaxSize ) { loginConstraintToDo } } } } @Composable funEditorTextField(textFieldState: TextFieldState, label : String, modifier: Modifier= Modifier) { // 定义一个可观测的text,用来在TextField中展示 TextField( value = textFieldState.text, // 显示文本 onValueChange = { textFieldState.text = it }, // 文字改变时,就赋值给text modifier = modifier, label = { Text(text = label) }, // label是Input placeholder = @Composable{ Text(text = "123456") }, // 不输入内容时的占位符 ) } @SuppressLint( "CoroutineCreationDuringComposition") @Composable internalfunloginConstraintToDo(model: ComposeLoginViewModel= viewModel ){ valstate bymodel.uiState.collectAsState valcontext = LocalContext.current loginConstraintLayout( onLoginBtnClick = { text1, text2 -> lifecycleScope.launch { model.sendEvent(TodoEvent.DoLogin(text1, text2)) } }, state.isThumbUpSuccessful ) when{ state.isLoginSuccessful -> { Toast.makeText(baseContext, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show model.sendEvent(TodoEvent.VerifyAccount( "123456", "123456")) } state.isAccountSuccessful -> { Toast.makeText(baseContext, "账号校验成功,开始点赞", Toast.LENGTH_SHORT).show model.sendEvent(TodoEvent.ThumbUp( "123456", "123456")) } state.isThumbUpSuccessful -> { Toast.makeText(baseContext, "点赞成功", Toast.LENGTH_SHORT).show } } } @Composable funloginConstraintLayout(onLoginBtnClick: ( String, String) -> Unit, thumbUpSuccessful: Boolean){ ConstraintLayout { //通过createRefs创建三个引用 // 初始化声明两个元素,如果只声明一个,则可用 createRef 方法 // 这里声明的类似于 View 的 id val(firstText, secondText, button, text) = createRefs valfirstEditor = remember { TextFieldState } valsecondEditor = remember { TextFieldState } EditorTextField(firstEditor, "123456", Modifier.constrainAs(firstText) { top.linkTo(parent.top, margin = 16.dp) start.linkTo(parent.start) centerHorizontallyTo(parent) // 摆放在 ConstraintLayout 水平中间 }) EditorTextField(secondEditor, "123456", Modifier.constrainAs(secondText) { top.linkTo(firstText.bottom, margin = 16.dp) start.linkTo(firstText.start) centerHorizontallyTo(parent) // 摆放在 ConstraintLayout 水平中间 }) Button( onClick = { onLoginBtnClick( "123456", "123456") }, // constrainAs 将 Composable 组件与初始化的引用关联起来 // 关联之后就可以在其他组件中使用并添加约束条件了 modifier = Modifier.constrainAs(button) { // 熟悉 ConstraintLayout 约束写法的一眼就懂 // parent 引用可以直接用,跟 View 体系一样 top.linkTo(secondText.bottom, margin = 20.dp) start.linkTo(secondText.start, margin = 10.dp) } ){ Text( "Login") } Text( if(thumbUpSuccessful) "点赞成功"else"点赞失败", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 36.dp) start.linkTo(button.start) centerHorizontallyTo(parent) // 摆放在 ConstraintLayout 水平中间 }) } }关键代码段就在于下面:
MVI+Compose架构代码
Text( if(thumbUpSuccessful) "点赞成功"else"点赞失败", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 36.dp) start.linkTo(button.start) centerHorizontallyTo( parent) // 摆放在 ConstraintLayout 水平中间 })TextView的text在页面初始化的时候就跟数据源中的thumbUpSuccessful变量进行了绑定,并且这个TextView不可以在其他地方二次赋值,只能通过这个变量thumbUpSuccessful进行修改数值。当然,使用这个方法,也解决了数据更新是无法diff更新的问题,堪称完美了。
5.3 MVI+Compose的优缺点
MVI+Compose的优点如下:
MVI+Compose的也存在一些缺点:
不能称为缺点的缺点吧。
由于Compose实现界面,是纯靠kotlin代码实现,没有借助xml布局,这样的话,一开始学习的时候,学习成本要高些。并且性能还未知,最好不要用在一级页面。
六、如何选择框架模式
6.1 架构选择的原理
通过上面这么多架构的对比,可以总结出下面的结论。
耦合度高是现象,关注点分离是手段,易维护性和易测试性是结果,模式是可复用的经验。
再来总结一下上面几个框架适用的场景:
6.2 框架的选择原理
切勿混合使用架构模式,分析透彻页面结构之后,选择一种架构即可,不然会导致页面越来越复杂,无法维护
上面就是对所有框架模式的总结,大家根据实际情况进行选择。建议还是直接上手最新的MVI+Compose,虽然多了些学习成本,但是毕竟Compose的思想还是很值得借鉴的。
END