Use of ViewModel and Livedata of jettpack architecture components

Author: stars one
Reprint address: https://juejin.cn/post/7128993794283798536

The Jetpack architecture recommends the use of MVVM structure. For this reason, several MVVM component libraries have been launched for our developers to quickly access. The first thing to talk about is ViewModel

Personal understanding: Activity is View, VM is ViewModel, which is responsible for the logical processing of data, and Model is the data source

ViewModel

introduce

What can ViewModel do?

The ViewModel life cycle is independent of the Activity, and it can elegantly save the data in the memory (the data can be retained when the screen rotates and the horizontal and vertical screens are switched)

ViewMoel can be regarded as a data processor and data warehouse, which is only responsible for processing data

[external link image transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-2r9glffz-1660642390098)( https://upload-images.jianshu.io/upload_images/16900214-82cb22827b235adf.png?imageMogr2/auto -orient/strip%7CimageView2/2/w/1240)]

Basic use

First, import dependencies

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

Here, because I use kotlin, I use the version with kotlin features. If it is pure Java, the following dependencies can be used:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

Here is a simple counter example:

class ViewModelActivity : AppCompatActivity() {
    //4. Declare variables
    lateinit var myViewModel:MyViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_model)
        //5. Get a single instance object through ViewModelProvider
        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        //7. Click listener of setting button
        btnPlus.setOnClickListener {
            myViewModel.countPlus()
            refreshCount()
        }

        refreshCount()
    }

    //6. Set the update data (temporarily, it will be adjusted to live data format later)
    fun refreshCount() {
        tvContent.text = myViewModel.count.toString()
    }
}

//1. Define ViewModel
class MyViewModel : ViewModel() {
    //2. Define data
    var count = 0

    //3. External exposure method, used to modify the value
    fun countPlus() {
        count++
    }
}

The effect is as follows:

Why do I need to use ViewmodelProvider to get a single object? The reason is that the life cycle of ViewModel is independent of Activity, and data can be saved temporarily

The above is a more traditional way. The UI is modified by clicking and listening on the corresponding button. After that, LiveData will be used for modification

The constructor of ViewModel passes parameters

Since ViewModel is a singleton mode, you need to use the interface class ViewModelProvider.Factory to pass parameters

If the MainViewModel needs to receive a parameter from an Activity, we can write as follows:

Add a constructor to the original MainViewModel class

class MyViewModel(val saveCount: Int?) : ViewModel() {
    var count = 0

    init {
        //If no parameter is passed, the default value is 0
        count = saveCount ?: 0
    }

    fun countPlus() {
        count++
    }
}

After that, define a factory class MyViewModelFactory to implement the ViewModelProvider.Factory interface

class MyViewModelFactory(val myCount: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        //Here, the constructor is used to pass parameters
        return MyViewModel(myCount) as T
    }
}

PS: you can write MyViewModelFactory in MyViewModel, so there is no need to complete another file

In Activity, we need to create a new MyViewModelFactory object and use it with ViewModelProvider. The code is as follows:

myViewModel = ViewModelProvider(this,MyViewModelFactory(12)).get(MyViewModel::class.java)

It can be seen that the default is 12, as shown in the following figure

