Android Compose UI (三) (Compose UI + MVI)结合使用

1.前言

在这里插入图片描述

在上一篇文章中已经介绍了常规的没有结合Compose UI来使用的MVI模式了,本篇文章就是把之前的内容结合起来,在之前的基础上修改为完整的Compose UI + MVI的案例,如果对于文章中有不理解的可以回过头去看之前的内容.

2.ViewModel的完整代码


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跳转页面的信息出去,实际的业务这里应该还会有登录失败了的话,需要提示用户登录失败之类的提示.

3.View的完整代码

LoginActivity

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就行了
LoginView
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结合起来,这里就不用再费劲的订阅了,直接通过函数引用就可以了.

4.扩展函数相关

@Composable
fun <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来.后续还会横向对比优缺点.

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值