Android笔记(二十四)基于Compose组件的MVVM模式和MVI模式的实现

news2024/10/9 23:34:44

仔细研究了一下MVI(Model-View-Intent)模式,发现它和MVVM模式非常的相识。在采用Android JetPack Compose组件下,MVI模式的实现和MVVM模式的实现非常的类似,都需要借助ViewModel实现业务逻辑和视图数据和状态的传递。在这篇文章中,将通过简单的货币兑换实例来展示一下MVVM模式和MVI模式的不同。

一、MVVM模式

MVVM
图1 MVVM模式架构
在MVVM模式中:
M:表示Model,即数据域模型。数据模型中的数据通过视图模型传递给视图,从而更新视图的界面;
V: 表示View,即视图,可以看到的界面;从视图界面中将输入的数据发送给视图模型,视图模型执行业务逻辑,完成某些业务功能。
VM:表示ViewModel,即视图模型,是业务的实际的处理者。它承担着中介的作用,它一方面将数据模型传递给界面,使得界面发生刷新;另一方面,将视图中的数据传递发送给Model数据模型,为后续的业务处理提供数据。在MVVM模式中是双向的数据绑定的。在Android Compose组件定义界面的过程中,往往是数据单向流动的。因此在结合Compose组件实现MVVM模式时,处理与DataBinding组件实现双向绑定是有些不同的。下面通过中美货币兑换应用实例来说明:
1.1 定义数据模型

/**
 * @property type String:货币转换类别,例如RMB->USD,或USD->RMB
 * @property moneny Float:要转换的钱数
 * @property rate Float:转换汇率
 * @constructor
 */
data class Currency(var type:String="RMB->USD",
                    var money:Float=0.0f,
                    var rate:Float=0.14f)

1.2 定义视图模型CurrencyViewModel.kt
CurrencyViewModel类是ViewModel的子类,定义核心业务,即修改界面的状态数据和兑换货币业务处理,代码如下:

class CurrencyViewModel: ViewModel() {
   private var currency = Currency()//要处理的数据

   private var _result: MutableStateFlow<String> =  MutableStateFlow("")//视图模型内部调用
   val result:StateFlow<String> = _result.asStateFlow()//单向数据流提供给视图

   fun updateUI(type:String,money:Float){//修改数据
       if(type == "USD->RMB")
           currency.rate = 7.07f
       else if(type == "RMB->USD")
           currency.rate = 0.14f
       currency.type = type
       currency.money = money
   }

   fun convert() {//兑换货币
     _result.value  ="${currency.money*currency.rate}"
   }
}

1.3 定义视图CurrencyScreen
CurrencyScreen是可组合函数,由多个可组合项构成,代码如下:

@Composable
fun CurrencyScreen(modifier: Modifier, viewModel:CurrencyViewModel) {
    val expandState = remember{ mutableStateOf(false) }//控制下拉列表的状态
    val types = listOf("","CNY->USD","USD->RMB")//定义货币兑换的所有类别
    var type by remember{ mutableStateOf("") }  //定义兑换类别
    var money by remember { mutableFloatStateOf(0.0f) }//定义要兑换的钱数
    val result = viewModel.result.collectAsState()//获取结果状态
    Column(modifier=modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally){

        Text("货币兑换简单应用",fontSize = 32.sp)
        //输入钱数的文本框
        OutlinedTextField(
            modifier = Modifier.size(300.dp,60.dp),
            label = {//提示
                Text("要兑换的钱数:")
            },
            value = "${money}",
            onValueChange = {it:String->
                money = it.toFloat()
            })
        //自定义下拉列表,控制类别
        Row(modifier = Modifier.size(300.dp,60.dp)){
           OutlinedTextField(value = "$type", onValueChange = {}, readOnly = true)//不可编辑
           DropdownMenu(expanded = expandState.value ,
               onDismissRequest = {
                   expandState.value = false
               }) {
               types.forEach {it:String->
                   DropdownMenuItem(text = {
                       Text(it)
                   }, onClick = {
                       type = it           //修改兑换类别
                       expandState.value = false//关闭下拉列表框
                   })
               }
           }
           IconButton(onClick={
               expandState.value = !expandState.value
               viewModel.updateUI(type,money)
           }) {
              Icon(Icons.Filled.PlayArrow, contentDescription = "下拉图标")
           }
        }
        //兑换按钮
        Button(onClick={
            viewModel.convert()                   //执行货币转换
        }){
            Text("货币转换")
        }

        //显示结果
        if(type.isNotBlank())
            Text("$type${result.value}",fontSize = 30.sp)
    }
}

