写一个MVVM快速开发框架(一)基础类封装

Posted by 卢小胖 on 2021-07-28
Estimated Reading Time 9 Minutes
Words 2.1k In Total
Viewed Times

前言

最近想要将老项目用MVVM模式去重构,原来的App采用MVP+MVVM的混合模式,老项目嘛大家都懂,最开始用MVP,后来慢慢改成MVVM,但是又没完全重构,所以整个项目看起来乱糟糟的,每次新加功能的时候写的那叫一个难受。

工欲善其事必先利其器

用MVVM+Jetpack组件的优点就不用我说了,写过的人肯定都说爽,此次就是想要重新整理下一些基础开发工具,封装一个自己用的顺手的MVVM模式快速开发框架。
一是平常用来写测试,二是以便在需要的时候快速投入使用。

明确思路

写之前需要明确好自己的思路,自己需要用的东西,是否熟悉,Jetpcak有很多组件,我们平常开发中不太可能全部用到,有需要的时候再加上也不迟。比如:

  • App架构是采用单activity+多fragment架构,还是采用传统的多activty模式。
  • 比如是否需要组件化开发,如果项目不大或者是单人开发,可以不需要组件化。
  • 比如选择databinding还是viewbidning
  • 比如选择LiveData or Flow or Rx

动手之前先想好你需要什么,有些可能不太成熟的框架可以尝鲜,但是投入使用需要谨慎考虑,不然后期的维护可能体验到什么是一把辛酸泪…

步入正题:基础Activity封装

最基础的BaseActivity():

abstract class BaseActivity(@LayoutRes private val layout: Int ?= null) : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
layout?.let {
setContentView(it)
}
initData(savedInstanceState)
}


abstract fun initData(savedInstanceState: Bundle?=null)

}

我这里选择直接传入layout id进行布局绑定,尽量简洁明了

对DataBindingBaseActivity封装:

abstract class DataBindingBaseActivity<T : ViewDataBinding>(@LayoutRes private val layout: Int) : BaseActivity() {

private var _mBinding: T ?= null
val mBinding get() = _mBinding!!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_mBinding = DataBindingUtil.setContentView(this, layout)
}

override fun onDestroy() {
super.onDestroy()
_mBinding?.unbind()
}


}

DataBindingBaseActivity 继承 BaseActivity,开放mBinding给子类使用。

使用ViewBinding

并不是所有人都喜欢DataBinding,但是一定所有人都喜欢ViewBinding(我猜的哈哈哈),平常我用到viewBinding比较多,dataBinding只会在少数几个页面用到,之前一直是直接创建binding,导致模板代码非常多。

先列出参考博客:

  1. https://juejin.cn/post/6960914424865488932
  2. https://github.com/hi-dhl/Binding
  3. https://github.com/kirich1409/ViewBindingPropertyDelegate

之前使用findViewById(),后来使用butterKnife,在后来使用kotlin直接使用控件id,说实话我挺讨厌直接使用控件id的,所幸这种方式在之后也会被抛弃。

怎样使用viewbinding在这里就不做介绍了,现在有大佬发现了使用反射或委托去创建viewBinding,请参考链接一,也有现成的库去使用,参考链接二 链接三其覆盖了activity,fragment,dialog,adapter等多种使用场景。

因为viewbinding的创建已经非常简单了,所以我不再将其封装在BaseActivity中,最终activity使用如下:

class MainActivity : BaseActivity(R.layout.activity_main){


private val binding by viewBinding(ActivityMainBinding::bind)

private val viewmodel by viewModels<CommonViewModel>()


override fun initData(savedInstanceState: Bundle?) {

}

}

此处viewmode创建方法见:官网

基础Fragment封装

abstract class BaseFragment(@LayoutRes private val layout: Int, private val lazyInit:Boolean = false) : Fragment(layout) {

val TAG by lazy {
this.javaClass.name;
}

private var isLoaded = false
lateinit var mContext: Context

override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!lazyInit){
initData()
}
}


override fun onResume() {
super.onResume()
//Fragment是否可见
if (!isLoaded && !isHidden && lazyInit) {
initData()
isLoaded = true
}
}


/**
* 初始化数据
*/
abstract fun initData()


/**
* fragment跳转,防止重复点击崩溃
*/
fun navigate(destination: Int, bundle: Bundle ?= null) = NavHostFragment.findNavController(this).apply {
currentDestination?.getAction(destination)?.let {
navigate(destination,bundle)
}
}

override fun onDestroyView() {
super.onDestroyView()
isLoaded = false
}

}

我个人倾向使用单activity+多fragment架构,使用Navigation作为导航,基本能应对大部分场景。navigate()方法是为了防止重复点击崩溃。lazyInit参数代表是否延迟加载

DataBindingBaseFragment

abstract class DataBindingBaseFragment<T : ViewDataBinding>(@LayoutRes private val layout: Int,lazyInit:Boolean = false) : BaseFragment(layout = layout,lazyInit = lazyInit) {

private var _mBinding: T? = null
val mBinding get() = _mBinding!!


override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_mBinding = DataBindingUtil.inflate(inflater, layout, container, false)
return mBinding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//LiveData needs the lifecycle owner
mBinding.lifecycleOwner = requireActivity()
}

override fun onDestroyView() {
super.onDestroyView()
_mBinding?.unbind()
}

}

mBinding.lifecycleOwner = requireActivity() 是databinding配合LiveData使用时需要的

使用:

class DataBindingFragment : DataBindingBaseFragment<FragmentDatabindBinding>(R.layout.fragment_databind) {

private val viewmodel by activityViewModels<DataBindViewModel>()

override fun initData() {
//不要忘了赋值
mBinding.viewmodel = viewmodel
}

}

