1. 前言
上一篇介绍了 ardf
(android rapid development framework,Android 快速开发框架) 基于 DataBinding 对 RecyclerView 的封装实现和使用,ardf
目的是封装一系列 Android 开发框架帮助开发者快速开发提高开发效率。本篇是 ardf
的第二篇,将介绍基于 DataBinding Koin 实现的 MVVM 模式页面快速开发框架的使用和详细实现。
Android基于DataBinding封装RecyclerView实现快速列表开发
DataBinding 是 Google 官方的一个数据绑定框架,借助该库,您可以声明式的将应用中的数据源绑定到布局中的界面组件上,实现通过数据驱动界面更新,从而降低布局和逻辑的耦合性,使代码逻辑更加清晰。更多关于 DataBinding 的介绍请查阅 Google 官方文档:DataBinding[1]
Koin 是一个基于 Kotlin 的 DSL 实现的轻量级依赖注入框架,相比于 Dagger2, Koin 无反射、无代码生成且使用更简单;借助该库可轻松在基于 kotlin 的 Android 应用开发中实现依赖注入,降低代码的耦合性。更多关于 Koin 的介绍及使用请查阅官方文档:Koin[2]
2. 使用效果
在 Android 应用中页面显示几乎是每个应用必不可少的功能,要让页面布局在手机上进行显示绝大多数情况都是使用 Activity/Fragment 来承载;而创建一个 Activity/Fragment 需要先加载布局,然后从布局中找到我们需要的 View 对象再去更新其数据或为其添加相应事件处理,那么如果将这些封装成通用的 Activity/Fragment 基类则将减少很多开发代码从而提高开发效率。
先看一下封装后的代码使用效果。
2.1 项目配置
在项目 Module 的 build.gradle 中添加依赖,如下:
代码语言:javascript复制dependencies {
implementation 'com.loongwind.ardf:base:1.0.1'
}
因 ardf
基于 DataBinding 进行封装,需要开启 DataBinding,启用方式如下:
android {
...
buildFeatures {
dataBinding true
}
}
同时在插件中添加 kotlin-kapt
的插件,如下:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// 添加 kotlin-kapt 插件
id 'kotlin-kapt'
}
配置完成后,点击 Sync Now
同步 build.gradle 配置生效后即可进行代码开发。
2.2 自动装载布局
通过继承 ardf
提供的 BaseBindingActivity
/ BaseBindingFragment
可快速装载页面布局。
在 layout 里创建一个 test_page.xml
的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="text"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="15dp"
android:textSize="30sp"
android:text="@{text}"/> //绑定数据
</LinearLayout>
</layout>
然后创建一个 TestActivity:
代码语言:javascript复制//泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
class TestActivity : BaseBindingActivity<TestPageBinding>() {
// 通过 binding 操作界面元素更新界面
override fun initDataBinding(binding: TestPageBinding) {
binding.text = "Hello ardf"
}
}
代码完成了,只需继承 BaseBindingActivity
泛型填写布局自动生成的 Binding 类,然后在实现的 initDataBinding
方法中绑定界面数据即可。
运行效果如下:
同样 Fragment 的使用方法类似,创建一个 TestFragment :
代码语言:javascript复制//泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
class TestFragment : BaseBindingFragment<TestPageBinding>() {
// 通过 binding 操作界面元素更新界面
override fun initDataBinding(binding: TestPageBinding) {
binding.text = "Hello ardf"
}
}
运行效果跟 Activity 一样,这里就不重复贴图了。
2.3 自动注入 ViewModel
ardf
除了自动装载布局以外,还支持自动注入 ViewModel 并将 ViewModel 与界面布局自动进行绑定。
首先创建一个 TestViewModel 继承自 BaseViewModel
:
class TestViewModel : BaseViewModel(){
val text = "Hello ardf ViewModel"
}
修改上面的 test_page.xml
布局接收vm
变量的 TestViewModel 数据:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!--通过 DataBinding 接收 ViewModel 对象-->
<variable
name="vm"
type="com.loongwind.ardf.demo.TestViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@ id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="@{vm.text}" //绑定 TestViewModel 中的 text 数据
android:padding="15dp"/>
</LinearLayout>
</layout>
通过 DataBinding 方式将 ViewModel 中的数据绑定到界面元素中。
然后再创建 TestActivity 继承自 BaseBindingViewModelActivity
:
//第一个泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
//第二个泛型就是上面创建的 ViewModel 类型
class TestActivity : BaseBindingViewModelActivity<TestPageBinding, TestViewModel>() {
}
可以发现,Activity 的代码又简介了许多。
最后一步是实现 ViewModel 的注入,ardf
基于 koin
实现依赖注入,需要创建 appModule 将 实现的 TestViewModel 添加到依赖中,然后在 Application 中初始化 koin,代码如下:
val appModule = module {
// 将 ViewModel 添加到 koin 依赖
viewModel{ TestViewModel()}
}
class App : Application() {
override fun onCreate() {
super.onCreate()
// 启动 koin
startKoin{
androidLogger()
androidContext(this@App)
// 添加 appModule
modules(appModule)
}
}
}
代码实现完成,运行效果如下:
跟之前的实现效果一致,同样的 Fragment 使用方法是一样的,只需继承 BaseBindingViewModelFragment
即可,如下:
//第一个泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
//第二个泛型就是上面创建的 ViewModel 类型
class TestFragment : BaseBindingViewModelFragment<TestPageBinding, TestViewModel>() {
}
2.4 事件处理
前面界面加载完成了,数据也可以在 ViewModel 中进行更新,常规事件也可以在 ViewModel 中进行处理,但是跟 Context 相关的处理在 ViewModel 中是没办法进行处理的,因为 ViewModel 中没办法拿到 Context 实例,比如 toast 提示、弹框、页面跳转等,这些情况怎么处理呢?
ardf
提供了事件的处理机制,可以将事件传递到 Activity / Fragment 中,然后在 Activity / Fragment 中进行涉及 Context 的处理,并且 ardf
提供了两种事件的默认处理:toast
(弹出 toast 提示)、back
(返回上一个页面)。
2.4.1 toast 提示
在 BaseViewModel
的子类中调用 postHintText
即可在界面上弹出对应的 toast 提示:
class TestViewModel : BaseViewModel(){
val text = "Hello ardf ViewModel"
fun showToastString(){
//传入字符串
postHintText("Hello ardf toast")
}
fun showToastStringRes(){
//传入字符串资源
postHintText(R.string.hello)
}
}
在布局里添加两个按钮,事件绑定对应的 showToast 方法,运行效果:
2.4.2 back 返回
在 BaseViewModel
的子类中调用 back()
方法即可:
class TestViewModel : BaseViewModel(){
val text = "Hello ardf ViewModel"
fun goBack(){
//调用父类提供的 back 方法
back()
}
}
同样在布局里添加按钮事件触发 goBack 方法,运行效果如下:
目前 back 方法只在 BaseBindingViewModelActivity 宿主的 BaseViewModel 子类中使用下有效
2.4.3 自定义事件
自定义事件可通过调用 postEvent
方法将事件传递到 Activity / Fragment 中,代码如下:
class TestViewModel : BaseViewModel(){
val text = "Hello ardf ViewModel"
companion object {
// 定义跳转到详情页的事件 id
const val EVENT_TO_DETAILS = 0x00
// 定义弹出 Dialog 的事件 id
const val EVENT_SHOW_DIALOG = 0x01
}
fun toDetailsPage(){
// 发送跳转详情页事件
postEvent(EVENT_TO_DETAILS)
}
fun showDialog(){
// 发送弹出 Dialog 事件
postEvent(EVENT_SHOW_DIALOG)
}
}
然后在 Activity / Fragment 中重写 onEvent
方法接收事件进行相应处理:
class TestActivity : BaseBindingViewModelActivity<TestPageBinding, TestViewModel>() {
// 接收事件
override fun onEvent(eventId: Int) {
super.onEvent(eventId)
// 判断事件 id 并进行对应处理
when(eventId){
TestViewModel.EVENT_TO_DETAILS -> startActivity(Intent(this, DetailsActivity::class.java))
TestViewModel.EVENT_SHOW_DIALOG -> showXxxDialog()
}
}
}
运行效果如下:
3. 源码解析
前面介绍了 ardf
实现自动装载布局、自动注入 ViewModel 和事件的处理的使用,那么 ardf
是如何实现这些功能的呢?
首先来看一下 ardf
关于页面封装的整体结构,如下:
主要分为四层:依赖库、基础支撑、布局自动绑定、ViewModel 自定绑定:
- • 依赖库:
ardf
关于页面封装所依赖的第三方库,核心是 databinding 和 koin 库,用于数据绑定和依赖注入。 - • 基础支撑:封装工具类、扩展和事件的 Model 及接口。
- • 布局自动绑定:基于 DataBinding 封装的 BaseBindingActivity 和 BaseBindingFragment。
- • ViewModel 自动绑定:在 BaseBindingActivity 和 BaseBindingFragment 的基础上再基于 koin 实现 ViewModel 的注入与绑定。
下面将通过源码详细介绍对应功能的实现原理。
3.1 自动装载布局的实现
在 2.2 的使用介绍中可以发现,自动装载布局的实现依赖了 DataBinding,将 DataBinding 通过布局文件生成的 Binding 类作为泛型传递给了 BaseBindingActivity
/ BaseBindingFragment
,那么在 BaseBindingActivity
/ BaseBindingFragment
中是如何通过这个 Binding 类去将布局与我们的 Activity/Fragment 进行绑定的呢?
为了帮助大家更好的理解我画了一个简单的时序图:
从时序图中可以发现核心实现是在 BaseBindingActivity 的 onCreate 中,主要分为以下三步:
- • 调用 createDataBinding 创建对应布局的 Binding 类,也就是传入的泛型的实例
- • 通过 setContentView 将实例化的 Binding 对象的 root View 设置给当前 Activity
- • 调用子类实现的 initDataBinding 方法初始化界面数据
结合时序图再来看一下源码:
代码语言:javascript复制abstract class BaseBindingActivity<BINDING :ViewDataBinding>:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//创建 ViewDataBinding 实例
val binding = createDataBinding()
//绑定当前 Activity 生命周期
binding.lifecycleOwner = this
//设置 View
setContentView(binding.root)
// 初始化数据绑定
initDataBinding(binding)
}
/**
* 根据泛型 BINDING 创建 ViewDataBinding 实例
*/
private fun createDataBinding(): BINDING {
return getBindingType(javaClass) // 获取 ViewDataBinding 泛型实际类型
?.getMethod("inflate", LayoutInflater::class.java) // 反射获取 inflate 方法
?.invoke(null, LayoutInflater.from(this)) as BINDING // 通过反射调用 inflate 方法
}
/**
* 初始化数据绑定
* 子类实现该方法通过 binding 绑定数据
*/
abstract fun initDataBinding(binding: BINDING)
}
代码不多,具体作用也写了相应注释,关键代码在 createDataBinding
方法,做了三件事:
- • 获取当前 Activity 上 ViewDataBinding 的实际类型,即 DataBinding 通过布局文件生成的 Binding 类。
- • 通过反射获取到 ViewDataBinding 的
inflate
方法,该方法会返回当前 Binding 实例。 - • 通过反射调用
inflate
方法初始化 Binding 实例
getBindingType
是一个全局的工具方法,源码如下:
fun getBindingType(clazz: Class<*>) : Class<*>? {
val superclass = clazz.genericSuperclass
if (superclass is ParameterizedType ) {
//返回表示此类型实际类型参数的 Type 对象的数组
val actualTypeArguments = superclass.actualTypeArguments
return actualTypeArguments.firstOrNull {
// 判断是 Class 类型 且是 ViewDataBinding 的子类
it is Class<*> && ViewDataBinding::class.java.isAssignableFrom(it)
} as? Class<*>
}
return null
}
实现是通过反射获取传入类型的所有泛型,然后取出第一个是 ViewDataBinding
子类的类型进行返回。
这样就实现了通过泛型传入 Binding 自动加载布局并与当前 Activity 进行绑定。
BaseBindingFragment
的实现逻辑与 BaseBindingActivity
的实现逻辑基本一致,只是将实现换到了 onCreateView
方法中,如下:
abstract class BaseBindingFragment<BINDING:ViewDataBinding>: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//创建 ViewDataBinding 实例
val binding = createDataBinding(inflater, container)
//绑定当前 Fragment 生命周期
binding.lifecycleOwner = this
// 初始化数据绑定
initDataBinding(binding)
//返回布局 View 对象
return binding.root
}
/**
* 根据泛型 BINDING 创建 ViewDataBinding 实例
*/
private fun createDataBinding(inflater: LayoutInflater, container: ViewGroup?): BINDING {
return getBindingType(javaClass)// 获取泛型类型
?.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java) // 反射获取 inflate 方法
?.invoke(null, inflater, container, false) as BINDING // 通过反射调用 inflate 方法
}
/**
* 初始化数据绑定
* 子类实现该方法通过 binding 绑定数据
*/
abstract fun initDataBinding(binding: BINDING)
}
3.2 自动注入 ViewModel 的实现
在 MVVM 模式的开发中,一般是通过 DataBinding 将布局与 ViewModel 绑定使用,ViewModel 中的数据变化自动刷新界面,实现数据驱动 UI 刷新,那么我们怎么将这个过程进行通用的封装呢?
还是先来看一个简单的时序图:
从时序图中不难发现,核心是基于上面介绍的 BaseBindingActivity
实现的 BaseBindingViewModelActivity
类,重写了 initDataBinding
方法并实现了如下功能:
- • 调用
createViewModel
方法创建 ViewModel 实例对象 - • 调用 Binding 的
setVariable
方法绑定 ViewModel 对象
BaseBindingViewModelActivity
源码如下:
open class BaseBindingViewModelActivity<BINDING : ViewDataBinding, VM : BaseViewModel>:
BaseBindingActivity<BINDING>(){
//创建 ViewModel 变量并延迟初始化
val viewModel:VM by lazy {
createViewModel()
}
override fun initDataBinding(binding: BINDING) {
//绑定 viewModel
//绑定变量为 vm。
// 具体业务实现中在实际的布局 xml 文件中声明当前视图的 ViewModel 变量为 vm 即可自动进行绑定。
binding.setVariable(BR.vm,viewModel)
}
/**
* @description 初始化 ViewModel 并自动进行绑定
* @return VM ViewModel 实例对象
*/
private fun createViewModel():VM{
try {
//注入 ViewModel,并转换为 VM 类型
return injectViewModel() as VM
}catch (e:Exception){
// 抛出异常
throw Exception("ViewModel is not inject", e)
}
}
}
定义 viewModel 变量并延迟调用 createViewModel
方法进行初始化;在 initDataBinding
将 viewModel 与布局的 vm
变量进行绑定。
vm
变量来源是因为在框架里创建了一个空的 ardf_base_activity.xml
布局中定义后生成的:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="vm" type="Object"/>
</data>
</layout>
createViewModel
方法里调用了扩展方法 injectViewModel
通过 Koin 注入 ViewModel,源码如下:
@OptIn(KoinInternalApi::class)
fun ComponentActivity.injectViewModel() : ViewModel?{
return getViewModel(javaClass, getKoinScope(), this, viewModelStore )
}
/**
* @param javaClass Class类型
* @param scope koin生命周期范围
* @param owner ViewModelStoreOwner 类型,ViewModel 绑定什么周期对象,Activity、Fragment 都实现了该接口
* @param viewModelStore 存储 ViewModel 的对象
*/
@OptIn(KoinInternalApi::class)
fun getViewModel(javaClass : Class<*>,
scope: Scope,
owner: ViewModelStoreOwner,
viewModelStore: ViewModelStore) : ViewModel?{
// 获取当前 Activity 上 ViewModel 泛型的实际类型
val viewModel = getViewModelType(javaClass)?.let {
// 获取 ViewModelFactory
val viewModelFactory = getViewModelFactory(owner, it.kotlin, null, null, null, scope)
//获取注入的 ViewModel
ViewModelLazy(it.kotlin, { viewModelStore }, { viewModelFactory} ).value
}
return viewModel
}
injectViewModel
中调用 getViewModel
方法:
- • 通过
getViewModelType
获取 ViewModel 的类型 - • 调用 Koin 提供的
getViewModelFactory
获取 ViewModelFactory - • 调用 Koin 提供的
ViewModelLazy
获取注入的 ViewModel
getViewModelType
的实现跟上面的 getBindingType
的原理一样,源码如下:
fun getViewModelType(clazz: Class<*>) : Class<out ViewModel>? {
val superclass = clazz.genericSuperclass
if (superclass is ParameterizedType) {
//返回表示此类型实际类型参数的 Type 对象的数组
val actualTypeArguments = superclass.actualTypeArguments
//返回第一个符合条件的 Type 对象
return actualTypeArguments.firstOrNull{
it is Class<*> && BaseViewModel::class.java.isAssignableFrom(it)
} as? Class<out ViewModel>
}
return null
}
最终实现自动注入 ViewModel 并与当前 Activity / Fragment 布局进行绑定的功能。
BaseBindingViewModelFragment 的实现原理与 BaseBindingViewModelActivity 的实现原理相同,这里就不在重复贴代码,有兴趣的可以直接去看源码
3.3 事件处理的实现
ViewModel 的自动绑定实现了,那怎么实现事件的处理呢?我们知道通过 DataBinding 可以将事件传递到 ViewModel 中进行处理,那么又怎么将需要用到 Context 等特殊事件传递到 Activity / Fragment 里去处理呢?
同样的先看一个简单的时序图:
时序图解析:
- • 事件通过 Activity 传到到 View
- • Binding 里监听到事件后将事件传递到 ViewModel
- • ViewModel 中调用父类
BaseViewModel
的postEvent
方法将事件传递到 Activity
前面两步是由 Android 本身事件机制和 DataBinding 来完成的,第三步是 ardf
实现的 BaseViewModel
来完成的,源码如下:
open class BaseViewModel: ViewModel() {
// 提示文字
var hintText = MutableLiveData<Event<String>>()
// 提示文字资源
var hintTextRes = MutableLiveData<Event<Int>>()
// 事件
var event = MutableLiveData<Event<Int>>()
protected fun postHintText(msg: String) {
hintText.value = Event(msg)
}
protected fun postHintText(msgRes: Int) {
hintTextRes.value = Event(msgRes)
}
protected fun postEvent(eventId: Int) {
event.value = Event(eventId)
}
/**
* 返回事件
*/
fun back(){
postEvent(EVENT_BACK)
}
}
声明了三个变量:hintText
、hintTextRes
、event
分别用于传递提示文字、提示文字资源和事件,并提供了对应的 post
方法用于快速调用;另外提供了一个 back
方法用于传递返回事件。
所有事件都是通过一个 Event
类进行包裹,源码如下:
class Event<T>(private val value: T) {
//是否已被处理
private var handled = false
/**
* @description 防止粘性事件被多次消费,多个观察者场景下,只会被一个观察者消费
*/
fun getValueIfNotHandled(): T? {
return if (handled) {
// 已处理返回 null
null
} else {
// 标记为已处理
handled = true
value
}
}
fun get(): T {
return value
}
}
使用 value
存放传入的值并提供获取值的 get 方法,其中定义 handled
变量标记事件是否已处理,通过 getValueIfNotHandled
获取值时如果已处理则返回空,未处理则返回对应的值并将事件标记为已处理,以防止一个事件被多次消费,当然如果需求如此的话可以调用 get()
方法获取事件值。
在 ViewModel 中传递事件以及事件的封装完成了,那怎么将这个事件传递到 Activity / Fragment 呢?
首先为 ViewModel 扩展一个 bind
方法:
fun BaseViewModel.bind(activity: BaseBindingViewModelActivity<*,*>) {
observe(activity, activity){
activity.onEvent(it)
}
}
fun BaseViewModel.observe( owner: LifecycleOwner, context: Context?, onEvent: (Int) -> Unit){
// 订阅提示文字变化
hintText.observe(owner){
val content = hintText.value?.getValueIfNotHandled()
if (!content.isNullOrBlank()) {
context?.toast(content)
}
}
// 订阅提示文字资源变化
hintTextRes.observe(owner) {
val contentRes = hintTextRes.value?.getValueIfNotHandled() ?: -1
if (contentRes > 0) {
context?.toast(contentRes)
}
}
// 订阅事件变化
event.observe(owner) {
event.value?.getValueIfNotHandled()?.let {
onEvent(it)
}
}
}
作用是订阅 hintText
、hintTextRes
的变化后弹出 toast
提示;同时订阅事件 event 的变化调用 onEvent
方法, onEvent
是接口 OnEventListener
提供的方法:
interface OnEventListener {
/**
*
* @description ViewModel 事件响应
* @param eventId 事件 id,根据实际业务自定义
* @return
*
*/
fun onEvent(eventId:Int)
}
BaseBindingViewModelActivity
需实现 OnEventListener
并在初始化 ViewModel 后调用 bind 方法,如下:
open class BaseBindingViewModelActivity<BINDING : ViewDataBinding, VM : BaseViewModel>:
BaseBindingActivity<BINDING>(), OnEventListener {
...
override fun onEvent(eventId: Int) {
if(eventId == EVENT_BACK){
onBackPressed()
}
}
/**
* @description 初始化 ViewModel 并自动进行绑定
* @return VM ViewModel 实例对象
*/
private fun createViewModel():VM{
try {
//注入 ViewModel,并转换为 VM 类型
val viewModel = injectViewModel() as VM
// 订阅事件
viewModel.bind(this)
return viewModel
}catch (e:Exception){
// 抛出异常
throw Exception("ViewModel is not inject", e)
}
}
}
这样就将事件传递到了 Activity / Fragment 的 onEvent 回调方法中,在该回调中就可以自定义处理 ViewModel 中传递过来的事件。
4. 总结
本文主要介绍了 ardf
(Android 快速开发框架)中基于 DataBinding Koin 的 MVVM 模式的页面快速开发及事件处理的使用方法,并通过源码解析详细介绍了其实现原理,从而进一步提高 Android 开发的效率。
源码地址:ardf[4] mavenCentral:
com.loongwind.ardf:base:1.0.1
引用链接
[1]
DataBinding: https://link.juejin.cn?target=https://developer.android.com/topic/libraries/data-binding
[2]
Koin: https://github.com/InsertKoinIO/koin
[3]
ardf: https://github.com/loongwind/ardf