拥抱新的交互方式,在 Android 中应用 MVVM

1、个人的开源库

不知不觉维护自己的几个开源库已经快两年了,现在我已经成功地将它们应用到了我的多个产品上面,比如 言叶。通过使用这些开源库能够大大降低开发的复杂度,为我节省大量的代码,提高效率。前些日子忙着做应用的新特性,现在终于有了些许时间。这里介绍下我的开源库,以及我做个人开发过程中如何通过新特性和技巧简化开发的。

AndroidVMLib

其实之前我也写了很多文章了介绍自己的设计了,这应该是最后一篇了吧,所以尽可能做到集大成吧。

2、Jetpack

Google 最新推出的 Jetpack 里出了很多好玩的框架,这里我的 VMLib 当然是基于 Jetpack 开发的。不过,VMLib 跟其他库不一样的地方在于,我并不喜欢绑定各种框架,诚然,我开发的时候也使用 Retrofit OkHttp 之类的框架,但是我不喜欢将其绑定到 VMLib 中。因为,这类的框架层出不穷,比如异步框架,昨天 RxJava 还是主流,今天已经是 Kotlin 协程的天下了。所以,我们没必要将其全部置入进去。而我觉得我的库设计有趣的地方在于,它使用了一种新的交互方式。这当然本质上还归功于 Jetpack 的 ViewModel 和 LiveData. 说到,ViewModel 和 LiveData,之前我有两篇文章各自介绍了它们的源码和实现原理,这里分别简单介绍下吧:

ViewModel 通过空的 Fragment 控制自身的生命周期。也就是 ViewModel 是存储在空的 Fragment 中的。这种技巧已经屡见不鲜了,比如 Glide, RxPermission 都使用这种方式维护自身生命周期。而 LiveData 说起来也简单,LiveData = 观察者模式+LifecycleOwner. 调用 LiveData 的 observe 方法的时候会传入 LifecycleOwner,Activity 和 Fragment 都实现了这个接口,所以 LiveData 能够获取到 Activity 或者 Fragment 的生命周期,因此也就可以很容易地做到,页面处于后台的时候不通知观察者,页面回归前台的时候通知观察者。

3. VMLib 最佳实践

3.1 拥抱新的交互方式

在 VMLib 中,我们提出了新的交互方式,既通过对 ViewModel 和 LiveData 进行包装,进一步简化它们的使用。如果不对 LiveData 进行封装,那么对每个数据都需要定义一个对应的 LiveData 实例。这样在 ViewModel 中会存在大量的全局变量。在本库中,我们对此做了优化。以下是一个使用异步操作从网络中请求数据的示例。在 ViewModel 中,我们只需要像下面这样调用:

// me.shouheng.eyepetizer.vm.EyepetizerViewModel#requestFirstPage
fun requestFirstPage() {
    // 通知 ui 数据加载状态
    setLoading(HomeBean::class.java)
    // 执行网络请求
    eyepetizerService.getFirstHomePage(null, object : OnGetHomeBeansListener {
        override fun onError(code: String, msg: String) {
            // 通知 ui 加载失败
            setFailed(HomeBean::class.java, code, msg)
        }

        override fun onGetHomeBean(homeBean: HomeBean) {
            // 通知 ui 加载成功
            setSuccess(HomeBean::class.java, homeBean)
        }
    })
}

在 ui 层,我们像下面这样对数据进行监听,

observe(HomeBean::class.java, {
    // 成功的回调 ...
}, { 
    // 失败时的回调 ...
}, { 
    // 加载中的回调 ...
})

按照上这样开发,将使得你的代码更加简洁明了。

其魔力在于,我们通过 Class 来唯一地定位一个 LiveData. 当在 ViewModel 中使用 setXXX 方法或者在 View 层使用 observe 方法的时候,我们会根据 Class 从 ViewModel 的 “LiveData 池” 中根据 Class 类型获取一个 LiveData 实例。也就是说,Class 类型是 LiveData 的唯一标识。

在最初的时候我也是直接在 ViewModel 中定义 LiveData. 后来,同事建议我通过 HashMap 收集起来,而后借鉴了其他 MVVM 框架,比如 Vue 之后,我直接修改成了上述通知逻辑。当然,传递一个 Class 过去怎么看都不如 Vue 调用的方式优雅。但在 Android 中应用 MVVM 和 Vue 中存在着天然的区别。因为不论 Java 还是 Kotlin,本质上还是一个强类型语言,无法做到像 JavaScript 那样的自由拓展。既然我们总是要传递一个类型过去,那么为什么不直接把类型当作唯一标识呢?

