I’ve recently read several articles about Jetpack MVVM that have made me want to get involved in this mess. I was introduced to the Jetpack suite of development tools in the second half of 2017 and have been using it as the main framework for development ever since. Over the course of this time, I’ve stepped in a few holes, gained some experience, and encapsulated a library in order to extend it to other projects. Of course, the components that Jetpack provides have been perfected, and my work is just icing on the cake. Let me show you how I’m using Jetpack MVVM in my project now.

1. The up-and-comer and the eclipsed MVP

MVP is very powerful and is, or was, the preferred development framework for many large companies. However, compared to today’s MVVM, the former MVP mode in the use of a lot of inconvenience, is inevitably overshadowed.

First and foremost, when writing client code with MVP you need to write a lot of interfaces and classes. In MVP mode, we need to define the methods to be implemented by the Model, View and Presenter through the interface. Then, you need to write three more classes to implement the logic of the three interfaces above. That is, in general, to implement the logic of an interface, you need to write at least six class files.

Second, presenters are bloated. The Presenter is responsible for the interaction between the View and Model layers. The result of the request and the logic of its interaction with the View layer are completed in the Presenter. In many frameworks, to address the strong reference between the View and Model layers, handlers are defined in presenters to pass information through messages. This makes the code too stereotyped, adding a lot of code that has nothing to do with specific business logic. In addition, if the Handler is used as a messaging bridge, because updating the UI occurs in the main thread, the main thread may become overloaded with messages.

So how does MVVM work? The following diagram roughly illustrates the Jetpack MVVM. In fact, the only core MVVM functionality that Jetpack provides is ViewModel and LiveData. ViewModel is mainly used to request data information through the Model, and then pass the information to the View layer through LiveData as the bridge of information interaction. In terms of the number of classes, using MVVM to implement a page, you only need to write three class files.

What about LiveData, which mediates the interaction between the ViewModel and the View layer? You just have to think of it as a normal variable. It caches our data internally. It will notify all observers of changes when you modify the data. Therefore, LiveData does not have the same message congestion problem as handlers.

In addition, using LiveData solves the problem of frequent page refreshes and the timing of the refreshes. For example, a page in the background that listens for data changes via EventBus is first saved to LiveData, and then the UI is refreshed once the page is back in the foreground. So you don’t have to refresh the page multiple times in the background and you control the timing of the refresh.

If you are not familiar with MVP, MVVM and other architectural patterns, or if you want to know how LiveData and ViewModel are implemented, you can refer to my previous articles on this subject (for more technical articles, you can follow my official account [Code Brick]) :

  • MVC, MVP, MVVM, and Componentization in Android Application Architecture
  • Demystify ViewModel Lifecycle Control
  • Demystify The Notification Mechanism in LiveData

2. Jetpack MVVM project practice

The MVVM provided by Jetpack is already powerful enough that many people will see no need for further encapsulation on top of it. Because of this, MVVM presents a lot of strange posture in the actual application process. For example, MVVM and MVP are mixed together, the data interaction format of ViewModel and View layer is chaotic, the ViewModel lists a bunch of LiveData, and so on. In fact, we can better promote and apply MVVM in our projects by simply encapsulating it. That’s why I developed the Android-vmlib framework.

2.1 Reject hodgepodge, a Clean MVVM framework

First of all, as an MVVM framework, Android-VMlib doesn’t do much. I didn’t integrate it with various network frameworks, etc., so it’s a very clean framework. So far its dependencies are as follows,

In other words, it doesn’t add any more libraries to your project than the one I wrote, the image compression library, and the tool class library android-utils. For EventBus, we will not introduce any libraries for you other than a wrapper we provide in the project. As for Allies, we’re just going to inject some event tracking methods into the View layer at the top, and we’re not going to force you to add Allies to your project. That is, all but the two required libraries are optional. The main purpose of the framework is enablement, and it is entirely up to the user to apply it to the project.

Now, let’s look at the library in more detail and the proper posture for using MVVM.

2.2 The ambiguous relationship between MVVM and Databinding

When it comes to MVVM, it’s always hard to get around Databinding. I’ve looked at many MVVM frameworks before, and many of them have Databinding as a necessary solution for their projects. In fact, there is no relationship between MVVM and Databinding.

