Jane address book: www.jianshu.com/p/77e42aebd…

An overview of the

When it comes to MVVM, everyone will think of the front-end MVVM framework, which is less popular in the mobile development world than the front-end MVVM. Google only launched the DataBinding framework in 2015, which was a late start, and 2015 was the year of the MVP model explosion, and 2016 was the year of all the hotfixes and plugins explosion, which was a bad time.

PS: DataBinding and MVVM are not the same. MVVM is an architectural pattern, and DataBinding is an Android framework for binding data to UI, a tool for building MVVM architectures, similar to an enhanced ButterKnife.

Since I came into contact with DataBinding in 2016, I have been struggling with my knowledge in this area, but DataBinding is very convenient to use. Therefore, I have been constantly exploring how to build an appropriate MVVM architecture program. After several project reconstructions, Finally, I recently explored a more suitable MVVM architecture for Android with the Kotlin language.

Github Example for building An Android MVVM application using Kotlin: github.com/ditclear/Pa…

Let’s take a look at what MVVM is and then walk through the entire MVVM framework step by step

MVC, MVP, MVVM

Let’s start with an overview of the build model for Android development

MVC

Model: Entity Model, data acquisition, storage, etc

View: Activity, Fragment, View, Adapter, XML, etc

Controller: Handles data, business, etc for the View layer

From this point of view, Android itself conforms to the MVC architecture. However, because XML is so weak as a pure View, and the Controller offers little to the developer, you might as well have handled it directly on the Activity page, but this creates a code explosion. A page with complex logic that can run to thousands of lines, poorly written comments that are difficult to maintain, and difficult to unit test, is more of a Model-View architecture than a solid Android project.

MVP

Model: Entity Model, data acquisition, storage, etc

View: Activity, Fragment, View, Adapter, XML, etc

Presenter: Performs the interaction and business logic between the View and Model to call back results.

Previously, the Activity acts as a View and Controller, causing the code to explode. The MVP architecture handles this very well. The core idea is to decouple Presenter from the real View layer through an abstract View interface (not the real View layer). Persenter holds the View interface and operates on it rather than directly on the View layer. This decouples the View operations from the business logic, making the Activity a true View layer.

This is the more popular architecture today, but it has its drawbacks. If the business is complicated, the P layer may be too bloated, and the V and P layer have a certain degree of coupling, if there is a need to change the UI, then the P layer is not only a simple change, but also need to change the View interface and its implementation, lead the whole body, using MVP peers are complaining about this.

MVVM

Model: Entity Model, data acquisition, storage, etc

View: Activity, Fragment, View, Adapter, XML, etc

ViewModel: Responsible for the interaction and business logic between the View and Model, changing the UI based on the DataBinding

MVVM’s goals and ideas are similar to those of MVP, but without the annoying callbacks of MVP, it uses DataBinding to update the UI and state to the desired effect.

Data-driven UI

In MVC or MVP development, if we want to update the UI, we first need to find the reference to the view and then assign the value to it before we can update it. In MVVM, this is not required. MVVM drives the UI through data and this is all done automatically. The importance of data has increased in the MVVM architecture to become the dominant factor. In this architectural pattern, developers focus on how to process data and ensure that it is correct.

Separation of concerns

A common mistake is to write all your code in an Activity or Fragment. Anything not related to UI and system interaction should not be placed in these classes. Keep them as simple and light as possible to avoid many lifecycle issues. In MVVM architecture mode, data and business logic are in ViewModel. ViewModel only cares about data and business and does not need to deal with UI directly. Model only needs to provide data source of ViewModel, while View cares about how to display data and handle interaction with users.

From the above overview and comparison with MVC and MVP, we can see that MVVM still has a lot of advantages, and when coupled with Kotlin, it can be even better.

How to start?

In fact, the structure is already clear, so we just need to do what each layer of the M-V-VM layer should do, which is to achieve separation of concerns.

The M layer’s focus is how do you feed the ViewModel data

The ViewModel layer is concerned with how to handle data (including binding data with DataBinding and controlling loading and empty states).

The View layer is concerned with displaying data, receiving user actions, and calling methods in the ViewModel

In order to make the MVVM architecture more suitable for Android, AOP, Dagger2, RxJava, Retrofit, Room and Kotlin are used, and unified naming conventions and invocation guidelines are followed to ensure consistency in development.

Here is our current architecture:

Create the article details interface

Next I will show how the three layers of M-V-VM work together, using the article details page as an example

V – VM

The UI consists of artcileDetailActivity. kt and article_detail_activity. XML.

To drive the UI, our data model needs to hold several elements:

  • Article Id: the Id of the Article details used to load the Article details
  • Title: The title of the article
  • Content: The content of an article
  • State: Load state, encapsulated by a state class

We will create a ArticleDetailViewModel. Kt to save.

A ViewModel provides data for a particular UI component, such as a fragment or activity, and is responsible for communicating with the business logic part of the data processing, such as calling other components to load data or forwarding user changes. The ViewModel does not know the View exists and is not affected by configuration change.

