1. 前言

最近看到很多公司的招聘要求上写着“熟悉 MVVM + DataBinding + Kotlin 开发模式”,看来这种模式现在已经成了主流了。之前也零散的了解过 MVVM 和 DataBinding,但是也把它们串联在一起过,更何况加上不太熟悉的 Kotlin。对于 Kotlin ,从 2020 年开始写的博客强制要求自己采用 Kotlin,但是还处于零零散散的知识点。通过这篇文章来增加自己对 Kotlin 的熟悉。

2. MVVM

MVVM 中,M 是指 Model,V 是指 View,VM 是指ViewModel。

View :对应于Activity和 xml,负责 View 的绘制以及与用户交互;

Model:实体模型,网络请求并获取对应的实体模型;

ViewModel:负责完成View于Model间的交互,负责业务逻辑,ViewModel 中一定不能持有 View 的引用,否则又是 MVP 了。

在MVVM中,数据和业务逻辑处于独立的 View Model 中,ViewModel 只要关注数据和业务逻辑,不需要和UI或者控件打交道。由数据自动去驱动 UI 去自动更新 UI,UI 的改变又同时自动反馈到数据,数据成为主导因素,这样使得在业务逻辑处理只要关心数据,方便而且简单很多。

3. 构建 MVVM

这里采用 wanAndroid 中的一个接口,获取数据并显示在页面中:

Android kotlin 如何动态改变recycleview gridlayoutmanager的行数 android kotlin mvvm_DataBinding

3.1 ViewModel

创建 BaseViewModel,BaseViewmodel 继承于 AndroidViewModel,AndroidViewModel 的父类是 ViewModel。AndroidViewModel 和 ViewModel 相比多持有了一个 application,方便在创建的 ViewModel 中使用 Context。

AndroidViewModel:

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

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

    @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
    @NonNull
    public <T extends Application> T getApplication() {
        return (T) mApplication;
    }
}

BaseViewModel:

package cn.zzw.mvvmdemo.mvvm.base.viewmodel

import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import cn.zzw.mvvmdemo.App
import kotlinx.coroutines.*
import java.util.concurrent.CancellationException

typealias Block<T> = suspend () -> T
typealias Error = suspend (e: Exception) -> Unit
typealias Cancel = suspend (e: Exception) -> Unit

/*所有网络请求都在 viewModelScope 域中启动,当页面销毁时会自动*/
open class BaseViewModel() : AndroidViewModel(App.instance) {

    /*
    * 启动协程 
    */
    protected fun launch(block: Block<Unit>, error: Error? = null, cancel: Cancel? = null): Job {
        return viewModelScope.launch {
            try {
                block.invoke()
            } catch (e: Exception) {
                when (e) {
                    is CancellationException -> {
                        cancel?.invoke(e)
                    }
                    else -> {
                        handleError(e)
                        error?.invoke(e)
                    }
                }
            }
        }
    }

    /*
    * 启动协程 
    */
    protected fun <T> async(block: Block<T>): Deferred<T> {
        return viewModelScope.async { block.invoke() }
    }

    /**
     * 处理异常
     */
    private fun handleError(e: Exception) {
        Toast.makeText(App.instance, e.message, Toast.LENGTH_SHORT).show()
    }
}

typealias:类型别名,相关用法可以参考:https://kotlinlang.org/docs/reference/type-aliases.html

创建两个方法 launch 和 async 方法启动协程,launch 方法中采用 viewModelScope.launch ,async 方法中 viewModelScope.async。

采用 viewModelScope ,当 ViewModel 被销毁时候,它会自动取消协程任务。

启动协程的方法是 async {} 和 launch {}。async {} 会返回一个 Deferred<T> 实例,它拥有一个 await() 函数来返回协程执行的结果。launch {} 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值。

3.2 View

创建抽象类 BaseVmDbActivity:

package cn.zzw.mvvmdemo.base

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import cn.zzw.mvvmdemo.mvvm.base.viewmodel.BaseViewModel

abstract class BaseVmDbActivity<VM : BaseViewModel, DB : ViewDataBinding> : AppCompatActivity() {
    lateinit var viewModel: VM
    lateinit var dataBinding: DB
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initViewDataBinding() //初始化 DataBinding
        initViewModel()//初始化 ViewModel
        initView(savedInstanceState)//初始化控件
        initData()//初始化数据
    }


    abstract fun getLayoutId(): Int //获取布局文件
    abstract fun viewModelClass(): Class<VM>//获取 ViewModel 类
    abstract fun initView(savedInstanceState: Bundle?)
    abstract fun initData()

    private fun initViewDataBinding() {
        dataBinding = DataBindingUtil.setContentView(this, getLayoutId())
        dataBinding.lifecycleOwner = this
    }

    private fun initViewModel() {
        viewModel = ViewModelProvider(this).get(viewModelClass())
    }
}

