最近看到很多公司的招聘要求上写着“熟悉 MVVM + DataBinding + Kotlin 开发模式”,看来这种模式现在已经成了主流了。之前也零散的了解过 MVVM 和 DataBinding,但是也把它们串联在一起过,更何况加上不太熟悉的 Kotlin。对于 Kotlin ,从 2020 年开始写的博客强制要求自己采用 Kotlin,但是还处于零零散散的知识点。通过这篇文章来增加自己对 Kotlin 的熟悉。
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 的改变又同时自动反馈到数据,数据成为主导因素,这样使得在业务逻辑处理只要关心数据,方便而且简单很多。
这里采用 wanAndroid 中的一个接口,获取数据并显示在页面中:
创建 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 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 = 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, 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 async(block: Block): Deferred {
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
创建抽象类 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 : 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//获取 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。
Model 表示从 SQLite 或者 webService 中获取的数据来源,并转换为相应的实体类。
创建 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>
}
玩安卓网站上提供的接口返回数据结构定义为:
{
"data": ...,
"errorCode": 0,
"errorMsg": ""
}
根据该返回数据结构,定义 BasicResponse 类:
/*
{
"data": ...,
"errorCode": 0,
"errorMsg": ""
}
所有的返回结构均为上述
errorCode如果为负数则认为错误此时errorMsg会包含错误信息。
data为Object,返回数据根据不同的接口而变化。
errorCode = 0 代表执行成功,不建议依赖任何非0的 errorCode.
errorCode = -1001 代表登录失效,需要重新登录。
*/
data class BasicResponse(
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, 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(
val offset: Int,
val size: Int,
val total: Int,
val pageCount: Int,
val curPage: Int,
val over: Boolean,
val datas: List
)
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,
val title: String,
val type: Int,
val userId: Int,
val visible: Int,
val zan: Int
)
class HomeViewModel() : BaseViewModel() {
private var curPage = 0
private val repository by lazy { HomeRepository() }
val articleList: MutableLiveData> = MutableLiveData()
val statusIsRefreshing = MutableLiveData()
fun refreshArticleList() {
statusIsRefreshing.value = true
launch(block = {
val articleListDefferd = async { repository.getArticleList(0) }
val pageInfo = articleListDefferd.await()
curPage = pageInfo.curPage
articleList.value = mutableListOf().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 对象。
...
import kotlinx.android.synthetic.main.activity_main.*
class HomeActivity : BaseVmDbActivity() {
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 的布局文件:
这里用了 DataBinding,对于 DataBinding 的使用可以参考下我之前写的文章:Android Jetpack 之 DataBinding,也是19年用 Java 写的。这里根据此 xml 生成对应 DataBinding 类 HomeItemBinding。
继续看 HomeAdapter 类:
class HomeAdapter(var list: MutableList) :
RecyclerView.Adapter() {
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,以及对各个控件进行赋值的语句,整体看起来简洁了很多。
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 之间的交互是通过接口的。