Relearn Android - ViewModel

Posted by nepzap2 on Tue, 04 Jan 2022 02:16:44 +0100

Content overview

This article introduces the concept of ViewModel, the problems to be solved, its usage, and the real use of it to develop a function.

If you follow this series of notes, you don't need to worry about the content that hasn't appeared before in this note. Finally, this note implements a small function that depends on lifecycle observer and ViewModel, and doesn't depend on LiveData at all.

Starting from the problem

Note that when the concept of "component" is mentioned below, unless otherwise specified, it refers to components in Android SDK API s such as Activity and Fragment

We will face a problem when developing Android. The operation of many users or systems will rebuild components.

For example, when the user rotates the screen, the Activity will go through the following life cycles in turn:

onPause-->
onStop-->
onDestroy-->
onCreate-->
onStart-->
onRestoreInstanceState-->
onResume-->

This is equivalent to a complete reconstruction. What problems will this cause?

When we develop in the traditional way, all our data is initialized in onCreate (or other life cycle methods). Then, after the Activity goes through the above reconstruction process, all previous data will be lost. Officials call this problem transient data loss. We can use onSaveInstanceState method and Bundle to save and process these transient data, but it will be very troublesome. It can only store simple objects. For complex objects, this object must be serializable.

In fact, the root of the problem is that our data initialization is completely bound to the component life cycle. Finding a way to make it independent of the component life cycle is more reliable than finding a way to save data during component reconstruction and recover data later.

The second problem is that the component bears too much code, which makes the logic of the component chaotic and untenable. according to Application Architecture Guide According to the statement in, the component should only process UI events and render pages. It should not directly save UI states. UI states should be handled by State Holders one by one.

ViewModel solves these problems

ViewModel has a longer life cycle than components. It will not end because of component destruction. It will not end until the component it depends on is Finished. At this time, the onCleared method of ViewModel is called.

The life cycle of ViewModel starts from the first request for it from the component to the component Finished and destroyed.

Because the life cycle of ViewModel is longer than that of components, please do not have any code that depends on the life cycle of components, and do not have any references to views and Activity contexts in ViewModel

See here, the first problem is solved, and the second problem seems needless to say, because when we plan to create a ViewModel, we just want to move the UI state from the component to the ViewModel.

The first small case

class UserViewModel : ViewModel() {
    val users = arrayListOf(
        User("Yu Laoba", 12, "female", "ASDFASDFASDFASDFASDF"),
        User("Yu Laojiu", 13, "female", "ASDFASDFASDFASDFASDF"),
        User("Yu Laoshi", 12, "female", "ASDFASDFASDFASDFASDF"),
        User("Yu Laoxi", 12, "female", "ASDFASDFASDFASDFASDF"),
    )

    override fun onCleared() {
        super.onCleared()
        Log.i("UserViewModel", "ViewModel onCleared")
    }
}
class MainActivity : AppCompatActivity() {
    lateinit var viewModel: UserViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // ...
        val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        textview.text = "share ${viewModel.users.size}User name"
        
    }
}

Do a small experiment. At this time, you rotate the screen of your mobile phone and view the Logcat. You won't see the message of ViewModel onCleared printed in UserViewModel. When you actually end the MainActivity (such as ending the process), you will see this message.

The second case is Fragment communication

We want to create such a function. There are two fragments in the main interface. On the left is the list of all users and on the right is the details of the currently selected user. This situation is not easy to deal with using the previous method development.

We need to write additional code in the Activity to control their interaction logic. When the Fragment on the left is selected as the user, we need to upload an event to the Activity. When the Activity receives this event, it needs to pass the user object selected in the Fragment on the left to the Fragment on the right through some methods. All these interactions need well-defined interfaces, Define an interface for each interaction.

It sounds very uncomfortable. Let's see how to write using ViewModel

Let's finish the Fragment on the left first

class UserListFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val userListView = view.findViewById<RecyclerView>(R.id.user_list)
        userListView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)

        // Get the ViewModel of the Activity through requireActivity
        val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)

        // When a list item is selected, the select method of viewModel is called to select the user
        userListView.adapter = UserListAdaper(requireContext(), viewModel.users) { user ->
            viewModel.select(user)
        }

    }
    // ...
}