在 BaseVmDbActivity 中,根据泛型传入的参数类型,创建对应的 ViewModel 和 ViewDataBinding 对象。具体页面的 Activity 只需要继承 BaseVmDbActivity,并且传入具体的 ViewModel 类就就可以了,后面将展示具体的 Activity。

3.3 Model

Model 表示从 SQLite 或者 webService 中获取的数据来源,并转换为相应的实体类。

Android kotlin 如何动态改变recycleview gridlayoutmanager的行数 android kotlin mvvm_android_02

创建 Repository 类 HomeRepository 用于获取网络数据:

class HomeRepository {
    suspend fun getArticleList(page: Int) = RetrofitClient.service.getHomeArticleList(page).responsData()
}

RetrofitClient 类:初始化 Okhttp 和 Retrofit 以及 RetrofitService 。

object  RetrofitClient {
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .build()
    private val retrofit = Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(RetrofitService.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    val service: RetrofitService = retrofit.create(RetrofitService::class.java)
}

RetrofitService 类:

interface RetrofitService {
    companion object {
        const val BASE_URL = "https://www.wanandroid.com"
    }

    //    article/list/0/json
    @GET("/article/list/{page}/json")
    suspend fun getHomeArticleList(@Path("page") page: Int): BasicResponse<PageInfo<Article>>

}

玩安卓网站上提供的接口返回数据结构定义为:

{
    "data": ...,
    "errorCode": 0,
    "errorMsg": ""
}

根据该返回数据结构,定义 BasicResponse 类:

/*
{
    "data": ...,
    "errorCode": 0,
    "errorMsg": ""
}
所有的返回结构均为上述
errorCode如果为负数则认为错误此时errorMsg会包含错误信息。
data为Object,返回数据根据不同的接口而变化。
errorCode = 0 代表执行成功,不建议依赖任何非0的 errorCode.
errorCode = -1001 代表登录失效,需要重新登录。
*/
data class BasicResponse<T>(
    val data: T, val errorCode: Int, val errorMsg: String
) {
    fun responsData(): T {
        if (errorCode == 0) {
            return data
        } else {
            throw ResponseException(errorCode, errorMsg)
        }
    }
}

只有 errorCode ==0 才是正确的访问请求,其他的则抛出异常进行处理,这里封装了异常类 ResponseException,参数为 errorCode, errorMsg:

class ResponseException(var code: Int, override var message: String) : RuntimeException()

所以在 BaseViewModel 类中,才会采取 try-catch 的方式,当 errorCode 不为0,则进行异常处理:

protected fun launch(block: Block<Unit>, error: Error? = null, cancel: Cancel? = null): Job {
        return viewModelScope.launch {
            try {
                block.invoke()
            } catch (e: Exception) {
                when (e) {
                    is CancellationException -> {
                        cancel?.invoke(e)
                    }
                    else -> {
                        handleError(e)
                        error?.invoke(e)
                    }
                }
            }
        }
    }

BasicResponse 类中的 data 是泛型,根据传入的参数类型,实现对应的实体类。在这里根据相应的接口创建实体类 PageInfo 和 Article 类:

/*
每页的信息
*/
data class PageInfo<T>(
    val offset: Int,
    val size: Int,
    val total: Int,
    val pageCount: Int,
    val curPage: Int,
    val over: Boolean,
    val datas: List<T>
)
package cn.zzw.mvvmdemo.bean

data class Article(
    val apkLink: String,
    val audit: Int,
    val author: String,
    val canEdit: Boolean,
    val chapterId: Int,
    val chapterName: String,
    val collect: Boolean,
    val courseId: Int,
    val desc: String,
    val descMd: String,
    val envelopePic: String,
    val fresh: Boolean,
    val id: Int,
    val link: String,
    val niceDate: String,
    val niceShareDate: String,
    val origin: String,
    val prefix: String,
    val projectLink: String,
    val publishTime: Long,
    val realSuperChapterId: Int,
    val selfVisible: Int,
    val shareDate: Long,
    val shareUser: String,
    val superChapterId: Int,
    val superChapterName: String,
    val tags: List<Any>,
    val title: String,
    val type: Int,
    val userId: Int,
    val visible: Int,
    val zan: Int
)

3.4 BaseViewModel 的实现类 HomeViewModel

class HomeViewModel() : BaseViewModel() {
    private var curPage = 0
    private val repository by lazy { HomeRepository() }
    val articleList: MutableLiveData<MutableList<Article>> = MutableLiveData()
    val statusIsRefreshing = MutableLiveData<Boolean>()
    fun refreshArticleList() {
        statusIsRefreshing.value = true
        launch(block = {
            val articleListDefferd = async { repository.getArticleList(0) }

            val pageInfo = articleListDefferd.await()
            curPage = pageInfo.curPage
            articleList.value = mutableListOf<Article>().apply {
                addAll(pageInfo.datas)
            }
            statusIsRefreshing.value = false
        },
            error = {
                statusIsRefreshing.value = false
            })

    }
}

ViewModel 和 View 的数据通信采用 LiveData,关于 LiveData 可以参考我写的 Android Jetpack 之 LiveData,此篇文章是19年写的,所以还是采用的 Java。继续回到 HomeViewModel 类:

采用延迟加载的方式创建 HomeRepository,创建了 articleList 为页面的 RecyclerView 提供数据,statusIsRefreshing 用于记录加载状态。

调用 BaseViewModel 中的 asyn 方法,asyn方法采用的是 asyn{} 的方式启动协程,会返回 Defferd 对象,并调用 await() 方法获取到 PageInfo 对象。

3.5 具体页面 HomeActivity

...
import kotlinx.android.synthetic.main.activity_main.*

class HomeActivity : BaseVmDbActivity<HomeViewModel, ViewDataBinding>() {
    lateinit var homeAdapter: HomeAdapter
    lateinit var layoutManager: LinearLayoutManager
    override fun getLayoutId(): Int = R.layout.activity_main
    override fun viewModelClass() = HomeViewModel::class.java
    private var isFirst: Boolean = true
    override fun initView(savedInstanceState: Bundle?) {
        swipeRefreshLayout.run {
            setOnRefreshListener { viewModel.refreshArticleList() }
        }
        layoutManager = LinearLayoutManager(this@HomeActivity)
        layoutManager.orientation = RecyclerView.VERTICAL
    }