Now we have three files.

Article_detail_activity. XML: Defines the UI of the page

ArticleDetailViewModel. Kt: prepare data for UI classes

ArtcileDetailActivity. Kt: display the data in the ViewModel controller and respond to user interaction

Here’s the implementation (only the main parts are shown for simplicity) :

<?xml version="1.0" encoding="utf-8"? >
<layout >

    <data>
        <import type="android.view.View"/>
        <variable
                name="vm"
                type="io.ditclear.app.viewmodel.ArticleDetailViewModel"/>
    </data>

    <android.support.design.widget.CoordinatorLayout>

        <android.support.design.widget.AppBarLayout>

            <android.support.design.widget.CollapsingToolbarLayout>
                <android.support.v7.widget.Toolbar
                        app:title="@{vm.title}"/>

            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.widget.NestedScrollView>

            <LinearLayout>
                <ProgressBar
                        android:visibility="@{vm.loading? View.VISIBLE:View.GONE}"/>

                <WebView
                        android:id="@+id/web_view"
                        app:markdown="@{vm.content}"
                        android:visibility="@{vm.loading? View.GONE:View.VISIBLE}"/>

            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>


    </android.support.design.widget.CoordinatorLayout>
</layout>
Copy the code
/** *@paramThe repo data source Model(M in MVVM) is responsible for providing the data that needs to be processed in the ViewModel
class ArticleDetailViewModel @Inject constructor(val repo: ArticleRepository) {

    //////////////////data//////////////
    lateinit var articleId:Int
    val loading=ObservableBoolean(false)
    val content = ObservableField<String>()
    val title = ObservableField<String>()

    //////////////////binding//////////////
    fun loadArticle(a):Single<Article> = repo.getArticleDetail(articleId) .async() .doOnSuccess { t: Article? -> t? .let { title.set(it.title)
                        content.set(it.content)

                    }
                }
                .doOnSubscribe { startLoad()}
                .doAfterTerminate { stopLoad() }



    fun startLoad(a)=loading.set(true)
    fun stopLoad(a)=loading.set(false)}Copy the code
/** * ArticleDetailActivity, which handles interactions with the user (click events), Created by ditClear on 2017/11/17. */
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {

    override fun getLayoutId(a): Int = R.layout.article_detail_activity

    @Inject
    lateinit var viewModel: ArticleDetailViewModel
  	
  	//init
  	override fun initView(a) {
       // all KEY_DATA, don't name it yourself
        val articleID: Int? = intent? .extras? .getInt(Constants.KEY_DATA)if (articleID == null) {
            toast("The article does not exist.", ToastType.WARNING)
            finish()
        }

        getComponent().inject(this)
      
        mBinding.vm = viewModel.apply {
            this.articleID = articleID
        }
    }
  
  	// Load data
    override fun loadData(a) {

        viewModel.loadData()
                .compose(bindToLifecycle())
// .doOnSubcribe{ showLoadingDialog() }
// .doAfterTerminate{ hideLoadingDialog() }
                .subscribe({},{ dispatchFailure(it) })

    }

}
Copy the code

How do they work?

After entering the ArticleDetailActivity page

  1. The init() method -> initializes the data first, binding the viewModel to the XML file
  2. The loadData() method -> calls the viewModel method

Enter the ArticleDetailViewModel

  1. Call the Model layer fetch details method to get the data source
  2. Convert the data as needed using the RxJava operator and update the UI via DataBinding
  3. Returns an observable Single object to the View

Return to the ArticleDetailActivity page

  1. Bind the life cycle to avoid memory leaks
  2. Subscribe to the returned observables
  3. Deal with successes and failures

At this point, it’s clear how the V-vms work together.

M – VM

Now we have the View associated with the ViewModel, but how does the ViewModel get its data?

We use Retrofit to get network data from the back end.

interface ArticleService{
    // Article details
    @GET("article_detail.php")
    fun getArticleDetail(@Query("id") id: Int): Single<Article>
}
Copy the code

Use the Room database for persistence

@Dao
interface ArticleDao{

    @Query("SELECT * FROM Articles WHERE articleid= :id")
    fun getArticleById(id:Int):Single<Article>


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertArticle(article :Article)

}
Copy the code

Network and local operations are then encapsulated using articlerepository.kt

ArticleRepository * provides data to the ViewModel layer, handles the relationship between network data and the local cache * Created by ditclear on 2017/11/17
class ArticleRepository @Inject constructor
	(private val remote: ArticleService, private val local: ArticleDao) {

    /* Update the local cache */ if there is no local cache */
    fun getArticle(articleId: Int): Single<Article> =
           local.getArticleById(articleId).onErrorResumeNext {
        if (it isEmptyResultSetException) { remote.getArticleDetail(articleId).doOnSuccess { t -> t? .let { local.insertArticle(it) } } }else throw it
    }
  
    }
Copy the code

Check to see if there is a local cache, if not, then request the network, and update the local cache after success.

