Jetpack Compse 实战 —— 全新的开发体验
公众号回复
compose
获取安装包项目地址: wanandroid-compose
经过前段时间的 android dev summit ,相信你已经大概了解了 jetpack compose 。如果你还没有听说过,可以阅读这篇文章 jetpack compose 最新进展 。总而言之,compose 是一个 颠覆性 的 声明式 ui 框架 ,它的口号就是 消灭 xml 文件 !
尽管 jetpack compose 还只是预览版,api 可能发生变化,缺乏足够的控件支持,甚至不是那么稳定,但这阻止不了我这颗好奇的心。我在第一时间就上手撸了一款 compose 版本 wanandroid 应用,功能也比较简单,仅仅包括首页,广告和最新项目,类似于 android 原生页面的 viewpager + tablayout
。下面的 gif 展示了应用的基本页面:
可以看出来页面并不是那么流畅,view 的复用应该是个问题,甚至我也没发现应该怎么做下拉刷新。那么,compose 给我们带来了什么呢?在解答这个问题之前,我想先来说说 android 应用架构问题。
荒芜年代 —— mvc
在我刚入行的时候,可以说是 android 开发的黄金时代,也可以说是开发者的荒芜时代。一方面,毫不夸张的说,基本会写 xml 都能谋得一份工作。另一方面,对于开发者来说,远远没有现在的规范的开发架构。没记错的话,我当年的主力开发框架是 xutils 2
,一个类库大包干,从 布局和 id 绑定,网络请求,到图片展示,orm 操作,一应俱全。当时的 布局和 id 绑定还是运行时反射,而不是编译期注解。很长一段时间以来,android 连一个官方的网络库都没有。
在架构方面,很多人都是一个 activity 撸到死,我真的见过上千行zouguolai的 mainactivity 。并且觉得这就是 mvc 架构,实体类 entity 就是 model
层,activity/fragment
就是 controller
层,布局文件就是 view
层。
但这当真是 mvc
吗?其实并不是。不管是 mvc
, mvp
,还是 mvvm
,都应该遵循一个最起码的原则,表现层和业务层分离 ,也就是 android 官网给出的 中强调的 分离关注点 。activity/fragment
既要承担视图层的任务,展示和更新 ui,又要处理业务层逻辑,获取数据等。这并不符合架构设计的基本原则。
正确的 mvc 模式中,model 层不仅包含实体类 entity,更重要的作用是处理业务逻辑。view 层负责处理视图逻辑。而 controller 就是 model 和 view 之间的桥梁。桥怎么建,其实并没有标准,根据你自己的需求就可以了。
引用一张阮一峰老师的图,大致是这么个意思,但是也不一定就完全都是单向依赖。
从荒芜时代走过来,mvc 总算有点分层的味道在里面了,分离了视图层和业务层。但是 view 层和 model 层的依赖关系,造成代码耦合,终将导致 activity 日益臃肿。那么有没有办法将 view 层和 model 层彻底分离,做到视图层和模型层完全分离呢? mvp 就应运而生了。
青铜年代 —— mvp
依旧是阮一峰老师的图片:
相较于 mvc ,mvp 用 presenter 层
代替了 controller 层
,且 view 层
和 model 层
完全分离,依靠 presenter 进行通信 。
想象一个获取用户信息的场景。iview
接口中定义了一系列视图层接口 ,view 层(activity)实现 iview
接口中相应视图逻辑。 view 层通过持有的 presenter 处理业务逻辑,即请求用户信息。一般情况下,presenter 也不直接处理业务逻辑,而是通过 model 层,例如数据仓库 repository, 来获取数据,避免 presenter 重蹈覆辙,日渐臃肿。同时,presenter 层也是持有 view 的,获取用户信息之后再转发给 view 。
总结一下,mvp 中 view 和 model 完全解耦,通过 presenter 通信。view 和 presenter 共同处理视图层逻辑,model 层负责业务逻辑。
在 github 上 android 官方的架构示例 中 mvp 作为主分支坚挺了很久。我最初也是根据这个官方示例改造了自己的 mvp 架构,并且使用了很长时间。但是 mvp 作为一款面向接口编程的架构,随着业务的复杂程度不断加大,有种遍地都是接口的既视感,实在显得有点繁琐。
另外一点,presenter 的职责边界不够清晰,它除了承担调用 model 层获取业务逻辑之外,还要控制 view 层处理 ui。用下面一段代码表示一下:
class loginpresenter(private val mview: logincontract.view) : logincontract.presenter { ...... override fun login(username: string, password: string) { coroutinescope(dispatchers.main).launch { val result = wanretrofitclient.service.login(username, password).await() with(result) { if (errorcode == -1) mview.loginerror(errormsg) else mview.login(data) } } } }
一旦 view 层发生任何变化,presenter 层也要做出相应改动。虽然 view 和 model 之间解耦了,但是 view 和 presenter 却耦合了。理想情况下,presenter 层应该仅负责数据的获取,view 层自动观察数据的变化。于是,mvvm 来了。
黄金时代 —— mvvm
google 官图镇楼 。
mvp 风光早已不在, android 官方的架构示例 的主分支已经切换到 mvvm 。在 android 的 mvvm 架构中,viewmodel 是重中之重,它一方面通过数据仓库 repository 获取数据,另一方面根据获取的数据更新 view 层的 activity/fragment。等等,这句话怎么听着这么耳熟,presenter 不也是干了这些事吗?的确,它们干的事情都差不多,但是实现上完全不一样。
以我的开源项目 wanandroid 中的 loginviewmodel
为例:
class loginviewmodel(val repository: loginrepository) : baseviewmodel() { private val _uistate = mutablelivedata<loginuimodel>() val uistate: livedata<loginuimodel> get() = _uistate fun logindatachanged(username: string, password: string) { emituistate(enableloginbutton = isinputvalid(username, password)) } // viewmodel 只处理视图逻辑,数据仓库 repository 负责业务逻辑 fun login(username: string, password: string) { viewmodelscope.launch(dispatchers.default) { if (username.isblank() || password.isblank()) return@launch withcontext(dispatchers.main) { showloading() } val result = repository.login(username, password) withcontext(dispatchers.main) { if (result is result.success) { emituistate(showsuccess = result.data,enableloginbutton = true) } else if (result is result.error) { emituistate(showerror = result.exception.message,enableloginbutton = true) } } } } private fun showloading() { emituistate(true) } private fun emituistate( showprogress: boolean = false, showerror: string? = null, showsuccess: user? = null, enableloginbutton: boolean = false, needlogin: boolean = false ) { val uimodel = loginuimodel(showprogress, showerror, showsuccess, enableloginbutton,needlogin) _uistate.value = uimodel } data class loginuimodel( val showprogress: boolean, val showerror: string?, val showsuccess: user?, val enableloginbutton: boolean, val needlogin:boolean ) }
可以看到,viewmodel 中是没有 view 的引用的,view 通过可观察的 livedata 来观察数据变化,基于观察者模式做到和 viewmodel 完全解耦。
数据驱动视图 ,这是 jetpack mvvm 推崇的一个重要原则。其基本数据流如下所示 :
- 数据层 repository 负责从不同数据源获取和整合数据,基本负责所有的业务逻辑
- viewmodel 持有 repository,获取数据并驱动 view 层更新
- view 持有 viewmodel,观察 livedata 携带的数据,数据驱动 ui
曾经和一些开发者讨论过这样一个问题,** 不使用 databinding 还算是 mvvm 吗 ?** 我认为 mvvm 的核心从来不在于 databinding 。databinding 只是可以帮助我们将 数据驱动视图 做到极致,顺便还可以双向绑定。
要说到对 jetpack mvvm 中最不满意的一块,那非 databinding 莫属了。在我狭隘的认为 databinding 就是一个在 xml 里面写逻辑代码的反人类的库时,我是坚决反对在任何项目中引入它的。固执己见的时候就容易走进误区,在阅读 kunminx 的 重学安卓:从 被反对 到 真香 的 jetpack databinding! 之后,正如这篇文章名字一样,真香。
香的确是香,一切能让我早下班的都是好东西。在我的某次提交日志上,我写下了 消灭 adapter 几个字,那时我刚用 databinding 消灭了大部分 recyclerview 的 adapter 。可是在提交之后,我的良心惴惴不安,我追究还是在 xml 文件里写逻辑代码了,难道这真的不反人类吗?
未来可期 —— jetpack compose
现在你应该可以理解我对 jetpack compose 的执念了。抛去其他特性,在我看来,它完美的解决了 数据驱动视图 的问题,我再也不需要使用 databinding 了。
简单代码展示一下 compose 的用法。下面的代码描绘的是首页 tab 下的文章列表。
@composable fun maintab(articleuimodel: articleviewmodel.articleuimodel?) { verticalscroller { flexcolumn { inflexible { heightspacer(height = 16.dp) } flexible(1f) { articleuimodel?.showsuccess?.datas?.foreach { articleitem(article = it) } articleuimodel?.showerror?.let { toast(app.context, it) } wenjian articleuimodel?.showloading?.let { progress() } } } } }
这种写法叫做 声明式编程 ,会用 flutter 的同学应该很熟悉。方法参数 articleuimodel
就是数据实体类,直接根据数据 articleuimodel 构建 ui 。说的大白话一点,就是给你长方形的长和宽了,让你画个长方形出来。最后加上 @compose
注解,就是一个可用的 ui 组件了。仔细看代码,里面还用了两个 ui 组件 ,articleitem
和 progress
,代码就不贴出来了。分别是文章列表的 item 项目 和加载进度条。
那么,数据如何更新呢?最简单的方式是使用 @model
注解。
@model data class articleuimodel(){ ...... }
对,就是这么简单。@model
注解会自动把你的数据类变成可观察对象,只要 articleuimodel 发生变化,ui 就会自动更新。
但是我在实际开发中结合 livedata 使用时,好像表现的不是那么正常。后来在 medium 上无意中看到了解决方案,针对 livedata 做了特殊处理 :
// general purpose observe effect. this will likely be provided by livedata. effect api for // compose will also simplify soon. fun <t> observe(data: livedata<t>) = effectof<t?> { val result = +state<t?> { data.value } val observer = +memo { observer<t> { result.value = it } } +oncommit(data) { data.observeforever(observer) ondispose { data.removeobserver(observer) } } result.value }wenjian
在 activity/fragment 中观测 livedata 即可:
class mainactivity : basevmactivity<articleviewmodel>() { override fun initview() { setcontent { +observe(mviewmodel.uistate) wanandroidapp(mviewmodel) } } override fun initdata() { mviewmodel.gethomearticlelist() } }
这样 view 层就可以自动观察 livedata 所包含的值了。
没有 xml,没有 databinding,一切看起来称心如意多了。但就是 ui 体验有那么一点糟心,你可以在公众号后台回复 compose 安装体验一下。由于还是早期的预览版,这也是可以理解的。我相信,等到发布 release 版本的时候,一定足以完全代替原声的 view 体系。
本文并没有详细介绍 jetpack compose 的详细使用过程和其他特性,更多信息我推荐下面两篇文章:
最后
正如 android 官网 jetpack 介绍页所说,jetpack 可以帮助开发者更轻松的编写优质应用。的确,随着应用架构的规范,我们只需要把精力放在需要的代码上,加速开发,消除样板代码,减少崩溃和内存泄露,构建高质量的强大应用。我想不出来有任何理由不使用 jetpack 来构建你的应用。而 compose 必将称为 jetpack 中极其重要的一块拼图。
jetpack compse ,未来可期 !
添加我的微信,加入技术交流群。
公众号后台回复 “compose”, 获取最新安装包。
上一篇: 使用zrender.js绘制体温单(2)
下一篇: 魏永明: MiniGUI的涅槃重生之路