Skip to content

3、基本使用

imyyq-mac edited this page Aug 16, 2020 · 11 revisions

1、配置 Application

如果你有自己的 Application,那么要么继承 BaseApp,要么在你的 Application 中调用 BaseApp 的 init 方法:

override fun onCreate() {
    super.onCreate()
    BaseApp.initApp(this)
}

如果没有自己的 Application,manifest 中声明 application 可直接使用 BaseApp。

2、配置 GlobalConfig

框架对封装的功能尽可能提供了全局/局部开启和关闭的能力,你不需要的功能可全局关闭,需要的功能可局部开启,反之亦然。具体配置详见字段注释。默认所有功能都是关闭的,按需开启。

比如你大部分的 Activity 需要侧滑返回的功能,可配置 GlobalConfig.gIsSupportSwipe 为 true,然后针对主页等小部分不需要侧滑返回的 Activity 复写 isSupportSwipe() 返回 false 即可。

3、创建 xml

如果你不使用 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 标签包裹布局。

图1

因为几乎每个页面都会有个 vm,所以可以把变量放到 Live Template 中,这样就可以快速生成了。

图2

4、创建 View 层

在 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)
    }
}

1. 复写 initViewModel 方法

通常是为了提供数据仓库 BaseModel 的实例,千万不要直接 new XxxViewModel(),直接 new 出来的就失去了 VM 的特性了。

initViewModel 并不是必须复写的,只有你想自己给 VM 提供 M 层仓库时才需要,否则框架可以帮你自动关联仓库实例,详见下方的 6、创建 Model 层

2. View 的数据和操作流向 ViewModel

View 层会持有 ViewModel 层的实例 mViewModel,xml 中也持有了 VM 层的实例,意味着 View 层的所有操作都可以流向 ViewModel 层

也会持有 View 层的实例 mBinding。mBinding 就完全可以消灭掉 findViewById 了,虽然 kotlin-android-extensions 也有这样的功能,但是 mBinding 可读性更好。

3. 配置可选

要注意的是,上述方法都不是必须复写的,一切配置都尽量可选。 像 isNeedLoadingDialog 这类的方法,是功能的开关,默认是由 GlobalConfig 决定的,但是当你的界面不需要对话框时,可以复写并返回 false,那么框架将不会实例化相关变量,减少内存占用,其他类似方法也是一样的。

4. 不使用 DataBinding

不想使用 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) })

可稍微减少代码。

其他说明都在注释里了。

5. View 层初始化方法的调用顺序

  • initParam,初始化参数,接收外界传入的参数,此时 mViewModel 已经实例化,可将参数传给 mViewModel,或者直接在 V 或 VM 中使用 getXxxxFromXxx 方法。
  • initViewObservable,初始化视图事件监听,通常用来初始化 v 和 vm 的 LiveData 交互。或者是设置 view 的点击事件等。
  • initData,开始初始化数据。

6. Fragment 共享 ViewModel 实例

一个 Activity 下可以有多个 Fragment,比如 ViewPager 中的 Fragment,有可能要共享同一个 ViewModel 实例,此时在创建 VM 实例时,应该将 Fragment 挂载的 Activity 传给 ViewModelProvider,框架对此提供了支持。

只需要在继承基类 Fragment 时,构造函数的 sharedViewModel 参数设置为 true 即可。

5、创建 ViewModel 层

/**
 * 如果没有数据仓库,可以使用 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 层。

6、创建 Model 层

按照 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

通常数据来源多来源于网络和本地,架构图如下:

图1

比如如果你的应用还有来自蓝牙的,那应该还有个 bluetooth 的目录。仓库 Repository 持有了这些数据来源的实例,并通过接口和他们通信,而 VM 是持有了 Repository 的实例并通过接口向仓库请求数据,这里说的接口就是语言层面上的接口。

这样设计的好处就是各个模块都分开了,有利于单元测试,也有利于扩展,但是稍显啰嗦和复杂,对于简单的应用,也许 M 层都直接在 VM 层实现了。具体使用可以查看示例: MVVMArchitectureSample

Model 层简化

通常来说,VM 只会持有一个 Repository 实例,即只有一个仓库数据来源,上述的操作中,是通过 ViewModelFactory 在创建 VM 时通过其构造方法传入 Repository 实例的,稍显麻烦,在 1.0.1 版本中,提供了自动创建仓库并缓存仓库实例的方法。

只需要在继承 BaseViewModel 的时候,指定 Repository 泛型即可:

class NetworkViewModel(app: Application) : BaseViewModel<AppRepository>(app) {
    /**
     * 是否缓存自动创建的仓库,默认是 true
     */
    override fun isCacheRepo(): Boolean {
        return true
    }
}
  1. 其中 AppRepository 不能是 object 类型的,且必须提供无参构造,因为自动创建时是通过反射创建的。默认会自动创建仓库并缓存仓库。后续会考虑使用 Hilt 依赖注入来创建仓库

  2. 如果不想缓存,可复写 isCacheRepo 并返回 false,这样每次 VM 需要仓库都会重新创建实例。

  3. 如果不需要仓库,泛型用 BaseModel 即可。

  4. 如果有仓库,但不想自动创建,还是想通过 ViewModelFactory 传给 VM,那么可以复写次构造函数和相关方法:

class NetworkViewModel(app: Application, model: AppRepository) : BaseViewModel<AppRepository>(app, model) {
}

如此框架就不帮你自动创建了。

自动创建的好处是节省代码,且 Model 层持有的相关 DataSource 都在内部直接引用了,不用外部通过构造传入了,要改也只需要改 Repository 类即可。