MVVM mode is based on data-driven UI, we can use ViewModel to decouble Activity and View. Compared with the FREQUENT interaction between Presenter and View in MVP mode and the complex engineering structure,MVVM mode is more clear and concise. With DataBinding, the View layer is no longer weak, and by binding properties/events, the ViewModel can interact with the View in the layout file. Here’s a look at my encapsulation practices for the MVVM pattern using the article search feature in the Wandroid project, and welcome to comment on your thoughts.

The project address

Github.com/iamyours/Wa…

DataBinding custom properties and events

Create a new commonbindings.kt file to handle DataBinding custom properties/events and add the BindingAdapter for SmartRefreshLayout and EditText to search the business

@BindingAdapter(
    value = ["refreshing"."moreLoading"."hasMore"],
    requireAll = false
)
fun bindSmartRefreshLayout(
    smartLayout: SmartRefreshLayout,
    refreshing: Boolean,
    moreLoading: Boolean,
    hasMore: Boolean

) {// State binding, control stop refresh
    if(! refreshing) smartLayout.finishRefresh()if(! moreLoading) smartLayout.finishLoadMore() smartLayout.setEnableLoadMore(hasMore) }@BindingAdapter(
    value = ["autoRefresh"])
fun bindSmartRefreshLayout(
    smartLayout: SmartRefreshLayout,
    autoRefresh: Boolean
) {// Control automatic refresh
    if (autoRefresh) smartLayout.autoRefresh()
}

@bindingAdapter (// drop down to refresh, load more value = ["onRefreshListener"."onLoadMoreListener"],
    requireAll = false
)
fun bindListener(
    smartLayout: SmartRefreshLayout,
    refreshListener: OnRefreshListener? , loadMoreListener:OnLoadMoreListener?). {
    smartLayout.setOnRefreshListener(refreshListener)
    smartLayout.setOnLoadMoreListener(loadMoreListener)
}

// Bind soft keyboard search
@BindingAdapter(value = ["searchAction"])
fun bindSearch(et: EditText, callback: () -> Unit) {
    et.setOnEditorActionListener { v, actionId, event ->
        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
            callback()
            et.hideKeyboard()
        }
        true}}Copy the code

BaseViewModel

We add all the properties and methods we use to the BaseViewModel. We need refreshing and moreLoading controls to refresh SmartRefreshLayout. HasMore controls whether there is more, Page controls paging requests, and agrees to process paging data (refresh status, is there more) in the mapPage method

open class BaseViewModel : ViewModel() {
    protected val api = WanApi.get(a)protected val page = MutableLiveData<Int> ()val refreshing = MutableLiveData<Boolean> ()val moreLoading = MutableLiveData<Boolean> ()val hasMore = MutableLiveData<Boolean> ()val autoRefresh = MutableLiveData<Boolean> ()//SmartRefreshLayout automatically refreshes the markup

    fun loadMore(a){ page.value = (page.value ? :0) + 1
        moreLoading.value = true
    }

    fun autoRefresh(a) {
        autoRefresh.value = true
    }

    open fun refresh(a) {// Some interfaces may have a first page of 1, so rewrite it
        page.value = 0
        refreshing.value = true
    }

    /** * process paging data */
    fun <T> mapPage(source: LiveData<ApiResponse<PageVO<T> > >): LiveData<PageVO<T>> {
        return Transformations.map(source) {
            refreshing.value = false
            moreLoading.value = falsehasMore.value = ! (it? .data? .over ? :false)
            it.data}}}Copy the code

SearchVM

The SearchVM is then created as the ViewModel for the search business

class SearchVM : BaseViewModel() {val keyword = MutableLiveData<String>() // The LiveData<ApiResponse<PageVO<ArticleVO>>> private val _articlePage = Transformations.switchMap(page) { api.searchArticlePage(it, keyword.value ? :""} // LiveData<PageVO<ArticleVO>> val articlePage = mapPage(_articlePage) funsearch() {// Search data autoRefresh()}}Copy the code

Layout file

DataBinding’s custom properties/events have been added earlier, and we will now use them in the layout. Start by adding SearchVM for data interaction

 <variable
    name="vm"
    type="io.github.iamyours.wandroid.ui.search.SearchVM"/>

Copy the code

You then bind events and properties in SmartRefreshLayout

 <com.scwang.smartrefresh.layout.SmartRefreshLayout
		.
    app:onRefreshListener="@{()->vm.refresh()}"
    app:refreshing="@{vm.refreshing}"
    app:moreLoading="@{vm.moreLoading}"
    app:hasMore="@{vm.hasMore}"
    app:autoRefresh="@{vm.autoRefresh}"
    app:onLoadMoreListener="@{()->vm.loadMore()}">


Copy the code

Bidirectionally bind the keyword property in EditText to add a soft keyboard search event