1.4 定义MainActivity
MainActivity调用上述的界面可组合函数CurrencyScreen和创建视图模型CurrencyViewModel对象,代码如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        val viewModel = ViewModelProvider(this).get(CurrencyViewModel::class.java)//创建视图模型对象
        setContent {
            Ch03_DemoTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CurrencyScreen(modifier = Modifier.padding(innerPadding),
                        viewModel =viewModel )//调用可组合函数,生成界面
                }
            }
        }
    }
}

运行结果如图2所示:
在这里插入图片描述
图2

二、MVI模式

在这里插入图片描述
图3 MVI模式架构
Model: 与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态。
View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新。
Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求;
下面仍以货币兑换为例进行介绍。

2.1 定义模型

/**
 * @property operator String:操作的类别
 * @property rmb Double:人民币
 * @property rate Double:汇率
 * @property usd Double:美元
 * @constructor
 */
data class CurrencyState(
    var operator:String="None",
    var rmb:Double=0.0,
    val rate:Double=1.0,
    var usd:Double=0.0)

2.2 定义意图

在本应用中有两个意图,刷新输入界面和兑换货币,因此定义密封类CurrencyIntent,两个子类ConvertToRMBIntent和ConvertToUSDIntent,分别对应兑换成人民币操作和兑换成美元的操作,并通过意图传递参数,代码如下:

sealed class CurrencyIntent {
    data class ConvertToRMBIntent(val operator:String,val usd:Double,val rate:Double):CurrencyIntent()
    data class ConvertToUSDIntent(val operator:String,val rmb:Double,val rate:Double):CurrencyIntent()
}

2.3 定义视图模型

class CurrencyViewModel: ViewModel() {
    private val _state = MutableStateFlow(CurrencyState())
    val output = _state.asStateFlow()

    fun processIntents(intent: CurrencyIntent){//根据意图类型的不同处理意图
        val currentState = _state.value
        when(intent){
            is CurrencyIntent.ConvertToRMBIntent->{
                val newState = currentState.copy(operator=intent.operator,usd =intent.usd,rate = intent.rate)
                newState.rmb = convertToRMB(newState.usd,newState.rate)//执行兑换
                _state.value = newState//修改状态
            }
            is CurrencyIntent.ConvertToUSDIntent->{
                val newState = currentState.copy(operator=intent.operator,rmb=intent.rmb,rate = intent.rate)
                newState.usd = convertToUSD(newState.rmb,newState.rate)//执行兑换
                _state.value = newState//修改状态
            }
        }
    }

    private fun convertToUSD(rmb:Double,rate:Double):Double = rmb*rate
    private fun convertToRMB(usd:Double,rate:Double):Double = usd*rate
}

2.4 定义视图

在视图部分,将处理输入的界面单独定义成CurrencyView,代码如下:

@Composable
fun CurrencyView(modifier:Modifier,onReceivedIntent:(CurrencyIntent)->Unit){
    var expand by remember{mutableStateOf(false)}
    var inputState by remember{mutableStateOf(0.0)}
    var operator by remember{mutableStateOf("None")}
    val operators = listOf("None","CNY->USD","USD->CNY")

    Column(modifier = modifier
        .fillMaxWidth().wrapContentSize()
        .padding(10.dp)){
        Column(horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement =  Arrangement.Center){
            Row{
                OutlinedTextField(
                    value="$inputState",
                    onValueChange = {it:String->
                        inputState = it.toDouble()
                    },
                    label = {
                        Text("输入货币",fontSize=20.sp)
                    }
                )
            }

            Row{
                OutlinedTextField(value = "$operator", onValueChange = {
                })
                IconButton(onClick={
                    expand = !expand
                }){
                    Icon(Icons.Filled.ArrowDropDown, contentDescription = "下拉按钮",tint= Color.Green)
                }
                DropdownMenu(expanded = expand, onDismissRequest = {
                    expand = false
                }) {
                    operators.forEach {it:String->
                        DropdownMenuItem(text = {
                            Text(it,fontSize = 24.sp)
                        }, onClick = {
                            if(it=="USD->CNY"){
                                operator ="USD->CNY"
                                onReceivedIntent(CurrencyIntent.ConvertToRMBIntent("USD->CNY",inputState,7.1))
                                expand = false
                            }else if(it=="CNY->USD"){
                                operator = "CNY->USD"
                                onReceivedIntent(CurrencyIntent.ConvertToUSDIntent("CNY->USD",inputState,0.14))
                                expand = false
                            }
                        })
                    }
                }
            }
        }
    }
}

然后将输入数据的界面CurrencyView在CurrencyScreen调用,并在CurrencyScreen增加兑换的输出结果显示,代码如下:

