通过ViewModel组件用于保存视图中需要的数据。ViewModel主要目的是将与用户界面相关的数据模型和应用程序的逻辑与负责实际显示和管理用户界面以及与操作系统交互的代码分离开来,为UI界面管理数据。常见的管理方式主要有:LiveData和StateFlow两种形式来实现的。在下列将结合一个简单字符串加密和解密的应用来说明ViewModel管理数据的过程。
一、ViewModel的配置
在项目模块的build.gradle.kt中增加依赖,内容如下:
通过ViewModel可以发现:
(1)ViewModel可以持久保留界面状态。
(2)可以提供对业务逻辑的访问权限
二、加密解密应用简介
加密解密的简单应用如图1和图2的界面展示的。通过在文本框中输入字符串,然后可以点击加密,则可以将文本框中的字符串加密并显示输出。也可以在文本框中输入密文,然后点击解密按钮,则将文本框中的密文进行解密显示。
图1 加密
图2 解密
三、利用界面自定义状态控制和存在的问题
在介绍LiveData和StateFlow之前,来了解一下ViewModel只定义业务逻辑,界面中的数据仍然是有界面自行管理形式。
1.定义ViewModel类
class MyCyperViewModel: ViewModel(){/*** 使用Base64加密* @param content String* @return String*/fun encodeByBase64(content:String):String{val bytes = content.encodeToByteArray()return String(Base64.encode(bytes,Base64.DEFAULT))}/*** 使用Base64解密* @param cyperContent String* @return String*/fun decodeByBase64(cyperContent:String):String=String(Base64.decode(cyperContent,Base64.DEFAULT))
}
在自定义ViewModel类MyCyperViewModel中,利用Base64实现了加密和解密的两个函数encodeByBase64和decodeByBase64的业务逻辑。在这个MyCyperViewModel类中并没有定义任何属性数据,并没有对数据进行处理。
2.定义界面
下列代码展示了加密和解密解密的定义:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(cyperViewModel:MyCyperViewModel = viewModel()){var input by remember{mutableStateOf("")}var output by remember{mutableStateOf("")}Box(modifier = Modifier.fillMaxSize().background(Color.Black).padding(top = 30.dp)){Column(modifier = Modifier.fillMaxWidth(),horizontalAlignment = Alignment.CenterHorizontally){Text("加密和解密的简单应用",fontSize = 30.sp,color = Color.White)TextField(value = input,onValueChange = {it:String->input = it})Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {Button(onClick = {//加密output = "加密的结果:"+cyperViewModel.encodeByBase64(input)}) {Text("加密")}Button(onClick={//解密output = "解密的结果:"+cyperViewModel.decodeByBase64(input)}){Text("解密")}}if(output.isNotBlank())Text("${output}",fontSize = 30.sp,color = Color.White)}}
}
从代码中可以发现,输入到文本输入框中的状态数据input和显示结果的文本中的数据output都是由界面自行来管理的。通过点击不同的按钮,实现加密和解密的处理。
3.在主活动MainActivity中调用
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {ForCourseTheme {Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {HomeScreen()}}}}
}
4.存在问题
在上述实现的过程中,发现了一个问题,当旋转手机(模拟器)时,原有的运行结果中所有输入的数据和输出的结果都会丢失,如图3所示。
图3 旋转手机(模拟器)界面中的状态数据丢失。
这是因为,旋转手机(模拟器),主活动MainActivity实例会被系统杀死,然后再创建一个新的MainActivity主活动的实例,则导致原有的状态数据丢失,无法再界面中显示,带来的问题就时运行效果不连续和完整。
四、ViewModel结合LiveData和生命周期Lifecycle来管理数据
LiveData是一种可观察的数据存储器类。LiveData的与其他数据存储器不同在于,它具有生命周期感知能力。这意味着LiveData可以遵循如 activity、fragment 或 service这些应用组件的生命周期,来感知应用组件的状态或数据是否发生变化。使得 LiveData 成为更新活跃生命周期状态的应用组件的观察者。
1.重新定义ViewModel
修改上述的CyperViewModel
class MyCyperViewModel: ViewModel(){val output: MutableLiveData<String> = MutableLiveData<String>()/*** 使用Base64加密* @param content String* @return String*/fun encodeByBase64(content:String){val bytes = content.encodeToByteArray()output.value = "加密的结果:"+String(Base64.encode(bytes,Base64.DEFAULT))}/*** 使用Base64解密* @param cyperContent String* @return String*/fun decodeByBase64(cyperContent:String){output.value ="解密的结果:"+String(Base64.decode(cyperContent,Base64.DEFAULT))}
}
在MyCyperViewModel增加了一个MutableLiveData可变的对象output,修改加密和解密的函数,使得加密和解密的结果保存在这一个可变的LiveData对象output中。
2.修改界面
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(cyperViewModel:MyCyperViewModel = viewModel()){var input by remember{mutableStateOf("")}var output1 by remember{mutableStateOf("")}val context = LocalContext.current as MainActivityBox(modifier = Modifier.fillMaxSize().background(Color.Black).padding(top = 30.dp)){Column(modifier = Modifier.fillMaxWidth(),horizontalAlignment = Alignment.CenterHorizontally){Text("加密和解密的简单应用",fontSize = 30.sp,color = Color.White)TextField(value = input,onValueChange = {it:String->input = it})Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {Button(onClick = {//加密cyperViewModel.encodeByBase64(input)cyperViewModel.output.observe(context){output1 = it}}) {Text("加密")}Button(onClick={//解密cyperViewModel.decodeByBase64(input)cyperViewModel.output.observe(context){output1 = it}}){Text("解密")}}if(!output1.isNullOrBlank())Text(text = "${output1}",fontSize = 30.sp,color = Color.White)}}
}
在界面中增加了context这个MainActivity的对象实例,把它作为生命周期的拥有者,使得MyCyperViewModel对象实例cyperVIewModel可以感知MainActivity实例的变化。因此,点击按钮实现加密和解密增加如下的处理:
cyperViewModel.output.observe(context){
output = it
}
cyperViewModel的LiveData对象output观察生命周期拥有者context(表示主活动)是否发生变化,一旦发生变化,通知观察者修改界面的状态值output1的值。由于界面的状态值仍是由界面自行处理,ViewModel对象的状态值只是作为观察数据是否变化,来修改界面的状态值而已,因此当旋转手机(模拟器)时,数据仍然会丢失。如果将下列的代码:
Text(text = “${output1}”,fontSize = 30.sp,color = Color.White)
替换成:
Text(text = “${cyperViewModel.output.value}”,fontSize = 30.sp,color = Color.White)
可以非常糟糕地发现,显示结果为null。这是因为cyperViewModel.output.value是一个值,这个值并不具备更新界面的能力。也就是MyCyperViewModel增加了一个可变的LiveData,这个LiveData只是作为感知变化的观察者而存在。
五、使用StateFlow来管理ViewModel中数据
StateFlow是一个状态容器可观察数据流,可向其收集器发出当前状态更新和新状态更新。也可以通过它的value属性读取当前状态。
1.修改ViewModel类增加StateFlow管理状态数据
class MyCyperViewModel: ViewModel(){//私有量只能内部修改数据private val _output = MutableStateFlow("")//获取StateFlow,只能只读val output = _output.asStateFlow()var input by mutableStateOf("")/*** 修改输入状态的值* @param content String*/fun changeText(content:String){input = content}/*** 使用Base64加密* @param content String* @return String*/fun encodeByBase64(content:String){val bytes = content.encodeToByteArray()_output.value = "加密的结果:"+String(Base64.encode(bytes,Base64.DEFAULT))}/*** 使用Base64解密* @param cyperContent String* @return String*/fun decodeByBase64(cyperContent:String){_output.value ="解密的结果:"+String(Base64.decode(cyperContent,Base64.DEFAULT))}
}
在上述代码中定义了一个可变的MutableStateFlow对象_output,将它作为私有量,可以在视图组件内容修改状态的数据。然后提供一个根据这个可变的_output,获取不可变的状态流StateFlow对象output,通过这个状态容器可观察数据流的变化,为界面提供数。另外定义了一个可变的状态input,定义了changeText()函数,用它来跟踪界面输入值的变化。
2.修改界面
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(cyperViewModel:MyCyperViewModel = viewModel()){val output = cyperViewModel.output.collectAsState()Box(modifier = Modifier.fillMaxSize().background(Color.Black).padding(top = 30.dp)){Column(modifier = Modifier.fillMaxWidth(),horizontalAlignment = Alignment.CenterHorizontally){Text("加密和解密的简单应用",fontSize = 30.sp,color = Color.White)TextField(value = cyperViewModel.input,onValueChange = {it:String->//修改输入值cyperViewModel.changeText(it)})Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {Button(onClick = {//加密cyperViewModel.encodeByBase64(cyperViewModel.input)}) {Text("加密")}Button(onClick={//解密cyperViewModel.decodeByBase64(cyperViewModel.input)}){Text("解密")}}if(!output.value.isNullOrBlank())Text(text = "${output.value}",fontSize = 30.sp,color = Color.White)}}
}
修改的HomeScreen可组合函数中,原有的状态值已经删除,只定义了:
val output = cyperViewModel.output.collectAsState()
用output来收集cyperViewModel.output状态流对象中保存的状态。
图4
运行结果可以发现在视图组件中结合StateFlow管理数据,数据不会再丢失。可以很好地完成ViewModel视图组件的主要两个任务:
(1) 持久保留界面状态。
(2) 可以提供对业务逻辑的访问权限。
参考文献:
1.LiveData概览
https://developer.android.google.cn/topic/libraries/architecture/livedata?hl=zh-cn
2.MutableStateFlow
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-state-flow/