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:
- Data monitoring for userId
- 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!!!