Databinding provides two-way binding, so many frameworks inject ViewModel directly into XML. I personally dislike this practice. If Databinding is not found, it can’t find variable. If Databinding is not found, it can’t find variable. I use it more to get controls and do assignments in my code. In addition, the code in XML lacks compile-time checking mechanisms, such that when you assign an int to a String in XML, it doesn’t report an error at compile time. In addition, there is a limit to how much business logic can be accomplished in XML. A ternary operator? : has exceeded the width limit of the code. A lot of times you have to put part of your business in Java code and part of your code in XML. When something goes wrong, this makes it harder to identify the problem. I remember that when I used Databinding in my project, there was a problem that the UI did not refresh in time. I can’t remember the specific reason because it was so long ago. Finally, using Databinding can slow down the compilation of your project, which may not be a problem if your project is small, but it can be a serious problem if you have a large project with over 100 modules.

As a framework, what Android-VMlib does on Databinding is enablement. We give you the ability to apply Databinding, but we also give you the option to eliminate Databinding altogether. In the case of Activity, we provide two abstract classes, BaseActivity and CommonActivity. If you want to use Databinding in your project, pass in the layout’s Databinding class and ViewModel using the following class, then bind the control and use it:

class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {

    override fun getLayoutResId(a): Int = R.layout.activity_main

    override fun doCreateView(savedInstanceState: Bundle?). {
       // Obtain the control by binding
       setSupportActionBar(binding.toolbar)
    }
}
Copy the code

If you don’t want to use Databinding in your project, you can extend BaseActivity like this, and then use the traditional findViewById to get the control and use it:

class ContainerActivity : BaseActivity<EmptyViewModel> {

    override fun getLayoutResId(a): Int = R.layout.vmlib_activity_container