    override fun onResume() {
        super.onResume()
        if (isFirst) {
            viewModel.refreshArticleList()
            isFirst = false
        }
    }

    override fun initData() {
        viewModel.run {
            statusIsRefreshing.observe(this@HomeActivity, Observer {
                swipeRefreshLayout.isRefreshing = it
            })
            articleList.observe(this@HomeActivity, Observer {
                homeAdapter = HomeAdapter(it)
                recyclerView.adapter = homeAdapter
                recyclerView.layoutManager = layoutManager
            })
        }
    }
}

HomeActivity 类继承了 BaseVmDbActivity,并重写父类的方法 getLayoutId() 和 viewModelClass(),初始化了布局和 viewmodel。在 onResume 方法中,判断是加载过,如果没有加载过则调用了 viewModel 的 refreshArticleList() 方法进行加载数据。

在 intiData 方法中,调用了 LiveData 的 observe 方法用于监听数据的变化,根据数据的变化,创建 RecyclerView 的适配器 HomeAdapter。这里调用了 Kotlin 的内联函数 run,关于 run 的介绍可以参考此篇文章:Kotlin系列之let、with、run、apply、also函数的使用

看 HomeAdapter 之前先看下 RecyclerView Item 的布局文件:

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

    <data class="HomeItemBinding">

        <variable
            name="article"
            type="cn.zzw.mvvmdemo.bean.Article" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="30dp"
            android:text="@{article.title}"
            android:textSize="14dp" />
    </RelativeLayout>
</layout>

这里用了 DataBinding,对于 DataBinding 的使用可以参考下我之前写的文章:Android Jetpack 之 DataBinding,也是19年用 Java 写的。这里根据此 xml 生成对应 DataBinding 类 HomeItemBinding。

继续看 HomeAdapter 类:

class HomeAdapter(var list: MutableList<Article>) :
    RecyclerView.Adapter<HomeAdapter.ItemViewHolder>() {
    inner class ItemViewHolder(var dataBinding: HomeItemBinding) :
        RecyclerView.ViewHolder(dataBinding.root) {
        fun bind(item: Article) {
            dataBinding.article = item
            dataBinding.executePendingBindings()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view: HomeItemBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.home_item,
            parent,
            false
        )
        return ItemViewHolder(view)
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(list[position])
    }
}

在 onCreateViewHolder 中初始化 HomeItemBinding,并以参数的形式传入 ViewHolder 中。HomeAdapter 跟之前的一贯写法相比少去了 findViewById,以及对各个控件进行赋值的语句,整体看起来简洁了很多。

4. 总结

MVVM 是 Model-View-ViewModel 的简写。在MVVM中,ViewModel 不能持有 View 的引用,否则又是 MVP了。数据和业务逻辑处于独立的 View Model 中,ViewModel 只要关注数据和业务逻辑,不需要和UI或者控件打交道。View 和 ViewModel 之间的数据通过 LiveData 进行传递。

MVP 中  Presenter从View中获取需要的参数,交给Model去执行业务方法,执行的过程中需要的反馈,以及结果,再让View进行做对应的显示。

MVC 中是允许 Model 和 View 进行交互的,而 MVP 中很明显,Model 与View 之间的交互由 Presenter 完成。还有一点就是Presenter 与 View 之间的交互是通过接口的。

 

Android kotlin 如何动态改变recycleview gridlayoutmanager的行数 android kotlin mvvm_android_03