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