    override fun doCreateView(savedInstanceState: Bundle?). {
        // Get the control by findViewById
        // Alternatively, use the control directly by id after introducing kotlin-Android-Extensions}}Copy the code

As you may have seen, I use Databinding more as ButterKinfe. I specifically provided the ability not to include Databinding for another consideration — after using Kotlin-Android-Extensions, you can use the control’s ID directly in your code. There is no need to use Databinding if you only want to retrieve controls through Databinding. For those who really like Databinding’s Databinding capabilities, you can personalize a layer on top of android-vmlib. Of course, I’m not against Databinding. Databinding is a great design concept, but I’m hesitant to apply it to a large scale project.

2.3 Unified Data Interaction Format

Those of you who have experience in back-end development may know that in back-end code, we usually divide the code into DAO, Service and Controler layers. When data is exchanged between each layer, it is necessary to make uniform encapsulation of the data exchange format. The interaction between the back end and front end also encapsulates the data format. As we extend this to MVVM, it is clear that the interaction between the ViewModel layer and the View layer should also have a layer of data wrapping. Here’s the code THAT I saw,

final private SingleLiveEvent<String> toast;
final private SingleLiveEvent<Boolean> loading;

public ApartmentProjectViewModel(a) {
    toast = new SingleLiveEvent<>();
    loading = new SingleLiveEvent<>();
}

public SingleLiveEvent<String> getToast(a) {
    return toast;
}

public SingleLiveEvent<Boolean> getLoading(a) {
    return loading;
}

public void requestData(a) {
    loading.setValue(true);
    ApartmentProjectRepository.getInstance().requestDetail(projectId, new Business.ResultListener<ProjectDetailBean>() {
        @Override
        public void onFailure(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {
            toast.setValue(s);
            loading.setValue(false);
        }

        @Override
        public void onSuccess(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {
            data.postValue(dealProjectBean(projectDetailBean));
            loading.setValue(false); }}); }Copy the code

Here we define a Boolean LiveData interaction to inform the View layer of the loading state of the data. So you need to maintain one more variable, which is not simple enough. In fact, we can do this more elegantly by standardizing the data interaction format.

In Android-vmlib, we use custom enumerations to represent the state of the data,

public enum Status {
    / / success
    SUCCESS(0),
    / / fail
    FAILED(1),
    / / load
    LOADING(2);

    public final int id;

    Status(int id) {
        this.id = id; }}Copy the code

Then, wrap the error message, data result, data state, and reserved fields into a Resource object that serves as a fixed data interaction format.

public final class Resources<T> {
    / / state
    public final Status status;
    / / data
    public final T data;
    // Status, success or error code and message
    public final String code;
    public final String message;
    // Reserve fields
    public final Long udf1;
    public final Double udf2;
    public final Boolean udf3;
    public final String udf4;
    public final Object udf5;

    // ...
}
Copy the code

Explain what the reserved fields are for: they are mainly used as data supplements. For example, when paging, if the View layer wants not only the actual data, but also the current page number, you can slip the page number information into the UDF1 field. Above, I have only one generic option for the various types of underlying data types, such as Long for integers and Double for floating-point types. In addition, we provide an unconstrained type, UDF5.

In addition to data interaction format encapsulation, Android-VMlib also provides a shortcut to the interactive format. As shown in the figure below,

So what does the code look like with resources?

// View layer code
class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {

    override fun getLayoutResId(a): Int = R.layout.activity_main

    override fun doCreateView(savedInstanceState: Bundle?). {
        addSubscriptions()
        vm.startLoad()
    }

    private fun addSubscriptions(a) {
        vm.getObservable(String::class.java).observe(this, Observer {
            when(it!! .status) { Status.SUCCESS -> { ToastUtils.showShort(it.data) }
                Status.FAILED -> { ToastUtils.showShort(it.message) }
                Status.LOADING -> {/* temp do nothing */ }
                else- > {/* temp do nothing */}}}// ViewModel layer code
class MainViewModel(application: Application) : BaseViewModel(application) {

    fun startLoad(a) {
        getObservable(String::class.java).value = Resources.loading()
        ARouter.getInstance().navigation(MainDataService::class.java) ? .loadData(object : OnGetMainDataListener{
                override fun onGetData(a) {
                    getObservable(String::class.java).value = Resources.loading()
                }
            })
    }
}
Copy the code

Isn’t the code much cleaner by encapsulating the data interaction format? As for making your code cleaner, Android-vmlib provides other ways for you, so read on.

2.4 Further simplify the code and optimize the ubiquitous LiveData

When USING ViewModel+LiveData, I needed to define a LiveData for each variable in order to interact with the data, so the code looked like this. I even saw a ViewModel that had 10+ LiveData defined in it. This makes the code look really ugly,

public class ApartmentProjectViewModel extends ViewModel {

    final private MutableLiveData<ProjectDetailBean> data;
    final private SingleLiveEvent<String> toast;
    final private SingleLiveEvent<Boolean> submit;
    final private SingleLiveEvent<Boolean> loading;

    public ApartmentProjectViewModel(a) {
        data = new MutableLiveData<>();
        toast = new SingleLiveEvent<>();
        submit = new SingleLiveEvent<>();
        loading = new SingleLiveEvent<>();
    }

    // ...
}
Copy the code

Then one of my colleagues suggested to me that I think about how to organize the LiveData, and the solution has been promoted and evolved to the point where it is now a better solution — managing singletons’ LiveData uniformly via HashMap. Later, in order to further simplify the ViewModel layer code, I gave this part of the work to a Holder. So the solution is basically,

public class BaseViewModel extends AndroidViewModel {

    private LiveDataHolder holder = new LiveDataHolder();

    // get a LiveData object from the data type to be passed
    public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) {
        return holder.getLiveData(dataType, false); }}Copy the code

Here the Holder is implemented as follows,

public class LiveDataHolder<T> {

    private Map<Class, SingleLiveEvent> map = new HashMap<>();

    public MutableLiveData<Resources<T>> getLiveData(Class<T> dataType, boolean single) {
        SingleLiveEvent<Resources<T>> liveData = map.get(dataType);
        if (liveData == null) {
            liveData = new SingleLiveEvent<>(single);
            map.put(dataType, liveData);
        }
        returnliveData; }}Copy the code

The principle is simple. Using this scheme your code will look like this:

/ / the ViewModel layer
class EyepetizerViewModel(application: Application) : BaseViewModel(application) {

    private var eyepetizerService: EyepetizerService = ARouter.getInstance().navigation(EyepetizerService::class.java)

    private var nextPageUrl: String? = null

    fun requestFirstPage(a) {
        getObservable(HomeBean::class.java).value = Resources.loading()
        eyepetizerService.getFirstHomePage(null.object : OnGetHomeBeansListener {
            override fun onError(errorCode: String, errorMsg: String) {
                getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)
            }

            override fun onGetHomeBean(homeBean: HomeBean) {
                nextPageUrl = homeBean.nextPageUrl
                getObservable(HomeBean::class.java).value = Resources.success(homeBean)
                // Request another page
                requestNextPage()
            }
        })
    }

    fun requestNextPage(a) {
        eyepetizerService.getMoreHomePage(nextPageUrl, object : OnGetHomeBeansListener {
            override fun onError(errorCode: String, errorMsg: String) {
                getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)
            }

            override fun onGetHomeBean(homeBean: HomeBean) {
                nextPageUrl = homeBean.nextPageUrl
                getObservable(HomeBean::class.java).value = Resources.success(homeBean)
            }
        })
    }
}

/ / the View layer
class EyepetizerActivity : CommonActivity<EyepetizerViewModel, ActivityEyepetizerBinding>() {

    private lateinit var adapter: HomeAdapter
    private var loading : Boolean = false

    override fun getLayoutResId(a) = R.layout.activity_eyepetizer

    override fun doCreateView(savedInstanceState: Bundle?). {
        addSubscriptions()
        vm.requestFirstPage()
    }

    private fun addSubscriptions(a) {
        vm.getObservable(HomeBean::class.java).observe(this, Observer { resources ->
            loading = false
            when(resources!! .status) { Status.SUCCESS -> { L.d(resources.data)
                    val list = mutableListOf<Item>()
                    resources.data.issueList.forEach {
                        it.itemList.forEach { item ->
                            if (item.data.cover ! =null
                                && item.data.author ! =null
                            ) list.add(item)
                        }
                    }
                    adapter.addData(list)
                }
                Status.FAILED -> {/* temp do nothing */ }
                Status.LOADING -> {/* temp do nothing */ }
                else- > {/* temp do nothing */}}})}// ...
}
Copy the code

Here we use getObservable(HomeBean::class.java) to get a LiveData

for the interaction between the ViewModel and the View layer, and then pass data through it. The advantage of this approach is that you don’t need to define LiveData everywhere in your code. You can treat the Holder as a pool of LiveData and just retrieve it from the Holder when you need to interact with the data.

Some of you may wonder if the use of Class as a unique marker for getting LiveData from a pool has limited application scenarios. Android-vmlib has taken this into account, which will be explained in the following section.

2.5 Shared ViewModel, configuration can be simpler

If multiple ViewModels are shared between fragments of the same Activity, how do you get them?

If you are not using Android-vmlib, you can simply fetch the ViewModel from the Activity in the Fragment.

ViewModelProviders.of(getActivity()).get(vmClass)
Copy the code

Using Android-vmlib makes this process even simpler — simply declare an annotation on the Fragment. For instance,

@FragmentConfiguration(shareViewModel = true)
class SecondFragment : BaseFragment<SharedViewModel>() {

    override fun getLayoutResId(a): Int = R.layout.fragment_second

    override fun doCreateView(savedInstanceState: Bundle?). {
        L.d(vm)
        // Get and display shared value from MainFragment
        tv.text = vm.shareValue
        btn_post.setOnClickListener {
            Bus.get().post(SimpleEvent("MSG#00001"))}}}Copy the code

Android-vmlib will read your Fragment’s comments, get the value of the shareViewModel field, and decide whether to use the Activity or Fragment to get the ViewModel. Is it more concise?

2.6 Android-VMlib another advantage, powerful utility class support

I’ve seen a lot of frameworks that pack some common utility classes together and offer them to the user. In contrast to Android-vmlib, we support the utility classes as separate projects. The purpose is: 1). It is hoped that the tool class itself can get rid of the dependency on the framework and be independently applied to various projects; 2). As a separate module, it should be optimized separately to continuously improve its functions.

So far, the utility library Android-utils offers 22 separate utility classes covering everything from IO, resource reading, image processing, animation, and runtime permission acquisition, which I’ll cover in a future article.

It should be noted that the library was developed with reference to many other libraries, and of course we developed our own featured utility classes, such as runtime permission fetching, theme property fetching, string concatenation, and so on.

3, Jetpack MVVM pit record and Android-VMLib solution

3.1 Repeated notice, should not come

This part involves the realization principle of ViewModel, if you have not understood its principle, can refer to “Uncover the mystery of ViewModel life cycle control” article to understand.

For example, in my sample code in this project, the MainFragment and SecondFragment share the SharedViewModel, and in the MainFragment, we plug a value into the LiveData. Then we jump to the SecondFragment, and when we come back from the SecondFragment we are notified of the value again.

Many times we only want to be notified of data changes once when we call LiveData#setValue(). At this point, we can solve this problem with SingleLiveEvent. The principle of this class is not that difficult, except that AtomicBoolean is used to manage notifications, currently only when setValue() is called. This solves many of the issues of the page being notified after coming back from the background.

In andoird-vmlib, when you retrieve LiveData from the pool via getObservable(), you can retrieve this type of event with a single parameter.

// Here we use SingleLiveEvent by specifying single to true
vm.getObservable(String::class.java, FLAG_1, true).observe(this, Observer {
    toast("#1.1: "+ it!! .data)
    L.d("#1.1: " + it.data)})Copy the code

The problem with using singleliveEvents,

  1. When a LiveData is retrieved from the pool, it is determined only if the LiveData is of type SingleLiveEvent based on the parameters first retrieved. That is, when you first use vm. GetObservable (String::class.java, FLAG_1, true) to get LiveData, GetObservable (String::class.java, FLAG_1) is used to obtain the same LiveData.

  2. The SingleLiveEvent itself has a problem: when there are multiple observers, it can only notify one of them, and you can’t be sure which one is being notified. This is related to the design principle of SingleLiveEvent, because it notifys the state with atomic Boolean tags, and the state is changed after notifying an observer. In addition, the registered observers are put into the Map and notified using iterator traversal, so the order of notifications cannot be determined (the order of pits after the hash cannot be determined).

3.2 The essence and functions of LiveData

LiveData is essentially the data itself, and is supposed to be a data cache.

According to the article “Demysering the Notification mechanism of LiveData”, the implementation principle is to define an Object of type Object, named data, inside LiveData, and we call setValue() to this Object. The neat thing about LiveData is that it makes use of LifecycleOwner’s lifecycle callback to notify observers of the results when the lifecycle changes. If you are not familiar with these features of LiveData, you are prone to coding problems.

An article, for example, has two main parts: the title and the content, and both are of type String. As a result, when listening through getObservable(Class

dataType), we cannot tell whether the content of the article or the title of the article has changed. So, in addition to getObservable(Class

dataType), in BaseViewModel, We also added the getObservable(Class

dataType, int flag) method to get the LiveData. One way to think about it is that when we specify different flags, we get the LiveData from different “pools” and therefore get different LiveData objects.

public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) {
    return holder.getLiveData(dataType, false);
}

public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType, int flag) {
    return holder.getLiveData(dataType, flag, false);
}
Copy the code