In Fragment, in fact, we use the same method as in Activity to obtain ViewModel, but we use requireActivity to obtain it as the Activity containing the Fragment. Recall what I said before: the life cycle of ViewModel starts from the first request for it from the component to the component Finished and destroyed. If this is the first time to obtain the ViewModel through the identity of the Activity, the life cycle of the ViewModel will be opened. No matter where you use the identity of the Activity to obtain the ViewModel, you will get the same instance until the Activity is completed and the ViewModel is destroyed.

Now let's write the logic of the selected user in the UserViewModel

class UserViewModel : ViewModel() {

    // ...

    private var selectedUser: User? = null

    fun select(user: User) {
        selectedUser = user
    }

}

A private attribute is used to record the currently selected user, which means that the currently selected user does not want to be directly accessed by components. Later, we will use other methods to get it in the user detail Fragment on the right.

Then write the user detail Fragment on the right.

class UserDetailsFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ...

        val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)

        viewModel.addOnUserSelectedListener {
            username.text = it.name
            ageAndSex.text = "${it.age}year ${it.sex}"
            desc.text = it.description
        }

    }
}

We see that here, we provide an addOnUserSelectedListener method in the ViewModel. When the user is selected, the ViewModel will call back this function, which is responsible for updating the UI.

The following is the implementation of addOnUserSelectedListener:

private val onUserSelectedListeners: MutableSet<(User) -> Unit> = mutableSetOf(

fun addOnUserSelectedListener(listener: (User) -> Unit) {
    onUserSelectedListeners.add(listener)
    // When a user has been selected before the listener is added, call back first
    selectedUser?.let {
        listener(it)
    }
}

In the above implementation, a private callback function collection is used to save the callback function. If a user has been selected before adding the callback, the existing user is directly used to call the callback once. The purpose is to display the component adding callback directly if there are already selected users, otherwise it will not be displayed until the next selected user.

This approach should be called sticky. Forget it. Anyway, LiveData is like this by default. Sometimes it is necessary, but it is absolutely impossible under some business logic.

Now we have realized the function. You can run it and try it.

But!!! Not yet, not yet. Think about what would happen if the Fragment on the right ended or rebuilt for what reason? The callbacks it added before are still in the callback collection of ViewModel, and these callbacks are useless. Even if they are called, nothing will happen, because the original Fragment that added them has disappeared. This is a memory leak.

We should remove the callback it added at the end of the Fragment.

That is, we want to write such code:

class UserViewModel : ViewModel() {
    // ... 
    fun removeOnUserSelectedListener(listener: (User) -> Unit) {

      onUserSelectedListeners.remove(listener)
    }
}
class UserDetailsFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ...
        viewModel.addOnUserSelectedListener(listener)
    }

    override fun onDestroy() {
        super.onDestroy()
        viewModel.removeOnUserSelectedListener(listener)
    }

}

Looking at the above code, we put the add and remove operations of userSelectedListener into the life cycle function of UserDetailsFragment. In this case, it's obviously harmless because it's too simple, but isn't this the most suitable work for the Lifecycle library learned in the last note? Just practice.

class UserSelectedListener(
    private val viewModel: UserViewModel,
    private val userSeletedListener: (User) -> Unit
) : DefaultLifecycleObserver{
    override fun onCreate(owner: LifecycleOwner) {
        super.onCreate(owner)
        viewModel.addOnUserSelectedListeners(userSeletedListener)
    }

    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        viewModel.removeOnUserSelectedListeners(userSeletedListener)
    }

}

Create a UserSelectedListener to implement defaultlifecycle observer, and write the functions of adding and removing monitoring here.

In UserDetailsFragment, we can write as follows:

val viewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)

lifecycle.addObserver(UserSelectedListener(viewModel) {
    username.text = it.name
    ageAndSex.text = "${it.age}year ${it.sex}"
    desc.text = it.description
})

When using ViewModel, because the UI state is no longer written in the Activity, but in the ViewModel, even the fragments in the page can easily obtain the ViewModel of the Activity. The fragments on both sides only need to interact with the ViewModel, and the Activity is liberated.

Moreover, ViewModel just defines UI States and exposes the interface for components to obtain these States, and it doesn't care who obtains these states. Moreover, the existence of the other party is not known between the two fragments.

So far, with a note, we have moved all life cycle related and UI state related operations outside the component, which brings better readability and testability to our program.

reference resources