3、基本使用
如果你有自己的 Application,那么要么继承 BaseApp,要么在你的 Application 中调用 BaseApp 的 init 方法:
override fun onCreate() {
super.onCreate()
BaseApp.initApp(this)
}
如果没有自己的 Application,manifest 中声明 application 可直接使用 BaseApp。
框架对封装的功能尽可能提供了全局/局部开启和关闭的能力,你不需要的功能可全局关闭,需要的功能可局部开启,反之亦然。具体配置详见字段注释。默认所有功能都是关闭的,按需开启。
比如你大部分的 Activity 需要侧滑返回的功能,可配置 GlobalConfig.gIsSupportSwipe 为 true,然后针对主页等小部分不需要侧滑返回的 Activity 复写 isSupportSwipe() 返回 false 即可。
如果你不使用 DataBinding,这一步可以忽略。解决 findViewById 可以使用 ViewBinding。DataBinding 的相关知识请自行查阅,可以看看我的文章:DataBinding,让人又爱又恨的一个框架。
<?xml version="1.0" encoding="utf-8"?>
<!--
使用 DataBinding 的话,必须用 layout 包裹
-->
<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>
<!--
声明变量,通常有 vm 的界面,都需要声明对应的 vm 变量
在构造界面时通过 构造函数 把 BR.viewModel 传给 DataBindingBaseXXXX
-->
<variable
name="viewModel"
type="com.imyyq.sample.MainViewModel"
/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity"
>
...
</LinearLayout>
</layout>
这里有个技巧,在 xml 根目录执行 Alt + Enter,可快速用 layout 标签包裹布局。
因为几乎每个页面都会有个 vm,所以可以把变量放到 Live Template 中,这样就可以快速生成了。
在 Android 的 MVVM 架构中,Activity/Fragment/XML 都属于 View 层。
/**
* View 层需要继承相应的基类:DataBindingBaseFragment/DataBindingBaseActivity,ViewBindingBaseActivity/ViewBindingBaseFragment
*
* ActivityMainBinding 是 R.layout.activity_main 生成的 binding 类,是 DataBinding/ViewBinding 相关的知识。
* MainViewModel 是界面的主 vm,如果你的界面没有 vm,可以用 BaseViewModel。
*
* 构造需传入两个参数,一个是 R.layout.xxxx,另一个是 BR.viewModel,viewModel 是 xml 中的变量,即上面 xml 中 variable 的 name,当然也可以命名其他的名称。
* 如果 xml 没有配置 vm 变量,可不传,即可空。
*/
class MainActivity : DataBindingBaseActivity<ActivityMainBinding, MainViewModel>(
R.layout.activity_main, BR.viewModel
) {
// 除了主 vm,还可以有其他的 vm,来自 fragment-ktx 的 viewModels 扩展,可快速一行代码创建实例
private val mTestViewModel by viewModels<TestViewModel>()
// 是否需要对话框
override fun isNeedLoadingDialog(): Boolean {
return super.isNeedLoadingDialog()
}
// vm 是否需要启动和结束界面
override fun isViewModelNeedStartAndFinish(): Boolean {
return super.isViewModelNeedStartAndFinish()
}
... 还有其他的配置方法
/**
* 打开这个界面时传入的参数可以在这里处理。
*
* 此时 mViewModel 和 mBinding 已实例化。
*/
override fun initParam() {
}
/**
* 这个方法是用来绑定 vm 中的响应式变量到界面中的,比如 LiveData。
*/
override fun initViewObservable() {
// mViewModel 是界面关联的主 VM 的实例,有上述的泛型参数决定,这里是 MainViewModel。
// mBinding 是 layout 文件的绑定类,包含了声明了 id 的所有 view 的引用。
Log.i("MainActivity", "initViewObservable: $mViewModel, $mBinding")
}
/**
* 这个方法被调用时,此时界面已经初始化完毕了,可以进行获取数据的操作了,比如请求网络。
*/
override fun initData() {
}
/**
* 如果你的 vm 是通过 自定义 factory 创建的,可复写此方法。
* 否则将由框架帮你实例化 vm。
*/
override fun initViewModel(viewModelStoreOwner: ViewModelStoreOwner): NetworkViewModel {
return ViewModelProvider(
viewModelStoreOwner,
AppViewModelFactory
).get(NetworkViewModel::class.java)
}
}
其中 initViewModel 的 AppViewModelFactory 类如下:
object AppViewModelFactory : NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(NetworkViewModel::class.java)) {
return NetworkViewModel(BaseApp.getInstance(), Injection.provideDemoRepository()) as T
}
throw IllegalArgumentException("Unknown ViewModel class: " + modelClass.name)
}
}
通常是为了提供数据仓库 BaseModel 的实例,千万不要直接 new XxxViewModel(),直接 new 出来的就失去了 VM 的特性了。
initViewModel 并不是必须复写的,只有你想自己给 VM 提供 M 层仓库时才需要,否则框架可以帮你自动关联仓库实例,详见下方的 6、创建 Model 层
View 层会持有 ViewModel 层的实例 mViewModel,xml 中也持有了 VM 层的实例,意味着 View 层的所有操作都可以流向 ViewModel 层。
也会持有 View 层的实例 mBinding。mBinding 就完全可以消灭掉 findViewById 了,虽然 kotlin-android-extensions 也有这样的功能,但是 mBinding 可读性更好。
要注意的是,上述方法都不是必须复写的,一切配置都尽量可选。 像 isNeedLoadingDialog 这类的方法,是功能的开关,默认是由 GlobalConfig 决定的,但是当你的界面不需要对话框时,可以复写并返回 false,那么框架将不会实例化相关变量,减少内存占用,其他类似方法也是一样的。
不想使用 DataBinding 的同学,可继承自 ViewBindingBaseActivity/ViewBindingBaseFragment,并复写 initBinding 方法即可,这样 xml 就不用 layout 标签包裹了,只需配置时在 build.gradle 中开启 ViewBinding 功能,生成的类名和 DataBinding 是一样的,即 R.layout.activity_nav 会生成 ActivityNavBinding 类,mBinding 变量也能彻底解决 findViewById 的问题。如下:
class NavActivity : ViewBindingBaseActivity<ActivityNavBinding, NavViewModel>() {
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): ActivityNavBinding =
ActivityNavBinding.inflate(inflater)
}
不使用 DataBinding 的话,就只能使用 LiveData 来让 VM 的数据流向 V 了,需要手动写 observe。
基类中封装了 observe 方法,如下:
/**
* <pre>
* // 一开始我们这么写
* mViewModel.liveData.observe(this, Observer { })
*
* // 用这个方法可以这么写
* observe(mViewModel.liveData) { }
*
* // 或者这么写
* observe(mViewModel.liveData, this::onChanged)
* private fun onChanged(s: String) { }
* </pre>
*/
fun <T> observe(liveData: LiveData<T>, onChanged: ((t: T) -> Unit)) =
liveData.observe(this, Observer { onChanged(it) })
可稍微减少代码。
其他说明都在注释里了。
- initParam,初始化参数,接收外界传入的参数,此时 mViewModel 已经实例化,可将参数传给 mViewModel,或者直接在 V 或 VM 中使用 getXxxxFromXxx 方法。
- initViewObservable,初始化视图事件监听,通常用来初始化 v 和 vm 的 LiveData 交互。或者是设置 view 的点击事件等。
- initData,开始初始化数据。
一个 Activity 下可以有多个 Fragment,比如 ViewPager 中的 Fragment,有可能要共享同一个 ViewModel 实例,此时在创建 VM 实例时,应该将 Fragment 挂载的 Activity 传给 ViewModelProvider,框架对此提供了支持。
只需要在继承基类 Fragment 时,构造函数的 sharedViewModel 参数设置为 true 即可。
/**
* 如果没有数据仓库,可以使用 BaseModel 作为 Model 层。
*/
class MainViewModel(app: Application) : BaseViewModel<BaseModel>(app) {
/**
* vm 可以感知 v 的生命周期
*/
override fun onResume(owner: LifecycleOwner) {
// 注意!!!!! vm 层绝对不可以引用 v 层的实例,需要 context 要么通过 application,要么通过 AppActivityManager
val app = getApplication<MyApp>()
Log.i("MainViewModel", "commonLog - onResume: $app")
}
... 还有封装了一系列的方法
}
ViewModel 层需要继承 BaseViewModel。最终继承自 AndroidViewModel,这样可拥有 Application 的实例。
一定要注意的是:vm 层绝对不可以引用 v 层的实例,否则会造成内存泄露。
vm 的数据流向 v 层,只能通过 DataBinding 或 LiveData,特别是耗时操作,不可以通过回调流回 v 层。
按照 Google 的设计建议,Model 数据层应该是一个仓库 Repository,对外暴露数据接口,使用者无需知道数据是从哪里来的,是本地还是网络还是其他什么的对使用者来说都不重要。在本框架中,它应该继承自 BaseModel。
推荐类结构如下:
data
source
http
service
ApiService.kt
HttpDataSourceImpl.kt
local
AppDatabase.kt
LocalDataSourceImpl.kt
UserDao.kt
HttpDataSource.kt
LocalDataSource.kt
Repository.kt
通常数据来源多来源于网络和本地,架构图如下:
比如如果你的应用还有来自蓝牙的,那应该还有个 bluetooth 的目录。仓库 Repository 持有了这些数据来源的实例,并通过接口和他们通信,而 VM 是持有了 Repository 的实例并通过接口向仓库请求数据,这里说的接口就是语言层面上的接口。
这样设计的好处就是各个模块都分开了,有利于单元测试,也有利于扩展,但是稍显啰嗦和复杂,对于简单的应用,也许 M 层都直接在 VM 层实现了。具体使用可以查看示例: MVVMArchitectureSample
通常来说,VM 只会持有一个 Repository 实例,即只有一个仓库数据来源,上述的操作中,是通过 ViewModelFactory 在创建 VM 时通过其构造方法传入 Repository 实例的,稍显麻烦,在 1.0.1 版本中,提供了自动创建仓库并缓存仓库实例的方法。
只需要在继承 BaseViewModel 的时候,指定 Repository 泛型即可:
class NetworkViewModel(app: Application) : BaseViewModel<AppRepository>(app) {
/**
* 是否缓存自动创建的仓库,默认是 true
*/
override fun isCacheRepo(): Boolean {
return true
}
}
-
其中 AppRepository 不能是 object 类型的,且必须提供无参构造,因为自动创建时是通过反射创建的。默认会自动创建仓库并缓存仓库。后续会考虑使用 Hilt 依赖注入来创建仓库
-
如果不想缓存,可复写 isCacheRepo 并返回 false,这样每次 VM 需要仓库都会重新创建实例。
-
如果不需要仓库,泛型用 BaseModel 即可。
-
如果有仓库,但不想自动创建,还是想通过 ViewModelFactory 传给 VM,那么可以复写次构造函数和相关方法:
class NetworkViewModel(app: Application, model: AppRepository) : BaseViewModel<AppRepository>(app, model) {
}
如此框架就不帮你自动创建了。
自动创建的好处是节省代码,且 Model 层持有的相关 DataSource 都在内部直接引用了,不用外部通过构造传入了,要改也只需要改 Repository 类即可。