3.2 使用 DataBinding 和 ViewBinding

之前看别人的框架,总是使用 DataBinding 做数据绑定。而我一开始的时候就不喜欢这种方式。之前的文章也说明过原因了:一是 DataBidning 的支持有限,语法静态检查无完善,直接在 xml 中写逻辑造成代码分离和混乱;二是使用 DataBinding 会拖延应用编译的速度。而后,Google 官方推出了 ViewBidning. 这个框架原理也很简单,即通过编译时生成代码,自动将 id 转换为 camelCase 格式的字段,这样我们直接可以通过 ViewBinding 对象引用控件。关于二者的对比,可以参考 视图绑定. 在这中间还出现过 Kotlin-android-extensions,不过现在据说被废弃了。之前看到过有朋友使用这个框架做开发,不知道现在还好不好,踩坑需谨慎 :D

VMLib 对 DataBinding 和 ViewBinding 或者其他框架都可以提供支持。当你想要在项目中使用 DataBinding 的时候,你只需要通过在 Gradle 中做如下配置:

dataBinding {
    enabled true
}

并让你的 Activity 继承 CommonActivity 即可。CommonActivity 跟下面将要出场的 ViewBindingActivity 比起来可能有歧义,这只能算是历史遗留问题了。

对于较大的项目,DataBinding 会使用较多的时间在编译期间进行处理。因此,对于较大的项目,推荐使用 ViewBinding,即只通过 Binding 对象获取控件而不进行数据绑定。此时,你只需要在 Gradle 配置中启用 ViewBinding:

viewBinding {
    enabled true
}

并让你的 Activity 继承 ViewBindingActivity 即可。

当然,如果你既不打算使用 DataBinding 也不打算使用 ViewBinding,你可以直接让 Activity 继承 BaseActivity 来利用 VMLib 提供的各种功能。除了这些,我也包装了一个 f() 方法作为 findViewById 的替代品。

对于 Fragment,VMLib 提供了类似的、对应的抽象基类,可以根据自己的配置进行选择。

当你通过继承 VMLib 提供的 Activity 和 Fragment 的时候,都会要求你指定一个 ViewModel. 有些时候我们的页面非常简单,根本不值得为其单独定义一个 ViewModel. 此时,你就可以使用 VMLib 为你提供的、默认的 EmptyViewModel. 是不是很贴心呢?

不过,话说回来,Google 在设计 ViewBinding 的时候存在一个设定:当你在项目中引用了 ViewBinding 之后,默认为所有的布局文件生成 ViewBinding 对象,除非你添加了 viewBindingIgnore。这简直蜜汁操作,这意味着,假如当你在一个老项目中使用 ViewBinding 的时候,你的所有布局都会生成 ViewBinding 对象,即便你引入了 ViewBinding 仅仅是为了在以后添加的各种布局之上使用 ViewBinding 而不改动老逻辑. 假如项目中之前就存在几十个布局文件,那么引入之后会为你生成大量用不到的 ViewBinding 对象,除非你为每个布局添加一行 viewBindingIgnore 说明,或者把这些对象充分利用起来……其实逻辑应该反过来,即只有添加了某种注释的布局才会生成 ViewBinding 对象,这样就不会影响之前的逻辑啦。

3.3 工具类及其拓展类支持

跟其他的框架不同,VMLib 中内置的工具类是通过引用另一个项目来完成的,即 Android-Utils. 该库提供了 22 个独立的工具类,涉及从 IO、资源读取、图像处理、动画到运行时权限获取等各种功能。这是 VMLib 强大的原因之一,该库提供的工具方法可以你日常开发的绝大部分需求。你可以通过该项目的说明文件来了解该工具类库所提供的所有能力。

除了基础的工具类,VMLib 还提供了工具类基于 Kotlin 的拓展。如果你需要使用这个拓展库,需要在项目中添加如下依赖:

implementation "me.shouheng.utils:utils-ktx:$latest-version"

通过以上配置,可以大大减少你的开发的代码量。比如获取 Drawable 着色之后并将其赋值给 ImageView 只需要下面一行代码就可以搞定:

iv.icon = drawableOf(R.drawable.ic_add_circle).tint(Color.WHITE)

而在 Activity 内请求存储权限的时候也只需要下面一行代码就可以完成:

checkStoragePermission {  /* 添加请求到权限之后的逻辑 */ }

而对于防止控件连续点击,只需要下面这样一行代码即可完成:

btnRateIntro.onDebouncedClick { /* do something */ }

