Android Notes (11): Using ViewModel in Compose

The ViewModel component is used to save the data needed in the view. The main purpose of ViewModel is to separate the data model and application logic related to the user interface from the code responsible for actually displaying and managing the user interface and interacting with the operating system to manage data for the UI interface. Common management methods are mainly implemented in two forms: LiveData and StateFlow. In the following, a simple string encryption and decryption application will be combined to illustrate the process of ViewModel managing data.

1. Configuration of ViewModel

Add dependencies in build.gradle.kt of the project module as follows:

You can find this through ViewModel:

(1) ViewModel can persist the interface state.
(2) Can provide access to business logic

2. Introduction to encryption and decryption applications

The simple application of encryption and decryption is shown in the interfaces of Figure 1 and Figure 2. By entering a string in the text box and then clicking Encrypt, you can encrypt the string in the text box and display the output. You can also enter the cipher text in the text box, and then click the decrypt button to decrypt and display the cipher text in the text box.
Insert image description here
Figure 1 Encryption
"Decryption result:"+
Figure 2 Decryption

3. Use the interface to customize status control and existing problems

Before introducing LiveData and StateFlow, let's understand that ViewModel only defines business logic, and the data in the interface is still managed by the interface itself.

1. Define the ViewModel class

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))
}

In the custom ViewModel class MyCyperViewModel, Base64 is used to implement the business logic of the two functions encodeByBase64 and decodeByBase64 for encryption and decryption. No attribute data is defined in this MyCyperViewModel class, and the data is not processed.

2. Define the interface

The following code shows the definition of encryption and decryption:

@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)
        }
    }
}

It can be found from the code that the status data input input into the text input box and the data output in the text display result are managed by the interface itself. Encryption and decryption processing can be achieved by clicking different buttons.

3. Called in the main activity MainActivity

class MainActivity : ComponentActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContent {
    
    
            ForCourseTheme {
    
    
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
    
    
                    HomeScreen()
                }
            }
        }
    }
}

4. There is a problem

During the above implementation process, a problem was discovered. When the mobile phone (simulator) is rotated, all input data and output results in the original running results will be lost, as shown in Figure 3.
Insert image description here

Figure 3 Status data is lost in the interface of rotating mobile phone (simulator).
This is because when the phone (simulator) is rotated, the MainActivity instance will be killed by the system, and then a new MainActivity instance will be created, resulting in the loss of the original state data. , can no longer be displayed in the interface, causing problems such as discontinuous and complete running effects.

4. ViewModel combines LiveData and Lifecycle to manage data

LiveData is an observable data storage class. LiveData differs from other data stores in that it is life cycle aware. This means that LiveData can follow the life cycle of application components such as activities, fragments or services to sense whether the state or data of application components has changed. Makes LiveData an observer for application components that update active lifecycle state.

1. Redefine ViewModel

Modify the above 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))
    }
}

A MutableLiveData variable object output is added to MyCyperViewModel, and the encryption and decryption functions are modified so that the encryption and decryption results are saved in this variable LiveData object output.

2. Modify the interface

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(cyperViewModel:MyCyperViewModel = viewModel()){
    
    
    var input by remember{
    
    mutableStateOf("")}
    var output1 by remember{
    
    mutableStateOf("")}

    val context = LocalContext.current as MainActivity
    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 = {
    
    
                    //加密
                    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)
        }
    }
}

The context object instance of MainActivity is added to the interface and used as the owner of the life cycle, so that the MyCyperViewModel object instance cyperVIewModel can perceive changes in the MainActivity instance. Therefore, clicking the button to implement encryption and decryption adds the following processing:

cyperViewModel.output.observe(context){
output = it
}

The LiveData object output of cyperViewModel observes whether the life cycle owner context (representing the main activity) changes. Once it changes, the observer is notified to modify the value of the interface's status value output1. Since the status value of the interface is still processed by the interface itself, the status value of the ViewModel object is only used to observe whether the data changes to modify the status value of the interface. Therefore, when the phone (simulator) is rotated, the data will still be lost. If you replace the following code:

Text(text = “${output1}”,fontSize = 30.sp,color = Color.White)

Replace with:

Text(text = “${cyperViewModel.output.value}”,fontSize = 30.sp,color = Color.White)

It can be discovered very badly that the displayed result is null. This is because cyperViewModel.output.value is a value, and this value does not have the ability to update the interface. That is, MyCyperViewModel adds a variable LiveData, which only exists as an observer that senses changes.

5. Use StateFlow to manage data in ViewModel

StateFlow is a state container observable data flow that emits current state updates and new state updates to its collector. The current status can also be read through its value attribute.

1. Modify the ViewModel class to add StateFlow to manage state data

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))
    }
}

In the above code, a variable MutableStateFlow object _output is defined as a private quantity, and the state data can be modified in the content of the view component. Then provide an immutable state flow StateFlow object output based on this variable _output. Through this state container, you can observe changes in the data flow and provide data for the interface. In addition, a variable state input is defined, and the changeText() function is defined to track changes in interface input values.

2. Modify the interface

@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)
        }
    }
}

In the modified HomeScreen composable function, the original status value has been deleted and only defined:

val output = cyperViewModel.output.collectAsState()

Use output to collect the state saved in the cyperViewModel.output state stream object.
Insert image description here
Figure 4
The running results show that by combining StateFlow to manage data in the view component, data will no longer be lost. The main two tasks of the ViewModel view component can be completed well:

(1) Persistently retain interface state.
(2) Can provide access to business logic.

references:

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/

Guess you like

Origin blog.csdn.net/userhu2012/article/details/134169733