 <EditText
    .
    android:text="@={vm.keyword}"
    android:imeOptions="actionSearch"
    app:searchAction="@{()->vm.search()}"
    />
Copy the code

The complete layout is as follows:

<?xml version="1.0" encoding="utf-8"? >
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
                name="vm"
                type="io.github.iamyours.wandroid.ui.search.SearchVM"/>
    </data>

    <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="48dp">

            <TextView
                    android:id="@+id/tv_cancel"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:textSize="14sp"
                    android:layout_marginRight="10dp"
                    android:gravity="center"
                    android:textColor="@color/text_color"
                    android:layout_alignParentRight="true"
                    android:text="Cancel"
                    app:back="@{true}"
                    />

            <EditText
                    android:layout_width="match_parent"
                    android:layout_height="40dp"
                    android:layout_toLeftOf="@id/tv_cancel"
                    android:lines="1"
                    android:layout_marginRight="10dp"
                    android:inputType="text"
                    android:paddingLeft="40dp"
                    android:hint="Search terms separated by Spaces"
                    android:layout_centerVertical="true"
                    android:textSize="14sp"
                    android:layout_marginLeft="10dp"
                    android:textColor="@color/title_color"
                    android:textColorHint="@color/text_color"
                    android:background="@drawable/bg_search"
                    android:text="@={vm.keyword}"
                    android:imeOptions="actionSearch"
                    app:searchAction="@{()->vm.search()}"
                    />

            <ImageView
                    android:id="@+id/iv_search"
                    android:layout_width="48dp"
                    android:layout_height="48dp"
                    android:tint="@color/text_color"
                    android:src="@drawable/ic_search"
                    android:padding="13dp"
                    android:layout_marginLeft="10dp"
                    />
        </RelativeLayout>

        <View
                android:layout_width="match_parent"
                android:layout_height="1px"
                android:background="@color/divider"
                />

        <com.scwang.smartrefresh.layout.SmartRefreshLayout
                android:id="@+id/refreshLayout"
                android:layout_width="match_parent"
                app:onRefreshListener="@{()->vm.refresh()}"
                app:refreshing="@{vm.refreshing}"
                app:moreLoading="@{vm.moreLoading}"
                app:hasMore="@{vm.hasMore}"
                app:autoRefresh="@{vm.autoRefresh}"
                android:background="@color/bg_dark"
                app:onLoadMoreListener="@{()->vm.loadMore()}"
                android:layout_height="match_parent">

            <com.scwang.smartrefresh.layout.header.ClassicsHeader
                    android:layout_width="match_parent"
                    app:srlAccentColor="@color/text_color"
                    app:srlPrimaryColor="@color/bg_dark"
                    android:layout_height="wrap_content"/>

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recyclerView"
                    android:overScrollMode="never"
                    tools:listitem="@layout/item_qa"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>

            <com.scwang.smartrefresh.layout.footer.ClassicsFooter
                    android:layout_width="match_parent"
                    app:srlAccentColor="@color/text_color"
                    app:srlPrimaryColor="@color/bg_dark"
                    android:layout_height="wrap_content"/>
        </com.scwang.smartrefresh.layout.SmartRefreshLayout>
    </LinearLayout>
</layout>
Copy the code

The Activity listens for data and updates the UI

To simplify ViewModel creation, create a new extension function for FragmentActivity, see lazy.kt, which is initialized as Lazy each time.

inline fun <reified T : ViewModel> FragmentActivity.viewModel(a) =
    lazy { ViewModelProviders.of(this).get(T::class.java)}
Copy the code

Create BaseActivity

open abstract class BaseActivity<T : ViewDataBinding> : AppCompatActivity() {
    abstract val layoutId: Int
    lateinit var binding: T

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, layoutId)
        binding.lifecycleOwner = this}}Copy the code

Then create a new SearchActivity to complete the last step

class SearchActivity : BaseActivity<ActivitySearchBinding>() {
    override val layoutId: Int
        get() = R.layout.activity_search
    val vm by viewModel<SearchVM>()
    val adapter = ArticleAdapter()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding.vm = vm
        initRecyclerView()
    }

    private fun initRecyclerView(a) {
        binding.recyclerView.also {
            it.adapter = adapter
            it.layoutManager = LinearLayoutManager(this)
        }
        vm.articlePage.observe(this, Observer {
            adapter.addAll(it.datas, it.curPage == 1)}}}Copy the code

See DataBoundAdapter here for the encapsulation of Adapter

Search results

With the SearchActivity and SearchVM, you can see that the activity and view are completely decoupled. We put the business logic in the ViewModel and trigger UI changes by modifying the LiveData.