[external link image transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-quqloysu-1660642390135)( https://upload-images.jianshu.io/upload_images/16900214-9dfba48099732c7b.png?imageMogr2/auto -orient/strip%7CimageView2/2/w/1240)]

PS: I feel that there are a few steps to pass parameters to ViewModel in the Activity. However, considering the architecture, this method of passing parameters using the construction method is not very consistent with the MVVM architecture.

Because the Activity changes the data, it can trigger the corresponding data change instead of passing parameters when constructing the method; However, there may also be special requirements (for example, the context object is required in the ViewModel), so this implementation method is left

AndroidViewModel(ViewModel extension class)

As mentioned above, what should we do if the Context object is required in the ViewModel?

It is often encountered that a Context context object needs to be obtained. You may think that it is not enough for us to transfer the current Activity into the ViewModel?

Although this is possible, it will cause other problems and cause too deep coupling between ViewModel and Activity. Originally, ViewModel was designed to reduce coupling, but this is putting the cart before the horse

When using ViewModel, it should be noted that ViewModel cannot hold View, Lifecycle, and Acitivity references, and cannot contain any classes containing the previous contents. This is likely to cause memory leakage.

Considering this problem, the development team provides a subclass AndroidViewModel for our use, which is also inherited from ViewModel. There is an application instance (i.e. application object) in it

You can see the source code of AndroidViewModel

Since each APP has only one application object, there is no need to worry about the above problems

If you use it, like the above, you also need to use the Factory interface. However, we don't need to implement it. We can use the built-in ViewModelProvider.AndroidViewModelFactory class

The specific code is as follows:

class MyViewModel(application: Application) : AndroidViewModel(application) {

    fun getCacheDirPath() :String {
        //MyApplication is my custom Application entry class. If you do not use a custom Application, you can write Application directly here
        var application = getApplication<MyApplication>()
        //Use the application object to get the cache directory path
        return application.cacheDir.path
    }
}

Use in Activity:

 myViewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(MyViewModel::class.java)

You can see that the value set by TextView is the path

Advanced usage

  • As mentioned earlier, ViewModel is actually a singleton mode and its life cycle is independent of Activity, so you can use ViewModel to share data between Activity and Fragment or between fragments

LiveData

The above ViewModel only provides a data warehouse. If we cannot implement the MVVM architecture using traditional objects, we must use LiveData at this time

LiveData is equivalent to adding an additional layer of packaging to the data so that the data can be observed

Since LiveData is also implemented internally using LifeCycle, it is designed to trigger page change only when the data is changed in the visible state of the page, saving resources and errors

Basic use

1. Import dependency

First, import dependency. The version is the same as the version used above. Select the corresponding version according to the type of the project

//kotlin feature version
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

//Java version
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

2. Use MutableLiveData to wrap data

Two classes, MutableLiveData and LiveData, are provided in LiveData to wrap data. Here, MutableLiveData is taken as an example to explain how to use them. The differences between the two classes will be supplemented later

We took the above text as an example and slightly modified it (mainly steps 2, 3 and 7)

class ViewModelActivity : AppCompatActivity() {
    //4. Declare variables
    lateinit var myViewModel:MyViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_model)
        //5. Get a single instance object through ViewModelProvider
        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        //Listening data, UI change
        myViewModel.count.observe(this){
            tvContent.text = it.toString()
        }

        //7. Click listener of setting button
        btnPlus.setOnClickListener {
            //Click to only punish the corresponding data modification, and do not update UI
            myViewModel.countPlus()
        }

    }

}

//1. Define ViewModel
class MyViewModel : ViewModel() {
    //2. Define data
    var count = MutableLiveData<Int>(0)

    //3. External exposure method, used to modify the value
    fun countPlus() {
        val value = count.value as Int
        count.value = value+1
    }
}

The main difference here is that we do not change the UI in the click event. To change the UI, we write a data listening method to listen for data changes and then update the UI

Although the feeling here is the same as before, it is also necessary to modify the value through the TextView object, but from the code point of view, the data processing logic has been separated from the page rendering, which is also convenient for us to write test cases

Difference between setValue() and postValue()

In the above code, we pass count Value = value + 1 to set data. Because it is written by kotlin, it can not be seen that it is setValue() method. If it is Java, it is necessary to call setValue() method to set data

In addition to setValue(),LiveData also provides the postValue() method. The difference between these two methods is that setValue() can only be operated in the main thread (UI thread), while postValue() can be operated in the sub thread or the main thread

We slightly change step 7 in the above code, click the button to start a thread, and then wait for 1s before updating the data

Then an error is reported during operation, as shown in the following figure

Change the setting data operation in step 3

It can operate normally. Click the button and wait for 1s before the data changes, as shown in the following figure:

map

This method is mainly used to convert the data class wrapped by LiveData into a single other type of LiveData, for example

For example, we have a User class, which contains two fields (name and age), and our page only needs to observe the change of name and does not care about the change of age, so we do not need to observe the whole object

You can convert mutablelivedata < user > to mutablelivedata < string > simply by writing as follows

Note: the following code is a code extracted from ViewMode

data class User(var name: String, var age: Int)
//Change to private, no external access
private val user = MutableLiveData<User>(User("Zhang San", 5))

val userName = Transformations.map(user){ it.name }

After that, we only need to listen to the userName data in the Activity and change the UI

