文章目录
1.前言
在上一篇文章中已经介绍了常规的没有结合Compose UI来使用的MVI模式了,本篇文章就是把之前的内容结合起来,在之前的基础上修改为完整的Compose UI + MVI的案例,如果对于文章中有不理解的可以回过头去看之前的内容.
2.ViewModel的完整代码
class LoginViewModel :ViewModel(){val loginChannel = Channel<LoginIntent>(Channel.UNLIMITED)privateval loginState =MutableStateFlow(LoginState())val uiLoginState: StateFlow<LoginState>= loginState
privateval toNewPage = MutableSharedFlow<Boolean>()val uiToNewPage: SharedFlow<Boolean>= toNewPage
init{
viewModelScope.launch{
loginChannel.consumeAsFlow().collect{when(it){is LoginIntent.RunLoginIntent ->{login()}}}}}privatefunlogin(){
loginState.value = loginState.value.copy(isLogin =true)
viewModelScope.launch{val name = loginState.value.nameCache.value
val password = loginState.value.passwordCache.value
val enPassword =getEncryptPassword(password)val loginToken = APIManager.requestLoginToken(name,getEncryptPassword(password),getVerify(name, enPassword))//以上delay是模拟了一个请求耗时
loginState.value = loginState.value.copy(isLogin =false)//如果token不是空的话,就代表登录成功了,emi登录成功的消息出去,让UI执行操作if(loginToken.isNotEmpty()){
toNewPage.emit(true)}}}privatefungetEncryptPassword(password: String): String {//这里随便做一下密码加密,实际应该是做MD5处理或者其他算法处理,密码不以明文形式提交return password.plus("abc")}privatefungetVerify(userName: String, password: String): String {//这里生成校验秘钥,用于接口请求校验,也可以在请求头里面做,一般是用于进一步防止别人模拟请求return"This is where the data is encrypted"}openclass LoginIntent {object RunLoginIntent :LoginIntent()}}
ViewModel
中定义了
Channel
和对应的登录状态数据还有需要
View
层执行动作的
Flow
,一样对外暴露的还是抽象接口,然后在
init
函数中订阅
channel
,当接收到
RunLoginIntent
意图的时候就执行
login
函数.在函数中分别执行了以下步骤
- 设置是否正在登录中为
true
,可以看到这里我们通过copy
函数,很方便的就修改了其中某一个值,在页面中接收到这个值的改变后,就会显示登录动画.- 对密码明文进行加密,这里只是简单演示一下,实际会复杂一些.
- 生成校验内容,这个一般是用来服务器防止模拟请求,客户端配合处理就行了
- 调用
APIManager
请求登录数据,这里模拟的是返回一个token
,后续使用这个token
去做其他事情,实际可能是返回一个用户完整数据+令牌或者是其他之类的.- 登录接口返回之后,就把登录中的状态取消掉,因为耗时操作已经完成了.
- 最后判定如果是登录成功,就通过
toNewPage
字段emit
跳转页面的信息出去,实际的业务这里应该还会有登录失败了的话,需要提示用户登录失败之类的提示.
3.View的完整代码
LoginActivity
class LoginActivity :ComponentActivity(){companionobject{constval TAG ="LoginActivity"}overridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)
setContent {LoginView().initView()CollectState()}}@ComposableprivatefunCollectState(){
hiltViewModel<LoginViewModel>().apply{
uiToNewPage.collectWithEffect{if(it){//执行登录跳转页面iLog("$TAG 跳转到主页页面")}}
uiLoginState.collectWithEffect{ value ->if(value.isLogin){//显示登录加载框iLog("$TAG 显示了登录进度条")return@collectWithEffect}iLog("$TAG 隐藏登录进度条")}}}}constval UNIT_EXT =0xff0011
需要注意一下,这里的
hiltViewModel
需要引入一个
compose
的库
androidx.hilt:hilt-navigation-compose:1.0.0
onCreate
中还是调用的setContent
函数去设置LoginView
完了去订阅相关的业务数据- 订阅数据中,通过接收到状态,去决定跳转以及显示网络请求动画(这里只是标识了一下,实际是操作对应的弹窗以及调用页面跳转代码)
- 一般情况下是一个组数据就可以了,但是这里因为有跳转页面这种一次性的逻辑操作,所以加了
uiToNewPage
的变量- 可以看到所有的数据都是通过
uiLoginState
来管理的,不管是记录的数据状态,还是通知到UI
的状态,维护数据的话也是通过这一个State
就行了
LoginView
classLoginView(){@OptIn(ExperimentalUnitApi::class)@Preview@ComposablefuninitView(){Column(
modifier = Modifier
.background(color =colorResource(id = R.color.bg_white)).fillMaxWidth().fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){LogIcon()VSpacer(30)Text(text ="Welcome to example", fontWeight = FontWeight.Bold, fontSize =TextUnit(23f, TextUnitType.Sp))VSpacer(30)InputEdit(hint ="please input your userName")VSpacer(5)InputEdit(hint ="please input your password",true)VSpacer(30)val loginViewModel: LoginViewModel =hiltViewModel()Button(modifier = Modifier.semantics{ testTag ="test"}, onClick ={
loginViewModel.loginChannel.trySend(LoginViewModel.LoginIntent.RunLoginIntent).getOrThrow()}){Text(text ="Login")}}}@ComposablefunLogIcon(){Image(
painter =painterResource(id = R.drawable.ic_launcher_background),
contentDescription ="图标",
modifier = Modifier
.width(50.dp).height(50.dp).clip(CircleShape).border(2.dp,colorResource(id = android.R.color.black), CircleShape))}@ComposablefunVSpacer(height: Int){Spacer(
modifier = Modifier
.fillMaxWidth().height(height.dp))}@OptIn(ExperimentalMaterial3Api::class)@ComposablefunInputEdit(hint: String, isPassword: Boolean =false){val loginViewModel: LoginViewModel =hiltViewModel()val name = remember { loginViewModel.uiLoginState.value.nameCache }val password = remember { loginViewModel.uiLoginState.value.passwordCache }val state =if(isPassword) password else name
val transformation =if(isPassword)PasswordVisualTransformation()else VisualTransformation.None
val hintStr =if(isPassword)"Password"else"UserName"TextField(visualTransformation = transformation, value = state.value, onValueChange ={
state.value = it
}, modifier = Modifier
.width(260.dp).height(56.dp), label ={Text(text = hintStr)}, keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done, keyboardType = KeyboardType.Password
), placeholder ={Text(text = hintStr, Modifier.alpha(0.5f))})}}
前面的还是和之前文章一样,只是在数据设置这一块做出了修改,
state
变量直接放在了数据集中,在实际使用的时候通过
remeber
直接引用,可以看到和
Compose UI
结合之后,有许多
Compose
现成的函数调用,可以直接和
View
结合起来,这里就不用再费劲的订阅了,直接通过函数引用就可以了.
4.扩展函数相关
@Composablefun<V> Flow<V>.collectWithEffect(collector1: FlowCollector<V>): Int {LaunchedEffect(key1 = UUID.randomUUID()){collect(collector1)}return UNIT_EXT
}
这里订阅的时候通过
LaunchedEffect
来订阅数据变化,进行了一下基础的封装.
5.总结
到此
Compose UI + MVI
整体结构内容基本完成,可以看到
MVI
模式在
android
中的使用
google
是想要结合
Compose UI
来使用的,所以对此在
Compose
中一些现成的函数支撑,但是
MVI
本身是一种模式,不一定要绑定
Compose UI
使用,甚至都不一定要使用
Kotlin
,像
Channel
,
Flow
这些,哪怕是换了语言或者换了平台,只要是面向对象的语音,应该都是可以定制出来,所以不用整个项目转换为
Compose UI + MVI
也可以使用这种模式 ,但是最好的毕竟是有
google
支持,使用这种模式还是绑定使用
Compose UI
来.后续还会横向对比优缺点.
版权归原作者 CreeLu 所有, 如有侵权,请联系我们删除。