Hello 啊各位老铁,明天带来一个陈词滥调的技术,MVVM,这篇文章,次要具体介绍如何封装一个 MVVM 的基类库,以及 MVVM 架构模式在理论业务中的用法,最初会把理论的封装代码开源,并提供近程依赖,不便给到大家应用以及二次批改,尽量做到细致入微,浅显易懂,OK,废话不多赘述,咱们进入注释。
这篇文章大略会依照以下几个模块进行论述,此次封装,做到绝无第三方依赖,都是 Android 原生的代码封装,请放心使用,如果您想间接进行应用,请间接跳到第 4 步,集成应用即可,此次的封装,和目前支流的 MVVM 架构模式,会完满符合,让架构模式简单化,让业务代码清晰化,必须值得举荐应用。
一、MVVM 简略概括
二、基于 MVVM 模式如何封装基类库
三、实战封装
四、封装后在业务中如何应用
五、开源以及 Demo 查看
舒适提醒:内容稍多,请合理安排好工夫,如果不想查阅具体封装过程,底部有开源地址,能够间接查看。
一、MVVM 简略概括
MVVM 的开发模式,相对来说低耦合,业务之间逻辑显得也非常明显,Model 层负责将申请的数据交给 ViewModel 层;ViewModel 层负责将申请到的数据做业务逻辑解决,最初交给 View 层去展现,与 View 一一对应;View 层只负责界面绘制刷新,不解决业务逻辑,非常适合进行独立模块开发。
三层简略概括
1、Model:数据层,蕴含数据实体和对数据实体的操作。
2、View:视图层,对应于 Activity,XML,View,负责数据显示以及用户交互。
3、ViewModel:关联层,将 Model 和 View 进行绑定,Model 或者 View 更改时,实时刷新对方。
须要留神:
1、View 只做和 UI 相干的工作,不波及任何业务逻辑,不波及操作数据,不解决数据,也就是 UI 和数据是严格离开的。
2、ViewModel 只做和业务逻辑相干的工作,不波及任何和 UI 相干的操作,不持有控件援用,不更新 UI。
二、基于 MVVM 模式如何封装基类库
MVVM 咱们曾经清晰,然而针对现有的三层,咱们如何进行拆解封装呢?面对这样的一个问题,咱们也是须要从三层以及和理论的业务进行相结合,从理论业务中来,也要从理论业务中去,这是咱们封装的一个潜在因素,一旦脱离了理论,封装的再优良,也只是一个花瓶,中看不中用。
针对 MVVM 中的三层,其实,咱们在封装中,也是基于这三层,View,ViewModel 和 Model。View 中,在理论的开发中,个别针对 Activity 和 Fragment 进行零碎的抽取封装,ViewModel 个别会抽取一个父类,做一些公共的办法或属性配置,Model 层个别封装的较少,依据理论业务,须要具体问题具体分析。
Activity 和 Fragment 的封装思路,其实是统一的,须要以简略和简单两种方向进行抽取,一种是简略的页面继承应用,一种是简单的页面继承应用,这样辨别的一个目标,就是,专职专用,防止大材小用,而具体的封装,除了使得代码简洁化,更重要的拓展化,不便子类的调用。
在具体封装的时候,与理论业务相结合,这个无比重要,比方理论的大部分页面,都带有一个标题栏,那么标题栏就能够间接封装父类外面,像子类拓展出,更改题目,右侧按钮,左侧按钮等功能属性;除了对立的标题栏,另外就是子类的视图了,对于子类的视图传递,这个是必须的,能够间接形象出一个必须要实现的办法,其余的,比方状态栏的扭转,缺省页的设置等等,也须要在父类中对立的给出。
简单的页面是基于简略的页面而来的,这里的简单,个别是蕴含很多逻辑的解决,那么,咱们就能够减少 ViewModel 层和 Model 层了,目前基于 DataBinding 的实现形式,无论简略和简单,都是必须须要思考的,也就是说在父类中,咱们就须要向子类提供出能够拿到的 databinding 和 viewmodel,个别以泛型的形式引入,这样子类再继承的时候,就能够很不便的进行调用。
在简单的页面,也就是蕴含 ViewModel 层和 Model 层的时候,须要思考绑定视图 variable 的传递,也就是以后的 ViewModel 和那个 xml 进行绑定,当然这是在须要的时候,必须要操作的,除了视图绑定,常见的,数据申请状态,比方申请胜利,申请失败,缺省页显示和暗藏,Dialog 的显示和暗藏,LiveData 的数据回传等等,在简单的页面中也是须要咱们思考的,除此之外,ViewModel 中如何和 View 层的生命周期绑定,在理论的业务中也是不得不须要思考的。
除了以上的惯例思考,在理论的业务中,比方事件消息传递,PagerAdapter 应用,状态栏通明等很多和基类的相干的性能,咱们其实也能够进行封装进去,便于子类的调用。
三、实战封装
通过第 2 条中的拆解和具体的封装思路,无妨咱们进行实战一下,因为 Activity 和 Fragment 的封装思路以及相干属性和办法,大部分都是雷同的,所以目前只介绍 Activity,更具体的封装,还请大家参考源码。
1、Activity 的简略封装
简略封装,不携带 ViewModel,只传递 ViewDataBinding,子类必须重写的办法只有一个 initData,其余均为选择性重写,如果绝对逻辑比较简单的页面,能够继承此类。
一个很简略的一般封装,就是把共有的常见的,封装到父类里,便于子类的调用,具体什么办法,什么逻辑进行采取封装,须要咱们依据具体业务或者公司的相干状况而定,以下是源码。
abstract class BaseActivity<VB : ViewDataBinding>(@LayoutRes layoutId: Int = 0) :
AppCompatActivity(layoutId) {
private var mActionBarView: ActionBarView? = null
private var mLayoutError: LinearLayout? = null
private var mLayoutId = layoutId
lateinit var mBinding: VB
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
try {
// 默认状态栏为白底黑字
darkMode(BaseConfig.statusBarDarkMode)
statusBarColor(ContextCompat.getColor(this, BaseConfig.statusBarColor))
setContentView(R.layout.activity_base)
val baseChild = findViewById<LinearLayout>(R.id.layout_base_child)
mLayoutError = findViewById(R.id.layout_empty_or_error)
mActionBarView = findViewById(R.id.action_bar)
if (mLayoutId == 0) {mLayoutId = getLayoutId()
}
if (savedInstanceState != null && getIntercept()) {noEmptyBundle()
return
}
val childView = layoutInflater.inflate(mLayoutId, null)
baseChild.addView(childView)
mBinding = DataBindingUtil.bind(childView)!!
initView()
initData()} catch (e: Exception) {e.printStackTrace()
noEmptyBundle()}
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取视图 id
*/
open fun getLayoutId(): Int {return 0}
open fun initView() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 初始化数据
*/
abstract fun initData()
/**
* AUTHOR:AbnerMing
* INTRODUCE: 动静扭转状态栏色彩和题目
*/
fun setDarkTitle(dark: Boolean, color: Int, title: String) {
try {darkMode(dark)
statusBarColor(ContextCompat.getColor(this, color))
setBarTitle(title)
} catch (e: Exception) {e.printStackTrace()
}
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 设置题目
*/
fun setBarTitle(title: String) {
mActionBarView!!.visibility = View.VISIBLE
mActionBarView!!.setBarTitle(title)
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 暗藏左侧按钮
*/
fun hintLeftMenu() {mActionBarView!!.hintLeftBack()
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取 ActionBarView
*/
fun getActionBarView(): ActionBarView {return mActionBarView!!}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 暗藏标题栏
*/
fun hintActionBar() {mActionBarView?.visibility = View.GONE}
/**
* AUTHOR:AbnerMing
* INTRODUCE:Bundle 为空进行拦挡,解决扭转权限后重回 App 解体问题
*/
open fun getIntercept(): Boolean {return false}
/**
* AUTHOR:AbnerMing
* INTRODUCE:Bundle 为空时的逻辑解决,解决扭转权限后重回 App 解体问题
*/
open fun noEmptyBundle() {}
override fun onDestroy() {super.onDestroy()
try {LiveDataBus.removeObserve(this)
LiveDataBus.removeStickyObserver(this)
} catch (e: Exception) {e.printStackTrace()
}
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 通明状态栏
*/
fun translucentWindow(dark: Boolean) {
try {immersive(0, dark)
} catch (e: Exception) {e.printStackTrace()
}
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 设置缺省页
*/
fun setEmptyOrError(view: View) {
mLayoutError?.visibility = View.VISIBLE
mLayoutError?.removeAllViews()
mLayoutError?.addView(view)
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 暗藏
*/
fun hintEmptyOrErrorView() {mLayoutError?.visibility = View.GONE}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取谬误或为空的 view
*/
fun getEmptyOrErrorView(): LinearLayout {return mLayoutError!!}
}
波及的办法概述
办法名 | 参数 | 概述 |
---|---|---|
getLayoutId | 无参 | 子类传递的 layout,用于加载视图,能够通过构造方法传递,也能够通过此办法传递。 |
initView | 无参 | 初始化 View,非必须重写 |
initData | 无参 | 初始化数据 |
setDarkTitle | dark: Boolean, color: Int, title: String,1、dark: Boolean,状态栏色彩,true 就是彩色,false 就是红色。2、color: Int,状态栏背景色彩,3、title: String,标题栏内容 | 设置题目,状态栏背景及色彩 |
setBarTitle | title: String,标题栏内容 | 设置题目 |
hintLeftMenu | 无参 | 暗藏左侧按钮 |
getActionBarView | 无参 | 获取标题栏 View,能够操作标题栏里的任何控件 |
hintActionBar | 无参 | 暗藏标题栏 |
translucentWindow | dark: Boolean,状态栏色彩,true 就是彩色,false 就是红色 | 通明状态栏 |
setEmptyOrError | view: View,传递的缺省 View 视图 | 设置缺省视图 |
hintEmptyOrErrorView | 无参 | 暗藏缺省视图 |
getEmptyOrErrorView | 无参 | 获取缺省视图 |
简略的 Activity 没有什么好说的,都是中规中矩,具体的应用请大家看第四条,具体应用即可。
2、Activity 的简单封装
也谈不上简单,只是在继承简略页面的根底之上多加了一个 ViewModel,绝对于比较复杂的页面,就能够继承此类,此类,拓展了 ViewModel,能够在 ViewModel 里进行逻辑的书写,此类也是 MVVM 的规范执行,V 继承于 BaseVMActivity,VM 继承于 BaseViewModel,至于 M,能够在 VM 中通过 getRepository 办法进行获取。
具体代码逻辑如下:
BaseVMActivity 继承于 BaseActivity。
abstract class BaseVMActivity<VB : ViewDataBinding, BM : BaseViewModel>(@LayoutRes layoutId: Int = 0) :
BaseActivity<VB>(layoutId) {
lateinit var mViewModel: BM
override fun initData() {mViewModel = getViewModel()!!
val variableId = getVariableId()
if (variableId != -1) {mBinding.setVariable(getVariableId(), mViewModel)
mBinding.executePendingBindings()}
initVMData()
observeLiveData()
initState()
lifecycle.addObserver(mViewModel)
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取绑定的 xml id
*/
open fun getVariableId(): Int {return -1}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 初始化状态
*/
private fun initState() {
mViewModel.mStateViewLiveData.observe(this, {when (it) {
StateLayoutEnum.DIALOG_LOADING -> {dialogLoading()
}
StateLayoutEnum.DIALOGD_DISMISS -> {dialogDismiss()
}
StateLayoutEnum.DATA_ERROR -> {dataError()
}
StateLayoutEnum.DATA_NULL -> {dataEmpty()
}
StateLayoutEnum.NET_ERROR -> {netError()
}
StateLayoutEnum.HIDE -> {hide()
}
}
})
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 初始化数据
*/
abstract fun initVMData()
/**
* AUTHOR:AbnerMing
* INTRODUCE:LiveData 的 Observer
*/
open fun observeLiveData() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE:dialog 加载
*/
open fun dialogLoading() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE:dialog 暗藏
*/
open fun dialogDismiss() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 数据谬误
*/
open fun dataError() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 数据为空
*/
open fun dataEmpty() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 网络谬误或申请谬误
*/
open fun netError() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 暗藏某些布局或者缺省页等
*/
open fun hide() {}
private fun getViewModel(): BM? {
// 这里取得到的是类的泛型的类型
val type = javaClass.genericSuperclass
if (type != null && type is ParameterizedType) {
val actualTypeArguments = type.actualTypeArguments
val tClass = actualTypeArguments[1]
return ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
)
.get(tClass as Class<BM>)
}
return null
}
override fun onDestroy() {super.onDestroy()
try {lifecycle.removeObserver(mViewModel)
} catch (e: Exception) {e.printStackTrace()
}
}
}
封装波及的办法概述
办法名 | 参数 | 概述 |
---|---|---|
getVariableId | 无参 | 获取绑定的 xml variable,也就是以后的 xml 和哪个对象进行绑定,用于 xml 里间接数据绑定 |
initVMData | 无参 | 初始化数据,必须要实现的办法 |
observeLiveData | 无参 | LiveData 的 Observer,UI 层监听 ViewModel 层的数据扭转 |
dialogLoading | 无参 | dialog 加载 |
dialogDismiss | 无参 | dialog 暗藏 |
dataError | 无参 | 数据谬误 |
dataEmpty | 无参 | 数据为空 |
netError | 无参 | 数据谬误 |
hide | 无参 | 暗藏缺省页等其余页面 |
BaseViewModel
BaseViewModel 绝对比较简单,只提供了一个能够获取 Repository 的办法,还有一个是刷新 UI 视图的一个 LiveData,就是数据申请,Dialog 加载,缺省页加载的状态。更改状态,只须要调用 changeStateView 办法即可,子类能够重写生命周期办法,便于生命周期的思考。
open class BaseViewModel : ViewModel() , BaseObserver{
/**
* 管制状态视图的 LiveData
*/
val mStateViewLiveData = MutableLiveData<StateLayoutEnum>()
/**
* 更改状态视图的状态
*/
public fun changeStateView(state: StateLayoutEnum) {
// 对参数进行校验
when (state) {
StateLayoutEnum.DIALOG_LOADING -> {mStateViewLiveData.postValue(StateLayoutEnum.DIALOG_LOADING)
}
StateLayoutEnum.DIALOGD_DISMISS -> {mStateViewLiveData.postValue(StateLayoutEnum.DIALOGD_DISMISS)
}
StateLayoutEnum.DATA_ERROR -> {mStateViewLiveData.postValue(StateLayoutEnum.DATA_ERROR)
}
StateLayoutEnum.DATA_NULL -> {mStateViewLiveData.postValue(StateLayoutEnum.DATA_NULL)
}
StateLayoutEnum.NET_ERROR -> {mStateViewLiveData.postValue(StateLayoutEnum.NET_ERROR)
}
StateLayoutEnum.HIDE -> {mStateViewLiveData.postValue(StateLayoutEnum.HIDE)
}
}
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取 Repository
*/
inline fun <reified R> getRepository(): R? {
try {
val clazz = R::class.java
return clazz.newInstance()} catch (e: Exception) {e.printStackTrace()
}
return null
}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期初始化
*/
override fun onCreate() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期页面可见
*/
override fun onStart() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期页面获取焦点
*/
override fun onResume() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期页面失去焦点
*/
override fun onPause() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期页面不可见
*/
override fun onStop() {}
/**
* AUTHOR:AbnerMing
* INTRODUCE: 生命周期页面销毁
*/
override fun onDestroy() {}
}
简单的 Activity,大家能够发现,其实就是规范的 MVVM 模式封装,Fragment 的封装也是基于此,搞清楚上述,基本上咱们这个基类库就实现了大半,的确也没什么好说的,大家间接看应用吧。
四、封装后在业务中如何应用
通过以上的封装,咱们在业务层所有的页面就能够继承父类进行应用,以达到代码的高度对立,使得架构模式简单化,让业务代码清晰化,目前的封装,大家能够间接封装成库或者打成 aar 给到其余开发者应用,目前我曾经上传到近程,不想麻烦的老铁,能够间接依照上面的步骤进行应用。
1、在你的根我的项目下的 build.gradle 文件下,引入 maven。
allprojects {
repositories {maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master"}
}
}
2、在你须要应用的 Module 中 build.gradle 文件下,引入依赖。
dependencies {implementation 'com.vip:base:1.0.2'}
通过以上的 Maven 仓库依赖,咱们就能够欢快的进行应用了,上面针对各个封装的性能进行一个简略的演示,当然,大家能够间接看源码中的实例,那里绝对比拟全面。
1、一般的 Activity 的继承
如果,你的 Activity 页面逻辑比较简单,倡议继承 BaseActivity,此父类,没有与 ViewModel 相结合,只蕴含失常且简略的逻辑解决,目前必须重写的只有一个 initData 办法,其余办法,大家能够依据业务重写即可。
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
/**
* AUTHOR:AbnerMing
* INTRODUCE: 初始化数据
*/
override fun initData() {setBarTitle("主页")
}
}
2、ViewModel 模式 Activity 的继承
View 层,须要继承 BaseVMActivity
class TestViewModelActivity : BaseVMActivity<ActivityViewModelBinding,
TestViewModel>(R.layout.activity_view_model) {override fun initVMData() {setBarTitle("ViewModel 形式应用")
}
}
ViewModel 层,须要继承 BaseViewModel
理论的业务中,遇到网络申请,缺省页展现,Dialog 显示暗藏,调用 changeStateView 办法,UI 层只须要重写对应的办法即可。
class TestViewModel : BaseViewModel() {
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取须要的 Repository
*/
private val repository by lazy {getRepository<TestRepository>()
}
}
Model 层,个别依据理论须要,进行具体的封装应用。
class TestRepository {}
3、DataBinding 模式应用
View 层,继承 BaseVMActivity,返回以后视图的绑定 variable
class DataBindActivity :
BaseVMActivity<ActivityDataBindBinding,
DataBindViewModel>(R.layout.activity_data_bind) {override fun initVMData() {setBarTitle("DataBinding 应用")
}
override fun getVariableId(): Int {return BR.data}
}
ViewModel 层继承 BaseViewModel
class DataBindViewModel : BaseViewModel() {
var oneWayContent = "单向绑定数据测试"
var twoWayContent = "双向绑定数据测试"
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取双向绑定数据
*/
var clickListener = View.OnClickListener {Toast.makeText(it.context, twoWayContent, Toast.LENGTH_SHORT).show()}
}
XML 视图,间接绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="data"
type="com.abner.base.bind.DataBindViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/gwm_dp_20"
android:paddingRight="@dimen/gwm_dp_20">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/gwm_dp_20"
android:text="@{data.oneWayContent}" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/gwm_dp_20"
android:hint="双向绑定"
android:text="@={data.twoWayContent}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/gwm_dp_20"
android:onClick="@{data.clickListener}"
android:text="获取双向绑定数据" />
</LinearLayout>
</layout>
4、Fragment 的简略应用
如果,你的 Fragment 页面逻辑比较简单,倡议继承 BaseFragment,此父类,没有与 ViewModel 相结合,只蕴含失常且简略的逻辑解决,目前必须重写的只有一个 initData 办法,其余办法,大家能够依据业务重写即可。
class TestPagerFragment : BaseFragment
<FragmentTestPagerBinding>(R.layout.fragment_test_pager) {override fun initData() {}}
5、ViewModel 模式 Fragment 的继承
View 层,须要继承 BaseVMFragment
class TestViewModelPagerFragment :
BaseVMFragment<FragmentTestPagerBinding,
TestFragmentViewModel>(R.layout.fragment_test_pager) {override fun initVMData() {}}
ViewModel 层,须要继承 BaseViewModel
class TestFragmentViewModel :BaseViewModel(){
/**
* AUTHOR:AbnerMing
* INTRODUCE: 获取须要的 Repository
*/
private val repository by lazy {getRepository<TestRepository>()
}
}
Model 层,个别依据理论须要,进行具体的封装应用。
class TestRepository {}
6、Fragment 的 DataBinding 模式应用和 Activity 相似,就不赘述了。
7、事件音讯总线应用
一般事件发送
LiveDataBus.send("send", "我发送了一条一般音讯")
一般发送事件接管
LiveDataBus.observe(this, "send", Observer<String> {Toast.makeText(this, it, Toast.LENGTH_SHORT).show()})
粘性事件发送
LiveDataBus.sendSticky("sendSticky", "我发送了一条粘性事件音讯")
粘性事件接管
LiveDataBus.observeSticky(this, "sendSticky", Observer<String> {Toast.makeText(this, it, Toast.LENGTH_SHORT).show()})
更多的其余性能应用,大家间接看 Github 即可,上边有比拟清晰的介绍。
五、开源以及 Demo 查看
以上的封装,目前曾经开源,大家能够下载查看源码,或者进行二次更改应用,地址是:
https://github.com/AbnerMing888/VipBase
相干 Demo,大家能够 down 下我的项目,运行即可,这里简略贴张效果图:
目前的封装,没有过多的冗余代码,齐全能够满足理论的业务须要,大家能够依照这种模式试验一番,遇到问题,能够多多交换,毕竟,技术是凋谢的,交换中能力一直的提高,当然了,须要联合本人的理论业务进行应用,毕竟我的项目中不应存在多个架构模式,MVC 也好,MVP,MVVM,MVI 也罢,无论应用哪种,适宜的才是最好的。
好了,各位老铁,这篇文章就到这里,下篇文章《组件化开发,从未如此简略》正在撰写中,大家敬请期待!