赞
踩
在上一篇文章中已经介绍了常规的没有结合Compose UI来使用的MVI模式了,本篇文章就是把之前的内容结合起来,在之前的基础上修改为完整的Compose UI + MVI的案例,如果对于文章中有不理解的可以回过头去看之前的内容.
class LoginViewModel : ViewModel() { val loginChannel = Channel<LoginIntent>(Channel.UNLIMITED) private val loginState = MutableStateFlow(LoginState()) val uiLoginState: StateFlow<LoginState> = loginState private val toNewPage = MutableSharedFlow<Boolean>() val uiToNewPage: SharedFlow<Boolean> = toNewPage init { viewModelScope.launch { loginChannel.consumeAsFlow().collect { when (it) { is LoginIntent.RunLoginIntent -> { login() } } } } } private fun login() { 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) } } } private fun getEncryptPassword(password: String): String { //这里随便做一下密码加密,实际应该是做MD5处理或者其他算法处理,密码不以明文形式提交 return password.plus("abc") } private fun getVerify(userName: String, password: String): String { //这里生成校验秘钥,用于接口请求校验,也可以在请求头里面做,一般是用于进一步防止别人模拟请求 return "This is where the data is encrypted" } open class LoginIntent { object RunLoginIntent : LoginIntent() } }
ViewModel
中定义了Channel
和对应的登录状态数据还有需要View
层执行动作的Flow
,一样对外暴露的还是抽象接口,然后在init
函数中订阅channel
,当接收到RunLoginIntent
意图的时候就执行login
函数.在函数中分别执行了以下步骤
- 设置是否正在登录中为
true
,可以看到这里我们通过copy
函数,很方便的就修改了其中某一个值,在页面中接收到这个值的改变后,就会显示登录动画.- 对密码明文进行加密,这里只是简单演示一下,实际会复杂一些.
- 生成校验内容,这个一般是用来服务器防止模拟请求,客户端配合处理就行了
- 调用
APIManager
请求登录数据,这里模拟的是返回一个token
,后续使用这个token
去做其他事情,实际可能是返回一个用户完整数据+令牌或者是其他之类的.- 登录接口返回之后,就把登录中的状态取消掉,因为耗时操作已经完成了.
- 最后判定如果是登录成功,就通过
toNewPage
字段emit
跳转页面的信息出去,实际的业务这里应该还会有登录失败了的话,需要提示用户登录失败之类的提示.
class LoginActivity : ComponentActivity() { companion object { const val TAG = "LoginActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { LoginView().initView() CollectState() } } @Composable private fun CollectState() { hiltViewModel<LoginViewModel>().apply { uiToNewPage.collectWithEffect { if (it) { //执行登录跳转页面 iLog("$TAG 跳转到主页页面") } } uiLoginState.collectWithEffect { value -> if (value.isLogin) { //显示登录加载框 iLog("$TAG 显示了登录进度条") return@collectWithEffect } iLog("$TAG 隐藏登录进度条") } } } } const val UNIT_EXT = 0xff0011
需要注意一下,这里的
hiltViewModel
需要引入一个compose
的库androidx.hilt:hilt-navigation-compose:1.0.0
onCreate
中还是调用的setContent
函数去设置LoginView
完了去订阅相关的业务数据- 订阅数据中,通过接收到状态,去决定跳转以及显示网络请求动画(这里只是标识了一下,实际是操作对应的弹窗以及调用页面跳转代码)
- 一般情况下是一个组数据就可以了,但是这里因为有跳转页面这种一次性的逻辑操作,所以加了
uiToNewPage
的变量- 可以看到所有的数据都是通过
uiLoginState
来管理的,不管是记录的数据状态,还是通知到UI
的状态,维护数据的话也是通过这一个State
就行了
class LoginView() { @OptIn(ExperimentalUnitApi::class) @Preview @Composable fun initView() { 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") } } } @Composable fun LogIcon() { 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) ) } @Composable fun VSpacer(height: Int) { Spacer( modifier = Modifier .fillMaxWidth() .height(height.dp) ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun InputEdit(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
结合起来,这里就不用再费劲的订阅了,直接通过函数引用就可以了.
@Composable
fun <V> Flow<V>.collectWithEffect(collector1: FlowCollector<V>): Int {
LaunchedEffect(key1 = UUID.randomUUID()) {
collect(collector1)
}
return UNIT_EXT
}
这里订阅的时候通过
LaunchedEffect
来订阅数据变化,进行了一下基础的封装.
到此Compose UI + MVI
整体结构内容基本完成,可以看到MVI
模式在android
中的使用google
是想要结合Compose UI
来使用的,所以对此在Compose
中一些现成的函数支撑,但是MVI
本身是一种模式,不一定要绑定Compose UI
使用,甚至都不一定要使用Kotlin
,像Channel
,Flow
这些,哪怕是换了语言或者换了平台,只要是面向对象的语音,应该都是可以定制出来,所以不用整个项目转换为Compose UI + MVI
也可以使用这种模式 ,但是最好的毕竟是有google
支持,使用这种模式还是绑定使用Compose UI
来.后续还会横向对比优缺点.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。