网络框架封装

我的想法是使用 协程 + ViewModel + Repository去做网络请求,返回结果使用LiveData保存在ViewModel

  • Reporisitory的职责就是数据处理,包括本地数据和网络数据
  • ViewModel的职责就是连接UI和数据
  • LiveData可以进行数据的观察

我们接口返回数据的基础类:


class ApiResponse<T> : Serializable {

var code : Int = 0

var data : T ?= null

var message : Any ?= null

var state : NetState = NetState.STATE_UNSTART

var error : String = ""

/**
* 如果服务端data肯定不为null,直接将data返回。
* 假如data为null证明服务端出错,这种错误已经产生并且不可逆,反射生成空对象
* 客户端只需保证不闪退并给予提示即可
*/
fun data(): T? {
when (code) {
//请求成功
0, 200 -> {
if (null==data){
data = Any::class.java.newInstance() as T
}
return data
}
}
throw ApiException(message as String, code)
}

}

一般接口返回的就是code data message,如果有其他的可以自行更改,其中state是状态类,如下:

enum class NetState {
STATE_UNSTART,//未知
STATE_LOADING,//加载中
STATE_SUCCESS,//成功
STATE_EMPTY,//数据为null
STATE_FAILED,//接口请求成功但是服务器返回error
STATE_ERROR//请求失败
}

ApiException是自定义的异常抛出类:

class ApiException(val errorMessage: String, val errorCode: Int) : Throwable()

定义接口:

interface Api {

@GET("banner/json")
fun loadProjectTree(): ApiResponse<List<BannerData>>

}

创建RetrofitFactory:


object RetrofitFactory {

private val okHttpClientBuilder: OkHttpClient.Builder by lazy {
OkHttpClient.Builder()
.readTimeout(
10000,
TimeUnit.MILLISECONDS
)
.connectTimeout(
10000,
TimeUnit.MILLISECONDS
)
}


fun factory(): Retrofit {
val okHttpClient = okHttpClientBuilder.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://www.wanandroid.com/")
.build()
return retrofit
}


}

创建Retrofit管理类:

object RetrofitManager {

/**
* 用于存储ApiService
*/
private val map = mutableMapOf<Class<*>, Any>()

private val retrofit by lazy {
RetrofitFactory.factory()
}

//动态指定域名
fun <T : Any> getApiService(apiClass: Class<T>): T {
return getService(apiClass)
}

/**
* 获取ApiService单例对象
*/
private fun <T : Any> getService(apiClass: Class<T>): T{
return if (map[apiClass] == null) {
val t = retrofit.create(apiClass)
if (map[apiClass] == null) {
map[apiClass] = t
}
t
} else {
map[apiClass] as T
}
}
}

在Repo中的调用:

class CommonRepo() {

private var mService: Api = RetrofitManager.getApiService(Api::class.java)

fun laod() = mService.loadProjectTree()

}

我们可以使用将网络请求封装在BaseRepo中:

  1. 修改API接口中的方法,使用suspend修饰
interface Api {

@GET("banner/json")
suspend fun loadProjectTree(): ApiResponse<List<BannerData>>

}

2.BaseRepo中需传入协程,在viewmodel中创建repo的时候最好使用viewModelScope

open class BaseRepository(private val coroutineScope: CoroutineScope) {


protected fun <T> launch(
block : suspend () -> ApiResponse<T>,
success: suspend (ApiResponse<T>) -> Unit
){
coroutineScope.launch(Dispatchers.IO){
var baseResp = ApiResponse<T>()
try {
baseResp.state = NetState.STATE_LOADING
//开始请求数据
val invoke = block.invoke()
//将结果复制给baseResp
baseResp = invoke
when(baseResp.code){
0,200 -> {
//请求成功,判断数据是否为空
if (baseResp.data == null || baseResp.data is List<*> && (baseResp.data as List<*>).size == 0) {
//TODO: 数据为空,结构变化时需要修改判空条件
baseResp.state = NetState.STATE_EMPTY
} else {
//请求成功并且数据为空的情况下,为STATE_SUCCESS
baseResp.state = NetState.STATE_SUCCESS
}
}
400,401 -> {
baseResp.state = NetState.STATE_FAILED
}
}
}catch (e:Exception){
baseResp.state = NetState.STATE_ERROR
e.printStackTrace()
}finally {
success(baseResp)
}
}
}

}
  • 通过NetState设置请求状态
  • block代表一个返回ApiResponse的suspend方法
  • success代表返回一个ApiResponse类型的对象

Repo中调用调用

fun laod(resultLiveData: MutableLiveData<ApiResponse<List<BannerData>>>){
launch(
block = {
mService.loadProjectTree()
},
success = {
resultLiveData.postValue(it)
}
)
}

如果你觉得MutableLiveData<ApiResponse>看起来头疼,可以再进行一次封装:

class ResultLiveData<T> : MutableLiveData<ApiResponse<T>>() {
}

最终网络请求完整流程:

viewModel:

class CommonViewModel : ViewModel() {

private val repo by lazy { CommonRepo(viewModelScope) }

val liveData = ResultLiveData<List<BannerData>>()
fun load(){
repo.laod(liveData)
}


}

repo:

class CommonRepo(scope: CoroutineScope) : BaseRepository(scope) {

private var mService: Api = RetrofitManager.getApiService(Api::class.java)

fun laod(resultLiveData: ResultLiveData<List<BannerData>>){
launch(
block = {
mService.loadProjectTree()
},
success = {
resultLiveData.postValue(it)
}
)
}

}

UI:

commonViewModel.load()

commonViewModel.liveData.observe(viewLifecycleOwner, Observer {
//do something
})

最后附上[Demo地址