此外,我的库在开发的过程中还借鉴了其他语言的语法特性。比如 Python 的 join() 方法用来拼接字符串,

",".join(note.header.tags)

诸如此来的例子还有很多。总之你可以通过 VMLib 大大降低自己开发的难度。

3.4 Result 的响应式设计

在 VMLib 中,我们对 Activity 获取结果的 onActivityResult 进行了调整。你可以使用与过去不同的方式来获取另一个 Activity 返回的结果,而无需覆写 onActivityResult 方法。你可以通过 onResult 方法并传入一个整型的 Request Code 作为参数来获取返回的结果:

onResult(0) { code, data ->
    if (code == Activity.RESULT_OK) {
        val ret = data?.getStringExtra("__result")
        L.d("Got result: $ret")
    }
}

这实际上就是在 BaseActivity 中根据传入的 RequestCode 做了分发和回调。不过,好处就是你无需再主动覆写 onActivityResult 了。

除了使用 onResult 进行监听,你还可以使用我们提供的 start 方法。请求启动 Activity 的同时指定对结果的处理逻辑:

start(intent, 0) { resultCode, data ->
    if (resultCode == Activity.RESULT_OK) {
        val ret = data?.getStringExtra("__result")
        toast("Got result: $ret")
    }
}

通过定义顶层的 Activity,这些逻辑实现起来非常简单,并不需要太多的代码量,可以放心使用。

3.5 EventBus 的应用

VMLib 对 EventBus 的应用也提供了支持。对于 EventBus,一般来说,你有 EvnetBusAndroidEventBus 两个选择。不论你使用哪个 EventBus,在 VMLib 中都可以对其提供支持。VMLib 对两者提供了统一的实现。要应用 EventBus,你只需要在自己的 Activity 上面使用 @ActivityConfiguration 注解并指定 useEventBus 为 true 即可:

@ActivityConfiguration(useEventBus = true)
class DebugActivity : CommonActivity<MainViewModel, ActivityDebugBinding>() {
    // ...
}

然后,你只需要通过 @Subscribe 注解进行监听,

@Subscribe
fun onGetMessage(simpleEvent: SimpleEvent) {
    // do something ...
}

并使用 VMLib 提供的 Bus 类发送消息即可:

Bus.get().post(SimpleEvent("MSG#00001"))

VMLib 会根据你的代码环境中的配置处理各种细节,比如 EventBus 中,如果对某个类使用了 restier() 而没有 @Subscribe 注解则会抛异常。对于这些细节,我们统统在框架内部做了兼容处理。

3.6 交互类设计

在 VMLib 中,ViewModel 和 View 层之间的交互本质上是通过包装类来包装两者之间传递的信息的。包装类由 Resources 和枚举 Status 组成。其中 Status 是一个枚举,共三个字段,分别表示“成功”、“失败”和“加载中”状态。而 Resources,包含了 Status,一个泛型的数据 data 字段,外加 5 个以 udf 开头的预备字段用来添加一些额外的信息。

交互类是一个交互对象的封装,这借鉴了后端框架内部分层设计中的包装类除了用在 ViewModel 和 View 层之间,你还可以将其用在其他场景中,比如从 Kotlin 协程中获取返回结果。这个包装类的好处就在于,它可以将逻辑的三种状态通过一个对象返回过来。

3.7 容器 Activity

为了在项目中应用 Fragment,我们提供了 ContainerActivity. 顾名思义是一个 Fragment 的容器 Activity. 它的使用非常简单。比如,我们希望在 ContainerActivity 中打开 SampleFragment 这个 Fragment. 并且为其指定一些输入参数,指定动画的方向,那么我们只需要按照下面这样的链式调用即可:

ContainerActivity.open(SampleFragment::class.java)
    .put(SampleFragment.ARGS_KEY_TEXT, stringOf(R.string.sample_main_argument_to_fragment))
    .put(ContainerActivity.KEY_EXTRA_ACTIVITY_DIRECTION, ActivityDirection.ANIMATE_BACK)
    .withDirection(ActivityDirection.ANIMATE_FORWARD)
    .launch(context!!)

ContainerActivity 可以根据打开的 Fragment 类型获取 Fragment 实例并传入参数和展示。这些逻辑都包装到了 ContainerActivity 中,而你只需要实现自己的 Fragment 即可。

ContainerActivity 非常适用于简单的 Fragment——你无需为其单独定义一个 Activity,只需要把 Fragment 的类型和启动参数通过容器 Activity 传入,容器 Activity 内部可以直接解析参数并展示对应的 Fragment.

3.8 图片压缩框架

