preface

This article was published in the second half of last year, and has not been updated since it was published in two series. During this period, I received many requests from readers, but I did not update Python due to work reasons and self-learning drive last year. I would like to thank you for your attention and apologize for your loss. This article series will be updated normally over the next two weeks, as well as updates to existing articles, including this one, to improve the reading experience. This series of articles involves knowledge points mainly for ViewModel, LiveData, DataBinding and Kotlin coroutines, welcome students to pay attention.

recommended

The article will be first published on the public account “Code-Full” and personal blog “Li Yi’s small station”, welcome to follow!

Architecture circumstances

In the current Android development, several commonly used project architecture patterns are MVC, MVP and MVVM. Of course, according to the different volume of the project and business, these models may be integrated to produce other variants, which we will not talk about for the moment. The main character of this article is MVVM, which has become increasingly popular in the last two years, based on Google’s Jetpack family bucket implementation.

First, introduction to ViewModel

The ViewModel is part of the Jetpack bucket and is an important part of building MVVM patterns. Since the ViewModel has a longer life cycle than the Activity/Fragment, it is best not to hold references to the Activity/Fragment in the ViewModel to avoid memory leaks. It is generally recommended that you only do data processing in the ViewModel, saving the page data in the Activity/Fragment, and getting the previous page data when the Activity is destroyed and rebuilt. In addition, Google recommends that an Activity/Fragment should have only one ViewModel, and that a ViewModel can have multiple Model instances (that is, classes that handle data logic, such as network request data).

Eg: When the screen rotates, the Activity may be destroyed before a new instance is created; Since a ViewModel has a longer lifetime than an Activity, a ViewModel in a new Activity holds a reference to the Activity that was destroyed (see the ViewModelProvider source code for ViewModel access). This can lead to a memory leak.

Add a page lifecycle function to the ViewModel

During development, methods in the ViewModel will inevitably be called in the Activity/Fragment lifecycle. We can actively call ViewModel methods in the Activity/Fragment lifecycle, but we have a more perfect method. Let ViewModel have the same lifecycle function as Activity/Fragment, ViewModel can actively call its own methods in its own lifecycle function, more decoupled from the Activity/Fragment, and more convenient unit testing.

Common lifecycle implementation methods

To make the ViewModel have the same lifecycle functions as the Activity, it is common to call the corresponding lifecycle function of the ViewModel base class in the lifecycle function of the Activity base class. Synchronize the ViewModel lifecycle function with the Activity lifecycle function.

class BaseActivity:AppCompatActivity() {...override fun onResume(a) {
        super.onResume()
        viewModel.onResume()
    }
}

class BaseViewModel:ViewModel() {fun onResume(a){}}Copy the code

The use of LifecycleObserver

While the above common approach was sufficient for our basic needs, Google’s Jetpack gives us an even more perfect implementation: LifecycleObserver. LifecycleObserver is an interface that represents an observer of a component’s life cycle, using:

interface ViewModelLifecycle:LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStart(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onStop(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy(a)
}
Copy the code

In the above code, we create an interface called ViewModelLifecycle that inherits from LifecycleObserver. Then, in ViewModelLifecycle, we define onCreate, onResume, and other methods, and add annotations to the methods corresponding to the lifecycle function, indicating that when the Activity executes its lifecycle function, The corresponding annotated custom function is also called (Lifecycle.event.on_any in the annotation means that this annotated method will be called whenever the Activity executes its own Lifecycle function).

The knowledge related to Lifecycle will not be explained in detail in this paper. Students who are interested can search for relevant information by themselves

Add lifecycle functions to the ViewModel

After the interface is defined, the next step is to use ViewModel to implement the ViewModelLifecycle interface mentioned above, and add lifecycle functions for ourselves. The implementation is as follows:

abstract class BaseViewModel : ViewModel(), ViewModelLifecycle {
    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate(a){}override fun onStart(a){}override fun onResume(a){}override fun onPause(a){}override fun onStop(a){}override fun onDestroy(a){}}Copy the code

At this point, the ViewModel has its own lifecycle function, but it’s only a work-in-progress. The LifecycleObserver mentioned above refers to an observer of the component life cycle, and since there are observers, there must be observed, and the interface that the observed needs to implement is LifecycleOwner. For Activity/Frament to be observed, LifecycleOwner needs to be implemented. Activity/Frament in AndroidX already implements the LifecycleOwner interface by default. To synchronize the ViewModel lifecycle function with the Activity/Fragment lifecycle function, do the following in the Activity/Fragment:

// add a ViewModol instance implementing the LifecycleObserver interface as an observer to the Activity/Fragment LifecycleObserver queue
// This method can be called immediately after instantiating the ViewModel
getLifecycle().addObserver(viewModel)

// Remove the ViewModel from the Activity/Fragment lifecycle observer queue
This method can normally be called in the onDestory() method
getLifecycle().removeObserver(viewModel)
Copy the code

Add some common events to the ViewModel

In development, we often encounter scenarios like this:

  • An error occurred while the network was requesting data and a TOAST message was displayed
  • After the network requests data, the data source is empty and no data view is displayed
  • When the network requests data, the Loading view is displayed and the Loading view is closed after the network requests are complete
  • .

For similar operations mentioned above, we can pre-embed the corresponding LiveData in the ViewModel for communication, taking advantage of LiveData’s observer mechanism and not notifying UI updates when the page is inactive. Easy to notify the Activity/Fragment to do the corresponding UI processing, and perfect to avoid memory leaks.

  1. Define common UI operations
interface ViewBehavior {
    /** * Whether to display Loading view */
    fun showLoadingUI(isShow: Boolean)

    /** * Whether to display blank view */
    fun showEmptyUI(isShow: Boolean)

    /** * Pop up Toast message */
    fun showToast(map: Map<String, * >)

    /** * Jump to the page without parameters */
    fun navigate(page: Any)

    /** * Return key click */
    fun backPress(arg: Any?).;

    /** * Close page */
    fun finishPage(arg: Any?).
}
Copy the code
  1. Add event LiveData to ViewModel
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
    // The loading view displays the Event
    var _loadingEvent = MutableLiveData<Boolean> ()private set

    // Display the Event without data view
    var _emptyPageEvent = MutableLiveData<Boolean> ()private set

    // Toast prompts Event
    var _toastEvent = MutableLiveData<Map<String, *>>()
        private set

    // The page with no parameters will jump to Event
    var _pageNavigationEvent = MutableLiveData<Any>()
        private set

    // Click the System return key Event
    var_backPressEvent = MutableLiveData<Any? > ()private set

    // Close the page Event
    var_finishPageEvent = MutableLiveData<Any? > ()private set

    lateinit var application: Application

    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate(a){}override fun onStart(a){}override fun onResume(a){}override fun onPause(a){}override fun onStop(a){}override fun onDestroy(a){}override fun showLoadingUI(isShow: Boolean) {
        _loadingEvent.postValue(isShow)
    }

    override fun showEmptyUI(isShow: Boolean) {
        _emptyPageEvent.postValue(isShow)
    }

    override fun showToast(map: Map<String, * >) {
        _toastEvent.postValue(map)
    }

    override fun navigate(page: Any) {
        _pageNavigationEvent.postValue(page)
    }

    override fun backPress(arg: Any?). {
        _backPressEvent.postValue(arg)
    }

    override fun finishPage(arg: Any?). {
        _finishPageEvent.postValue(arg)
    }

    protected fun showToast(str: String) {
        showToast(str, null)}protected fun showToast(str: String, duration: Int?). {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
            if(duration ! =null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun showToast(@StringRes resId: Int) {
        showToast(resId, null)}protected fun showToast(@StringRes resId: Int, duration: Int?). {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
            if(duration ! =null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun backPress(a) {
        backPress(null)}protected fun finishPage(a) {
        finishPage(null)}}Copy the code
  1. Notification events in the ViewModel are handled in the Activity
abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
    ViewBehavior {

    protected lateinit var viewModel: VM

    protected fun injectViewModel(a) {
        val vm = createViewModel()
        viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
            .get(vm::class.java)
        viewModel.application = application
        lifecycle.addObserver(viewModel)
    }

    override fun init(savedInstanceState: Bundle?). {
        injectViewModel()
        initialize(savedInstanceState)
        initInternalObserver()
    }

    fun getActivityViewModel(a): VM {
        return viewModel
    }

    override fun onDestroy(a) {
        super.onDestroy()
        binding.unbind()
        lifecycle.removeObserver(viewModel)
    }

    protected fun initInternalObserver(a) {
        viewModel._loadingEvent.observeNonNull(this, {
            showLoadingUI(it)
        })
        viewModel._emptyPageEvent.observeNonNull(this, {
            showEmptyUI(it)
        })
        viewModel._toastEvent.observeNonNull(this, {
            showToast(it)
        })
        viewModel._pageNavigationEvent.observeNonNull(this, {
            navigate(it)
        })
        viewModel._backPressEvent.observeNullable(this, {
            backPress(it)
        })
        viewModel._finishPageEvent.observeNullable(this, {
            finishPage(it)
        })
    }

    protected abstract fun createViewModel(a): VM

    /** * Initialize operation */
    protected abstract fun initialize(savedInstanceState: Bundle?).
}
Copy the code

Add an Application to the ViewModel

In a ViewModel, we might use Context, but as mentioned at the beginning of this article, it’s not appropriate to hold references to activities/fragments in a ViewModel. So Google has provided us with an AndroidViewModel. The source code is very simple, as follows:

public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    /** * Return the application. */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    @NonNull
    public <T extends Application> T getApplication(a) {
        //noinspection unchecked
        return(T) mApplication; }}Copy the code

AndroidViewModel adds an Application reference to ViewMdoel. We can do this ourselves to make our packaging more flexible.

abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
    ......

    @SuppressLint("StaticFieldLeak")
    lateinit var application: Application

    ......
}

abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
    ViewBehavior {

    protected lateinit var viewModel: VM

    protected fun injectViewModel(a) {
        val vm = createViewModel()
        viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
            .get(vm::class.java)
        viewModel.application = application
        lifecycle.addObserver(viewModel)
    }
}
Copy the code

Create ViewModel using factory mode

There is no one way to create a ViewModel. The common way to create a ViewModel is as follows

 val viewModel = ViewModelProvider(this).get(vm::class.java)
Copy the code

Creating a ViewModel using the above method is fine for our general needs, but if we need to pass parameters to the ViewModel instantiation, then we must create the ViewModel using the factory method, as shown in the following example:

val vm = LoginViewModel(loginRepository)
viewModel = ViewModelProvider(this,BaseViewModel.createViewModelFactory(vm)).get(vm:class.java)
Copy the code

Six, the summary

This chapter is over wrapping the ViewModel. The next chapter will focus on wrapping the Activity. The complete encapsulation code has been uploaded to Github at github.com/albert-lii/… . Finally, attach the full ViewModel wrapper code:

interface ViewModelLifecycle:LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStart(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onStop(a)

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy(a)
}

/ * * *@author: Albert Li
 * @contact: [email protected]
 * @time: 2020/6/9 8:01 PM
 * @description: Common page operations *@since: 1.0.0 * /
interface ViewBehavior {
    /** * Whether to display Loading view */
    fun showLoadingUI(isShow: Boolean)

    /** * Whether to display blank view */
    fun showEmptyUI(isShow: Boolean)

    /** * Pop up Toast message */
    fun showToast(map: Map<String, * >)

    /** * Jump to the page without parameters */
    fun navigate(page: Any)

    /** * Return key click */
    fun backPress(arg: Any?).;

    /** * Close page */
    fun finishPage(arg: Any?).
}

/ * * *@author: Albert Li
 * @contact: [email protected]
 * @time: 2020/6/7 10:30 PM
 * @description: ViewModel base class *@since: 1.0.0 * /
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {

    // The loading view displays the Event
    var _loadingEvent = MutableLiveData<Boolean> ()private set

    // Display the Event without data view
    var _emptyPageEvent = MutableLiveData<Boolean> ()private set

    // Toast prompts Event
    var _toastEvent = MutableLiveData<Map<String, *>>()
        private set

    // The page with no parameters will jump to Event
    var _pageNavigationEvent = MutableLiveData<Any>()
        private set

    // Click the System return key Event
    var_backPressEvent = MutableLiveData<Any? > ()private set

    // Close the page Event
    var_finishPageEvent = MutableLiveData<Any? > ()private set

    @SuppressLint("StaticFieldLeak")
    lateinit var application: Application

    private lateinit var lifcycleOwner: LifecycleOwner

    override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
        this.lifcycleOwner = owner
    }

    override fun onCreate(a){}override fun onStart(a){}override fun onResume(a){}override fun onPause(a){}override fun onStop(a){}override fun onDestroy(a){}override fun showLoadingUI(isShow: Boolean) {
        _loadingEvent.postValue(isShow)
    }

    override fun showEmptyUI(isShow: Boolean) {
        _emptyPageEvent.postValue(isShow)
    }

    override fun showToast(map: Map<String, * >) {
        _toastEvent.postValue(map)
    }

    override fun navigate(page: Any) {
        _pageNavigationEvent.postValue(page)
    }

    override fun backPress(arg: Any?). {
        _backPressEvent.postValue(arg)
    }

    override fun finishPage(arg: Any?). {
        _finishPageEvent.postValue(arg)
    }

    protected fun showToast(str: String) {
        showToast(str, null)}protected fun showToast(str: String, duration: Int?). {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
            if(duration ! =null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun showToast(@StringRes resId: Int) {
        showToast(resId, null)}protected fun showToast(@StringRes resId: Int, duration: Int?). {
        val map = HashMap<String, Any>().apply {
            put(
                FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
                FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
            )
            put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
            if(duration ! =null) {
                put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
            }
        }
        showToast(map)
    }

    protected fun backPress(a) {
        backPress(null)}protected fun finishPage(a) {
        finishPage(null)}companion object {

        @JvmStatic
        fun <T : BaseViewModel> createViewModelFactory(viewModel: T): ViewModelProvider.Factory {
            return ViewModelFactory(viewModel)
        }
    }
}


/** * create a factory for the ViewModel. The ViewModel created by this method can be passed with */ in the constructor
class ViewModelFactory(val viewModel: BaseViewModel) : ViewModelProvider.Factory {

    override fun 
        create(modelClass: Class<T>): T {
        return viewModel as T
    }
}
Copy the code