Some of you might want to use the reserved fields of the previously encapsulated Resource object to specify whether the title or the content of the article changed. I strongly advise you not to do this! Because, as we said above, LiveData and Data itself should be one-to-one. The result is that the title and content of the article are set to the same object, and only one cache is maintained in memory. As a result, when the page is in the background, if you update the title of the article first and then the content of the article, only the data of the article will be kept in the cache. When your page comes back from the background, the title cannot be updated to the UI. It should also be noted that if a piece of data is divided into a first half and a second half, you cannot change the second half of the data by overwriting the first half of the data. This will cause the first half of the data to not be updated to the UI. This is not the fault of Android-VMlib. Many times it is easy to fall into this trap without understanding the nature and functions of LiveData.

3.3 Data Recovery problem: Differences in ViewModel versions

In “Demytifying ViewModel lifecycle control,” I analyzed the problem of not being able to retrieve cached results from viewModels. This is because early viewModels implemented life cycle control through empty fragments. So, when the page is killed in the background, the Fragment is destroyed and the ViewModel retrieved is not the same object as the previous ViewModel. In later releases, another solution was adopted for the ViewModel’s state recovery problem. Here are the differences between the two versions of the library (the first is an earlier version, the second is a more recent version),

Recent versions have abandoned the previous Fragment solution and instead saved ViewModel data via savedState. This issue is mentioned again to remind you to be aware of the version of the library you choose and the problems with earlier versions to avoid pitfalls.

conclusion

This article introduced Android-VMlib and some of the problems encountered while working with MVVM. If you encounter other problems in the process of using can communicate with the author.

For this library, the address is github.com/Shouheng88/… The main reason for developing this library was to improve the efficiency of personal development and avoid having to Copy the code every time you start a new project. In addition to MVVM, examples of componentized, servized architectures are included in the project, and the source code is available for those interested.

I’ve been maintaining three libraries since last year. In addition to the utility library AndroidUtils, there’s also a UI library called AndroidUIX, which is not quite mature yet. In addition to making my own development more efficient, I opened it up to help more individual developers improve their development efficiency. After all, these days, 996, downsized, 35… Programmers have been “house of Flying daggers.” I’m also doing this to open up a new way to make a living for myself and other developers. This is my original intention and my purpose. You can also join us if you are interested 🙂

Above, thanks for reading ~