The reason for wrapping it as a Repository is that the ViewModel does not need to know exactly where its data comes from, which is not the ViewModel layer’s concern.

Even if your project does not cache data and always pulls data from the network, encapsulating it as a Repository is recommended. This means that your network layer is replaceable, similar to encapsulating an ImageLoadUtil.

The overall process is so much, in fact, it is very simple to understand. The key points are clear responsibilities between layers, decoupling (Dagger2) and the need for a unified specification when using DataBinding.

And subdivision, optimization, that is, modular, componentized work, more in-depth plug-in, hot repair and so on. However, great oaks grow from little acorns. Only when we lay a solid foundation will our future work be relatively easy.

The code for this article can be found at github.com/ditclear/Pa… Found in the

Some advice

Tip 1: Handle click events in an Activity or Fragment

Use Presenter to inherit view.onClickListener

interface Presenter:View.OnClickListener{
    override fun onClick(v: View?).
}
Copy the code

And then implement it in BaseActivity/BaseFragment

abstract class BaseActivity<VB : ViewDataBinding> : RxAppCompatActivity(),Presenter{
    
}
Copy the code

So when we want to set the click event, we just need to

class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
  
  	/ /...
 	//init
  	override fun initView(a) {

        mBinding.let{
            it.vm=mViewModel
          	it.presenter=this}}}Copy the code

When used in XML, the presby.onclick (view) method is used uniformly

<layout>
	<data>
  		<variable
                name="presenter"
                type="com.ditclear.paonet.view.helper.presenter.Presenter"/>
    </data>
  
  	<android.support.design.widget.CoordinatorLayout>
  		
      	<android.support.design.widget.FloatingActionButton
                android:id="@+id/stow_fab"                                                             				  
                android:onClick="@{(v)->presenter.onClick(v)}"
/>
  	</android.support.design.widget.CoordinatorLayout>
</layout>
Copy the code

The actual processing is placed in the corresponding Activity/Fragment

class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
  
  	/ /...
  	@SingleClick
  	override fun onClick(v: View?). {
        when(v? .id) { R.id.stow_fab -> stow()//more ..
          	R.id.other_action -> other()
        }
    }
  / / other
   private fun stow(a){}/ / collection
    private fun stow(a) {
        viewModel.stow().compose(bindToLifecycle())
                .subscribe({ toastSuccess(it?.message?:"Successful collection") }
                        , {  toastFailure(it) } })
    }
}
Copy the code

SingleClick is an annotation that acts as an AspectJ aspect to prevent multiple clicks. View is required as an argument. See article for more details

DataBinding in conjunction with AspectJ prevents multiple clicks

This is one of the reasons for handling click events this way, but another benefit is that it is easy to bind life cycles and perform callbacks (such as dialogs and toasts that need to use the Activity context, Both can be written in the doOnSubscribe and doAfterTerminate operators), preventing the ViewModel layer from holding the context.

Tip 2: Write more unit tests

Unit tests ensure the correctness of data and logic, and the syntax is relatively simple and easy to learn. And running a unit test takes milliseconds to run an app.

I think one of the immediate differences between a programmer and a regular coder is unit testing.

And since the ViewModel layer is pure Kotlin/Java code, it feels like writing simple console programs using Eclipse before.

Of course, unit testing is not limited to writing test code, I usually play with RxJava operators, do some algorithm exercises, verify that the data output is correct and so on.

If you want to learn or learn about unit testing, check out the following articles:

Everything you need to know about Android Unit Testing

A complete example of testing the MVP architecture using Kotlin and RxJava

About the DataBinding

Many developers abandon DataBinding because errors are difficult to troubleshoot. It only shows that many xxBindings were not found. If you have any experience with it, just look at the last error message. Here’s a method I often use to check for errors: Running it in Terminal in Android Studio

./gradlew clean assembleDebug

or

./gradlew compileDebugJavaWithJavac

Because DataBinding is compiled to generate code, many of the errors are caused by expressions written in XML that have problems, so running the above commands is easy to print specific error messages in Terminal. These commands are useful for troubleshooting errors in frameworks that need to compile generated code, such as Dagger2.

See the DataBinding Practical Guide for more information

specification

In order to use DataBinding without making mistakes, it is necessary to follow the uniform specification

  1. ViewModel — View — XML — Model should (as far as possible) correspond to each other, starting with a function module

Average page

ViewModel View XML
ArticleDetailViewModel.kt ArticleDetailActivity.kt article_detail_activity.xml

List pages: See the article goodbye to repetitive, redundant custom Adapters

ViewModel View XML
ArticleListViewModel.kt ArticleListActivity.kt article_list_activity.xml
Item ViewModel Item XML
ArticleItemViewModel.kt article_list_item.xml

Model layer named

Remote Local Repository
ArticleService.kt ArticleDao.kt ArticleRepository.kt

The structure is shown in the figure below:

  1. Unified naming of variables in XML layout files

    ViewModel Presenter(Click event) Item(list Item)
    vm presenter item

The resources

How to build Android MVVM application framework

App Development Architecture Guide