PS: add that LiveData cannot monitor a field in an object, but only the memory address of the object

If the memory addresses of the two objects are the same, the corresponding data change listening event will not be triggered

If you want to realize it, the current way of personal exploration is to use the koltin extension method copy(). The following code is to quickly copy an object and change the data of a certain field. After that, the monitoring event of data change can be triggered normally

val user = User("zz", 11)
val newUser = user.copy(name = "hello")

switchMap

As mentioned above, the objects of LiveData are all located in the same ViewModel. However, in reality, we need to get data from other places. At this time, we can consider using this method

Suppose that our User object is obtained through userId, and define a singleton to realize the above functions

object UserRepository{
    fun getUserById(userId: String) :LiveData<User>{
        val userLiveData = MutableLiveData<User>()
        userLiveData.value = User("Zhang San $userId",15)
        return userLiveData
    }
}

Define a method in ViewModel to obtain data

class MyViewModel : ViewModel() {

    fun getUser(userId: String): LiveData<User> {
        return UserRepository.getUserById(userId)
    }
}

At this time, we want to observe the change of this object to render the UI. What should we do?

It is estimated that most people will think of the following code:

myViewModel.getUser("111").observe(this){
    //todo UI rendering
}

However, note that the getUser method in the UserRepository returns new objects every time, so the objects observed each time are actually new and cannot be observed

Transformation idea:

  1. Data monitoring for userId
  2. Change of userId and trigger change of user object
class MyViewModel : ViewModel() {

    val userIdLiveData = MutableLiveData("")

    //User is a mutablelivedata < user > object
    val user = Transformations.switchMap(userIdLiveData){
        UserRepository.getUserById(it)
    }

    fun getUser(userId: String){
        userIdLiveData.value = userId
    }
}

Code in Activity:

myViewModel.user.observe(this){
    tvContent.text = it.name
}

//7. Click listener of setting button
btnPlus.setOnClickListener {
    //Click to only punish the corresponding data modification, and do not update UI
    myViewModel.getUser("445")
}

The effect is as follows:

PS: if parameters are not passed, you can set a mutablelivedata < any? > Object and reassign it to update the data, as shown in the following code

object UserRepository{

    fun refresh() :LiveData<User>{
        val userLiveData = MutableLiveData<User>()
        userLiveData.value = User("Zhang San", 15)
        return userLiveData
    }
}

class MyViewModel : ViewModel() {

    val refreshLiveData = MutableLiveData<Any?>()

    fun refresh() {
        //The corresponding data change notification will be triggered
        refreshLiveData.value = refreshLiveData.value
    }

    val user = Transformations.switchMap(refreshLiveData){
        UserRepository.refresh() 
    }

}

Difference between LiveData and MutableLiveData

LiveData is immutable, MutableLiveData is variable

LiveData is a MutableLiveData subclass. The setValue and postValue in it are not public, so they cannot be called externally. They can only register observers

MutableLiveData rewrites the method (as shown in the following figure) and declares it public, so we can call it anywhere to modify the value (however, it is recommended to change the data in ViewModel)

Code optimization

Although the above-mentioned basic use is realized, the encapsulated code in ViewModel is still unsafe. The reason is that our count variable, as long as we get the ViewModel object in a certain place, we will directly get the count variable and modify it

This will cause data problems, so we need to implement a trusted source to modify the data

Officially recommended practices:

1.ViewModel provides immutable observable data (LiveData object) externally. 2. Internal data can only be changed by calling methods externally

class MyViewModel : ViewModel() {
    //_ count is yes and can only be modified inside ViewModel
    private val _count =MutableLiveData(0)

    //For externally provided variables, observers can be registered in the Activity to modify the UI
    val count :LiveData<Int> get() = _count

    //How to modify data
    fun countPlus() {
        val value = count.value as Int
        _count.value = value+1
    }
}

The Activity code has not been changed. The test shows that the effect is the same, but the writing method of the code is optimized

According to the knowledge points of JetPack architecture components, a detailed explanation of JetPack family bucket is compiled. The specific contents are as follows:

Obtaining method: private message reply JetPack You can pick it up!!!

Tags: Android Design Pattern kotlin architecture jetpack

Posted by hassanz25 on Wed, 17 Aug 2022 10:50:46 +0300