@Composable
fun CurrencyScreen(modifier:Modifier,viewModel:CurrencyViewModel){
    val state = viewModel.output.collectAsState()
    Column(modifier = modifier){
        CurrencyView(modifier){
            viewModel.processIntents(it) //处理意图
        }
       //显示兑换结果
        if(state.value.operator=="USD->CNY") 
            Text("$ ${state.value.usd} 美元=¥ ${state.value.rmb} 人民币",fontSize=24.sp)
        else if(state.value.operator=="CNY->USD")
            Text("¥ ${state.value.rmb} 人民币=$ ${state.value.usd} 美元",fontSize=24.sp)
    }
}

2.5 定义MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        val viewModel: CurrencyViewModel = ViewModelProvider(this).get(CurrencyViewModel::class.java)
        setContent {
            Ch03_DemoTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CurrencyScreen(modifier=Modifier.padding(innerPadding),
                                   viewModel = viewModel)
                }
            }
        }
    }
}

运行结果如图4所示:
在这里插入图片描述

图4

参考文献

Android应用架构的未来:深入理解MVI模式及其优势 https://cloud.tencent.com/developer/article/2394218

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2200366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32-C3实现UART

配置串口参数 在编写代码之前&#xff0c;你需要确定要使用的 UART 端口号和配置参数&#xff08;波特率、数据位、停止位等&#xff09;。 // 定义 UART 端口 #define TX_PIN 1 // TX 管脚 #define RX_PIN 3 // RX 管脚// 定义串口配置参数 #define UART_BAUDRATE 115200 // …

springboot 项目使用 gitlab 的 API

springboot 项目使用 gitlab 的 API 前言获取用户 access tokenSpring boot项目集成GitLab依赖1 pom依赖2 配置文件3 启动类4 核心代码gitlab 的 API 说明前言 需求是通过gitlab的api获取其中的数据。 gitlab官方API文档:https://docs.gitlab.com/ee/api/users.html gitla…

SpringBoot实现电子文件签字+合同系统

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 在现代企业中&#xff0c;合同管理和电子文件签字已成为日常运营不可或缺的一部分。为了提升效率和安全性&#xff0c;我们可以使用SpringBoot框架来实现一个电子文件签字和合同管理系统。本文将详细介绍如何…

腾讯云SDK连麦应用

音视频终端 SDK&#xff08;腾讯云视立方&#xff09;将新版连麦管理方案的多个功能集成至 腾讯云视立方控制台 > 连麦管理&#xff0c;便于用户快捷使用&#xff0c;具体分为快速上手、连麦应用、用量统计和地址生成器四个功能页面。更多连麦功能说明&#xff0c;请参见 新…

【算法刷题指南】BFS解决FloodFill算法

&#x1f308;个人主页&#xff1a; 南桥几晴秋 &#x1f308;C专栏&#xff1a; 南桥谈C &#x1f308;C语言专栏&#xff1a; C语言学习系列 &#x1f308;Linux学习专栏&#xff1a; 南桥谈Linux &#x1f308;数据结构学习专栏&#xff1a; 数据结构杂谈 &#x1f308;数据…

Leetcode - 周赛418

目录 一&#xff0c;3309. 连接二进制表示可形成的最大数值 二&#xff0c;3310. 移除可疑的方法 三&#xff0c;3311. 构造符合图结构的二维矩阵 四&#xff0c;3312. 查询排序后的最大公约数 一&#xff0c;3309. 连接二进制表示可形成的最大数值 本题数据范围较小&#…

sklearn机器学习实战——随机森林回归与特征重要性分析全过程(附完整代码和结果图)

sklearn机器学习实战——随机森林回归与特征重要性分析全过程&#xff08;附完整代码和结果图&#xff09; 关于作者 作者&#xff1a;小白熊 作者简介&#xff1a;精通python、matlab、c#语言&#xff0c;擅长机器学习&#xff0c;深度学习&#xff0c;机器视觉&#xff0c;目…

南京大学《软件分析》李越, 谭添——1. 导论

导论 主要概念: soundcompletePL领域概述 动手学习 本节无 文章目录 导论1. PL(Programming Language) 程序设计语言1.1 程序设计语言的三大研究方向1.2 与静态分析相关方向的介绍与对比静态程序分析动态软件测试形式化(formal)语义验证(verification) 2. 静态分析:2.1莱斯…

爆红网络的膨胀飞天视频,背后竟是Pika1.5 AI视频模型!

最近&#xff0c;社交网络上疯传着各种动物、建筑等物体膨胀飞上天的搞笑视频。无论是真实的还是虚构的物体&#xff0c;都在视频中被压扁、融化、膨胀&#xff0c;引发了广泛的病毒式传播。而这些有趣的效果&#xff0c;都是由Pika最新推出的1.5版本AI视频模型所制作的。 Ai …

