Android View Component 架构设计思维
重构记事
为什么要重构?
- 项目当前采用的DataBinding框架严重限制了编译速度,并且DataBinding框架存在着出错提示混乱的毛病,在出错的时候大幅度降低了开发效率(当然没错的时候还是很快的)
- 在尝试为Freeline适配最新的DataBinding时候遇到了巨大的阻力,实现的可能性很低了,只能做到局部兼容,因此需要多长全量编译,开发效率低下
- 为Freeline适配kotlin增量成功,因此开始使用kotlin语言开发(kapt不敢用除外),准备大规模迁移至kotlin开发语言
- 一些之前的逻辑存在着混乱的毛病,模块间耦合关系有待进一步梳理
做什么?
- 使用自己的观察者框架代替Google自带的DataBinding实现数据流
- 使用kotlin写重构代码,并且局部替换Java代码
- 去除一些不痛不痒的注解处理框架,在大幅度应用之前去除AROUTER,Butterknife
先思考 => 什么架构
我应该用什么架构 MVP MVVM ?
- MVP 作为android应用很火的架构,因为充分的解耦被业界广泛使用,蛋疼之处在于需要些大量的接口来规范每一层的行为,来进行进一步的解耦。接口也可以被用于单元测试,目前的项目中还并没有足够的精力去写单元测试,也不存在替换model或其他某层的需求,因此可以使用只抽象View接口版的MVP架构(如果你有MVP情节的话)
- MVVM架构随着DataBinding架构的提出而在android上被慢慢推广,ViewModel作为数据渲染层,承接着讲model渲染到view上的重任,同时使用数据绑定的方式将其与view相关联,MVVM的设计原则是ViewModel层不持有View的引用,加之DataBinding功能有限和某些部分及其蛋疼,可以做到高效开发但是某些时候及其蛋疼,当然我个人而言是非常喜欢MVVM架构以及数据绑定思维的,所以它也是重构前微北洋(我的项目名字)主模块的架构
那么两种架构都有自己蛋疼的地方,可不可以有一种新的架构设计方式呢
有!
前些时间接触了React设计思维,React框架将各个UI组件称为Component
,每个Component内部维护了自己的view和一些逻辑,并且将状态而非是view暴露在外,通过改变Component的状态来实现这个组件UI和其他细节的改变,Component暴露在外的状态是可以确定全局状态的最小状态,即状态的最小集,Component的其他剩余状态都可以通过暴露状态的改变所推倒出来,当然这个推倒应该是响应式的,自动的。
当然android上无法写类似于JSX的东西,如果最类似的话,那就是Anko的DSL布局框架了,这样子就可以将view写在Component里面。
不过View写在Xml里面,然后在Component的初始化时候来find来那些view去操作也是ok的(因为anko的DSL写法依然是一种富有争议的布局方式,尽管我定制的Freeline已经可以做到kotlin修改的10s内增量编译,DSL还是有很多坑)
说了这么多,这个架构到底具体是什么样子呢?
- 所有的view组件抽象成
Component
- 每个
Component
内维护自己的view,对外暴露可以推倒出全局状态的最小状态集,view均为private,不可被外部访问到,只可以修改Component的状态而不可访问component的view -
Component
内部维护自己view与状态之间的关系,推荐使用响应式数据流的方式来进行绑定,某些数据发生变化的时候对应的view也发生自己的改变
可见,Component本身是高内聚的,对外暴露最小状态,所以外部只需修改最小的状态(集)就可以完成Component行为/view的变化,因此外部调用极其方便而且也不存在逻辑之间的相互干扰
怎么做?
-
Component
怎么分? -
Component
需要传入什么? -
Component
放在哪里? -
Component
内部数据流怎么写? -
Component
对外暴露什么?怎么暴露? -
Component
内部状态怎么管理?
先看一个图来解释
Screen Shot 2017-10-30 at 11.32.46 AM.png图示部分的页面,是使用Recyclerview作为页面容器,里面的每个Item,就可以作为一个Component来对待
Screen Shot 2017-10-30 at 11.34.29 AM.png进一步的,此Component里面的那几个图书详情item,又可以作为子Component来对待
Screen Shot 2017-10-30 at 11.36.31 AM.png他们的xml布局因为极其简单就跳过不谈,Component的设计部分我们可以从最小的item说起
因为它没有被放在Recyclerview里面,所以它继承ViewHolder与否都是随意的,但是为了统一性,就继承RecyclerView.ViewHolder
好了(事实上是否继承它都是随意的)
先来看这个Component对应的数据Model部分
public class Book {
/**
* barcode : TD002424561
* title : 设计心理学.2,与复杂共处,= Living with complexity
* author : (美) 唐纳德·A·诺曼著
* callno : TB47/N4(5) v.2
* local : 北洋园工学阅览区
* type : 中文普通书
* loanTime : 2017-01-09
* returnTime : 2017-03-23
*/
public String barcode;
public String title;
public String author;
public String callno;
public String local;
public String type;
public String loanTime;
public String returnTime;
/**
* 距离还书日期还有多少天
* @return
*/
public int timeLeft(){
return BookTimeHelper.getBetweenDays(returnTime);
// return 20;
}
/**
* 看是否超过还书日期
* @return
*/
public boolean isOverTime(){
return this.timeLeft() < 0;
}
public boolean willBeOver(){
return this.timeLeft() < 7 && !isOverTime();
}
}
我们的需求是:在这个view里面有 书的名字,应还日期,书本icon的涂色方案随着距离还书日期的长短而变色
首先声明用到的view和Context什么的
class BookItemComponent(lifecycleOwner: LifecycleOwner,itemView: View) : RecyclerView.ViewHolder(itemView) {
private val mContext = itemView.context
private val cover: ImageView = itemView.findViewById(R.id.ic_item_book)
private val name: TextView = itemView.findViewById(R.id.tv_item_book_name)
private val returntimeText: TextView = itemView.findViewById(R.id.tv_item_book_return)
}
LifecycleOwner是来自Android Architecture Components的组件,用来管理android生命周期用,避免组件的内存泄漏问题 Android Architecture Components
下来就是声明可观察的数据(也可以成为状态)
private val bookData = MutableLiveData<Book>()
因为此Component逻辑简单,只需要观测Book类即可推断确定其状态,因此它也是这个Component的最小状态集合
插播一条补充知识:
LiveData<T>
,MutableLiveData<T>
也都来自于Android Architecture Components的组件,是生命周期感知的可观测动态数据组件Sample:
LiveData<BigDecimal> myPriceListener = ...; myPriceListener.observe(this, price -> { // Update the UI. });
当然用kotlin给它写了一个简单的函数式拓展
/** * LiveData 自动绑定的kotlin拓展 再也不同手动指定重载了hhh */ fun <T> LiveData<T>.bind(lifecycleOwner: LifecycleOwner, block : (T?) -> Unit) { this.observe(lifecycleOwner,android.arch.lifecycle.Observer<T>{ block(it) }) }
好了,回到正题,然后我们就该把view和Component的可观测数据/状态绑定起来了
init {
bookData.bind(lifecycleOwner) {
it?.apply {
name.text = this.title
setBookCoverDrawable(book = this)
returntimeText.text = "应还日期: ${this.returnTime}"
}
}
}
//这里是刚刚调用的函数 写了写动态涂色的细节
private fun setBookCoverDrawable(book: Book) {
var drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_book)
val leftDays = book.timeLeft()
when {
leftDays > 20 -> DrawableCompat.setTint(drawable, Color.rgb(0, 167, 224)) //blue
leftDays > 10 -> DrawableCompat.setTint(drawable, Color.rgb(42, 160, 74)) //green
leftDays > 0 -> {
if (leftDays < 5) {
val act = mContext as? Activity
act?.apply {
Alerter.create(this)
.setTitle("还书提醒")
.setBackgroundColor(R.color.assist_color_2)
.setText(book.title + "剩余时间不足5天,请尽快还书")
.show()
}
}
DrawableCompat.setTint(drawable, Color.rgb(160, 42, 42)) //red
}
else -> drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_warning)
}
cover.setImageDrawable(drawable)
}
通过观测LiveData<Book>
来实现Component状态的改变,因此只需要修改Book就可以实现该Component的相关一系列改变
然后我们只需要把相关函数暴露出来
fun render(): View = itemView
fun bindBook(book: Book){
bookData.value = book
}
然后在需要的时候创建调用它就可以了
val view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
val bookItem = BookItemComponent(life cycleOwner = lifecycleOwner, itemView = view)
bookItem.bindBook(it)
bookItemViewContainer.add(view)
来点复杂的?
来看主页的图书馆模块
Screen Shot 2017-10-30 at 11.34.29 AM.png
图书馆模块本身也是一个Component。
需求:第二行的图标在刷新的时候显示progressbar,刷新成功后显示imageview(对勾),刷新错误的时候imageview显示错误的的图片
-
这个Item要放在Recyclerview里面,所以要继承ViewHolder
class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) { }
-
声明该Component里面用到的view
class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) { private val context = itemView.context private val stateImage: ImageView = itemView.findViewById(R.id.ic_home_lib_state) private val stateProgressBar: ProgressBar = itemView.findViewById(R.id.progress_home_lib_state) private val stateMessage: TextView = itemView.findViewById(R.id.tv_home_lib_state) private val bookContainer: LinearLayout = itemView.findViewById(R.id.ll_home_lib_books) private val refreshBtn: Button = itemView.findViewById(R.id.btn_home_lib_refresh) private val renewBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_renew) private val loadMoreBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_more)
-
声明Component里面的可观测数据流
private val loadMoreBtnText = MutableLiveData<String>() private val loadingState = MutableLiveData<Int>() private val message = MutableLiveData<String>() private var isExpanded = false
-
声明一些其他的用到的东西
//对应barcode和book做查询 private val bookHashMap = HashMap<String, Book>() private val bookItemViewContainer = mutableListOf<View>() //缓存的LinearLayout里面的view 折叠提高效率用 private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java)
-
建立绑定关系
init { //这里bind一下 解个耦 message.bind(lifecycleOwner) { message -> stateMessage.text = message } loadingState.bind(lifecycleOwner) { state -> when (state) { PROGRESSING -> { stateImage.visibility = View.INVISIBLE stateProgressBar.visibility = View.VISIBLE message.value = "正在刷新" } OK -> { stateImage.visibility = View.VISIBLE stateProgressBar.visibility = View.INVISIBLE Glide.with(context).load(R.drawable.lib_ok).into(stateImage) } WARNING -> { stateImage.visibility = View.VISIBLE stateProgressBar.visibility = View.INVISIBLE Glide.with(context).load(R.drawable.lib_warning).into(stateImage) } } } loadMoreBtnText.bind(lifecycleOwner) { loadMoreBooksBtn.text = it if (it == NO_MORE_BOOKS) { loadMoreBooksBtn.isEnabled = false } } }
-
再写一个OnBindViewHolder的回调(到时候手动调用就可以了,会考虑使用接口规范这部分内容)
fun onBind() { refreshBtn.setOnClickListener { refresh(true) } refresh() renewBooksBtn.setOnClickListener { renewBooksClick() } loadMoreBooksBtn.setOnClickListener { view: View -> if (isExpanded) { // LinearLayout remove的时候会数组顺延 所以要从后往前遍历 (bookContainer.childCount - 1 downTo 0) .filter { it >= 3 } .forEach { bookContainer.removeViewAt(it) } loadMoreBtnText.value = "显示剩余(${bookItemViewContainer.size - 3})" isExpanded = false } else { (0 until bookItemViewContainer.size) .filter { it >= 3 } .forEach { bookContainer.addView(bookItemViewContainer[it]) } loadMoreBtnText.value = "折叠显示" isExpanded = true } } }
-
剩下的就是方法的具体实现了这个看个人喜欢的处理方式来处理,比如说我喜欢协程处理网络请求,然后用LiveData处理多种请求的映射
比如说一个简单的网络请求以及缓存的封装
object LibRepository { private const val USER_INFO = "LIB_USER_INFO" private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java) fun getUserInfo(refresh: Boolean = false): LiveData<Info> { val livedata = MutableLiveData<Info>() async(UI) { if (!refresh) { val cacheData: Info? = bg { Hawk.get<Info>(USER_INFO) }.await() cacheData?.let { livedata.value = it } } val networkData: Info? = bg { libApi.libUserInfo.map { it.data }.toBlocking().first() }.await() networkData?.let { livedata.value = it bg { Hawk.put(USER_INFO, networkData) } } } return livedata } }
8.与其他Component的组合
使用简单的方法即可相互集成,传入inflate好的view和对应的LifecycleOwener即可
data?.books?.forEach {
bookHashMap[it.barcode] = it
val view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
val bookItem = BookItemComponent(lifecycleOwner = lifecycleOwner, itemView = view)
bookItem.bindBook(it)
bookItemViewContainer.add(view)
}
小总结:状态绑定,数据观测
在图书馆的这个Component的开发中,只需要在发起各种任务以及处理任务返回信息的时候,改变相关的状态值和可观测数据流即可,便可实现Component一系列状态的改变,因为所有逻辑不依赖外部,所有目前该Component不对外暴露任何状态和view。实现了模块内的数据流和高内聚。
模块内数据流可以大幅度简化代码,避免某种程度上对view直接操作所造成的混乱,例如异常处理方法
private fun handleException(throwable: Throwable?) {
//错误处理时候的卡片显示状况
throwable?.let {
Logger.e(throwable, "主页图书馆模块错误")
when (throwable) {
is HttpException -> {
try {
val errorJson = throwable.response().errorBody()!!.string()
val errJsonObject = JSONObject(errorJson)
val errcode = errJsonObject.getInt("error_code")
val errmessage = errJsonObject.getString("message")
loadingState.value = WARNING
message.value = errmessage
} catch (e: IOException) {
e.printStackTrace()
} catch (e: JSONException) {
e.printStackTrace()
}
}
is SocketTimeoutException -> {
loadingState.value = WARNING
this.message.value = "网络超时...很绝望"
}
else -> {
loadingState.value = WARNING
this.message.value = "粗线蜜汁错误"
}
}
}
}
在收到相关错误码的时候,修改state和message的观测值,相关的数据流会根据最初的绑定关系自动通知到相关的view
比如说loadingstate的观测:
loadingState.bind(lifecycleOwner) { state ->
when (state) {
PROGRESSING -> {
stateImage.visibility = View.INVISIBLE
stateProgressBar.visibility = View.VISIBLE
message.value = "正在刷新"
}
OK -> {
stateImage.visibility = View.VISIBLE
stateProgressBar.visibility = View.INVISIBLE
Glide.with(context).load(R.drawable.lib_ok).into(stateImage)
}
WARNING -> {
stateImage.visibility = View.VISIBLE
stateProgressBar.visibility = View.INVISIBLE
Glide.with(context).load(R.drawable.lib_warning).into(stateImage)
}
}
}
最近更新的
这个架构比较适合的场景就是,多个业务模块作为Card出现的时候。(或者说是Feed流里面的item,或者是你喜欢使用Recyclerview作为页面组件的容器)等等... 对于单页场景,其实一页就可以认为是一个Component,在页面的内部管理可观察数据流即可。
架构不是死的,思维也不是。大家还是要根据自己的业务场景适当发挥啊~
网友评论