目前来说,VMLib 的最后一块拼图。

这是一个非常好用的 Android 图片压缩框架,其地址是 Compressor. 其不仅可以支持异步 API 也支持同步调用。此外,还支持多种类型的参数和输出,可以满足绝大部分应用场景。可以通过该项目的地址来了解更多,这里不多做说明了。

4、示例工程

4.1 架构设计

Android-VMLib 这个工程中,我们对日常开发中设计的逻辑做了简单的演示。因为最近开发的几款软件没有开源打算,也就无从展示。在这个项目中我也示例了大中型项目中是如何开发的。整体的框架与下面张图类似,

架构设计

也就是:

  • ViewModel 通过 Repo 与数据库和服务器进行数据交互
  • Repo 是单例的便于实现内存缓存和缓存的唯一性,如果必要可以通过 Room 实现数据库缓存(磁盘缓存),对于数据变更通过 LiveData 或者观察者模式通知数据变更

不过在示例工程中没有直接使用 Room 而是用了 SP 存储,权当一个示范吧。因为本身项目中从服务器返回的数据的数据结构也很难使用 Room 存储。诚然,Room 可以实现数据库表关联,但是这种特性迁移可能会比较麻烦,还是谨慎应用(其实,这种数据结构下,使用 Realm 是个不错的选择)。

此外,在项目中也示例了 ViewBinding 和 DataBiding 两种用法。

4.2 组件化

此外,在项目中我使用了阿里的 ARouter 作为路由,并且使用了其 Provider 来实现类似于的服务化功能,也就是将接口的实现和定义分离。比如在这个项目中,我定义了 module-api 和 module-eyepetizer 两个模块,前者用来提供 API 接口暴露给调用者,后者是具体的实现。当其他开发者需要引用你的接口的时候,只需要引用接口定义 module-api 模块而无需引用具体的实现,这是组件化的一种常用的套路。

当然,是否使用组件化还要根据项目的实际情况,对于有些应用,比如我的这个工具应用 移动工具箱. 它内部把功能划分为各个模块,可以很容易地想到通过组件化开发,让每个模块对应一个功能模块。这样的好处是,当有一天我希望将应用的某个功能模块单独打包为一个应用,或者移除某个模块的时候,可以很容易完成。但对于有的应用,一个模块足够完成的,使用组件化就是画蛇添足了。

4.3 依赖注入

之前,我考虑在项目中引用 Hilt 或者 Koin 作为项目的依赖注入框架。因为除了 Repo 和 ViewModel,在我的项目中还存在着大量的 Manager 需要到处引用。我希望通过引入依赖注入框架来组织看似混乱的引用问题。但是,在 VMLib 的基础之上使用 Hilt 或者 Koin 存在一个问题,即依赖注入本质就是谁来创建和维护各个实例的问题。假如,我在需要在 ViewModel 中引用 Manager,那么就不得不将 ViewModel 的创建逻辑交给依赖注入框架完成。但在我们的库中,ViewModel 是我们自己在抽象基类中创建的,此时依赖注入框架就无能为力了。

5、其他框架

这段时间,除了这个库以为,我还同步更新了几个其他的库:UI 控件和页面组件化框架 Anroird-uix;相机库 iCamera,其组件化页面可以参考 uix 这个库;另外还有一个没开源的 C++ 底层加密库(没错,加固之后还要再加密,已经被逼到撸 C++ 了)。另外,还有一个基于 SpringBoot 的后端脚手架 SpringBooster

SpringBooster 可以做到拿来即用,毕竟我们做 Android 开发,做个人应用的时候,经常会遇到各种后端需求,比如,应用内部反馈收集、应用配置信息下发、内部购买(赚钱)或者把应用内的图片放在后端等等。此时,你都可以通过 SpringBooster 来直接开发。这个脚手架已经为你实现了限流、日志、鉴权、CURD 等各种常用逻辑。然而,其最大特色是可以通过内部提供的静态类为你自动生成 60% 的代码,比如 DAO、SQL 语句、Mapper、Service、各种数据对象以及 Service 及其实现等。此外,我还加入了直接生成 Android 客户端对应的 Retorfit Service 和 Repo 的逻辑等,从此解放你的双手。

总的来说吧,单纯地浸淫在技术之中还能赚些钱已经是莫大的宽慰了。这是我的新作品 言叶,假如你热爱写作那你一定不能错过。上述提到的各种技术和框架,基本都在这个项目中有具体的实践,可以下载体验下,是不是足够顺滑。

最后,不论你对技术还是产品感兴趣,关注我哦~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容