STM32输入捕获模式详解(下篇):PWM输入捕获与PWI模式

1. 前言 在上篇文章中&#xff0c;我们详细介绍了STM32输入捕获模式的基本原理和应用方法&#xff0c;包括测频法和测周法。本文将重点探讨如何通过STM32的PWI&#xff08;PWM Input&#xff09;模式实现对PWM信号的频率和占空比测量。我们将结合具体的硬件电路&#xff0c;解…

[万字解析]从零开始使用transformers微调huggingface格式的中文Bert模型的过程以及可能出现的问题

系列文章目录 使用transformers中的pipeline调用huggingface中模型过程中可能遇到的问题和修改建议 [万字解析]从零开始使用transformers微调huggingface格式的中文Bert模型的过程以及可能出现的问题 文章目录 系列文章目录前言模型与数据集下载模型下载数据集下载 数据加载、…

单细胞转录组 —— simpleaf 原始数据处理

单细胞转录组 —— 原始数据处理实战&#xff08;simpleaf&#xff09; 前言 Alevin-fry 是一个快速、准确且内存节约的单细胞和单核数据处理工具。 Simpleaf 是用 Rust 编写的程序&#xff0c;它提供了一个统一且简化的界面&#xff0c;用于通过 alevin-fry 流程处理一些最…

软件设计师——系统基础开发

&#x1f4d4;个人主页&#x1f4da;&#xff1a;秋邱-CSDN博客☀️专属专栏✨&#xff1a;软考——软件设计师&#x1f3c5;往期回顾&#x1f3c6;&#xff1a;软件设计师——信息安全&#x1f31f;其他专栏&#x1f31f;&#xff1a;C语言_秋邱 ​ 一、软件工程概述 1.1、考…

【Linux】man手册安装使用

目录 man(manual,手册) 手册安装: 章节区分&#xff1a; 指令参数: 使用场景&#xff1a; 手册内容列表: 手册查看快捷键: 实例: 仍致谢:Linux常用命令大全(手册) – 真正好用的Linux命令在线查询网站 提供的命令查询 在开头先提醒一下:在 man 手册中退出的方法很简单…

数字IC/FPGA AMBA总线 (内容参考B站UP主数字逻辑君)

1、 串行总线 SPI IIC UART Fsmc &#xff08;串行总线本文不再赘述&#xff0c;可以参考作者其他文章&#xff09; 总线简介&#xff1a; AMBA常用的系统总线&#xff1a;AHB&#xff0c;ASB&#xff0c;APB&#xff0c;AXI总线&#xff0c;一个Soc和外部的外设不可能每…

zigbee学习

24.10.7学习目录 一.简介1.分层2.zstack通信 一.简介 其是一种新兴的短距离无线通信技术&#xff0c;用于传感控制应用&#xff1b; 特性&#xff1a; 低功耗&#xff0c;比wifi蓝牙功耗更低&#xff1b;低成本&#xff1b;低速率&#xff1b;近距离&#xff1b;短时延&…

老外发微信时说“I‘ll ping you”是什么意思?发微信怎么用英语说柯桥学英语到哪里?

“发信息”还可以怎么说&#xff1f; 其实很简单&#xff0c;message做动词时&#xff0c;可以直接表达&#xff1a;发信息 ▼ &#x1f330;举个例子 I messaged him yesterday but havent had a reply. 昨天我给他发了短信&#xff0c;但没有回音。 我们现在常说的“发信…

使用Python批量修改文件修改日期为随机的6到8月份

使用Python批量修改文件修改日期为随机的6到8月份 每当雪花飘起的时候&#xff0c;总有一股抹不去的情节&#xff0c;会想起儿时雪天的记忆&#xff0c;虽然模糊但也清晰。那时每年的冬季很冷&#xff0c;但依然喜欢飘雪的日子&#xff0c;看着满天迷蒙飘舞的雪花总有想不完的心…

生成树实验

1 生成树关键点&#xff0c; 第一树根&#xff0c;第二在每个非根桥找root端口 第三 在每个物理片段找指定网桥&#xff0c;第四指定网桥对应的端口就是指定端口 bpdu 比较的方式 root 桥&#xff0c;到root 桥的路径开销&#xff0c;指定桥&#xff0c;指定端口&#x…

双登股份再战IPO:数据打架,实控人杨善基千万元股权激励儿子

撰稿|行星 来源|贝多财经 近日&#xff0c;双登集团股份有限公司&#xff08;下称“双登股份”&#xff09;递交招股书&#xff0c;准备在港交所主板上市&#xff0c;中金公司、建银国际、华泰国际为其联席保荐人。 贝多财经了解到&#xff0c;这并非双登股份首次向资本市场…