本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
杂乱度
Android 架构演进系列是围绕着杂乱度向前推动的。
软件的首要技术任务是“办理杂乱度” —— 《代码大全》
由于低杂乱度才能降低了解成本和沟通难度,提高应对改变的灵活性,削减重复劳动,终究提高代码质量。
架构的目的在于“将杂乱度分层”
杂乱度为什么要被分层?
若不分层,杂乱度会在同一层次展开,这样就太 … 杂乱了。
举一个杂乱度不分层的比方:
小李:“你会做什么菜?”
小明:“我会做用土鸡生的土鸡蛋配上切片的西红柿,放点油盐,开战翻炒的西红柿炒蛋。”
听了小明的答复,你还会和他做朋友吗?
小明把不同层次的杂乱度以不恰当的办法搓弄在一同,让人感觉是一种由“没有必要的具体”导致的“难以了解的杂乱”。
小李其实并不关怀土鸡蛋的来源、西红柿的切法、增加的佐料、以及烹饪办法。
这样的答复除了难以了解之外,局限性也很大。由于它太具体了!只要把土鸡蛋换成洋鸡蛋、或是西红柿片换成块、或是加点糖、或是换成电磁炉,其间任一要素产生改变,小明就不会做西红柿炒蛋了。
再举个正面的比方,TCP/IP 协议分层模型自下到上定义了五层:
- 物理层
- 数据链路成
- 网络层
- 传输层
- 应用层
其间每一层的功用都独立且明确,这样规划的优点是缩小影响面,即单层的变动不会影响其他层。
这样规划的另一个优点是当专心于一层协议时,其余层的技术细节能够不予重视,同一时间只需求重视有限的杂乱度,比方传输层不需求知道自己传输的是 HTTP 仍是 FTP,传输层只需求专心于端到端的传输办法,是树立衔接,仍是无衔接。
有限杂乱度的另一面是“基层的可重用性”。当应用层的协议从 HTTP 换成 FTP 时,其基层的内容不需求做任何更改。
引子
该系列的前三篇结合“查找”这个事务场景,叙述了不运用架构写事务代码会产生的痛点:
- 低内聚高耦合的制作:控件的制作逻辑散落在各处,散落在各种 Activity 的子程序中(子程序间相互耦合),涣散在现在和将来的逻辑中。这样的规划增加了界面改写的杂乱度,导致代码难以了解、容易改出 Bug、难排查问题、无法复用。
- 耦合的非粘性通讯:Activity 和 Fragment 经过获取对方引用并互调办法的办法完结通讯。这种通讯办法使得 Fragment 和 Activity 耦合,然后降低了界面的复用度。并且没有一种内建的机制来轻松的完成粘性通讯。
- 天主类:一切细节都在界面被铺开。比方数据存取,网络拜访这些和界面无关的细节都在 Activity 被铺开。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改变源不单一、改动影响规模大。
- 界面 & 事务:界面展现和事务逻辑耦合在一同。“界面该长什么样?”和“哪些事情会触发界面重绘?”这两个独立的改变源没有做到重视点别离。导致 Activity 代码不单纯、高耦合、代码量大、杂乱度高、改变源不单一、改动影响规模大、易改出 Bug、界面和事务无法独自被复用。
具体分析进程能够点击下面的链接:
-
写事务不必架构会怎么样?(一)
-
写事务不必架构会怎么样?(二)
-
写事务不必架构会怎么样?(三)
紧接着又用了三篇叙述了怎么运用 MVP 架构对该事务场景的重构进程。MVP 确实处理了一些问题,但也引进了新问题:
- 分层:MVP 最大的贡献在于将界面制作与事务逻辑分层,前者是 MVP 中的 V(View),后者是 MVP 中的 P(Presenter)。分层完成了事务逻辑和界面制作的解耦,让各自愈加单纯,降低了代码杂乱度。
- 面向接口通讯:MVP 将事务和界面分层之后,各层之间就需求通讯。通讯经过接口完成,接口把做什么和怎么做别离,使得重视点别离成为或许:接口的持有者只关怀做什么,而怎么做留给接口的完成者关怀。界面经过事务接口向 Presenter 发出恳求以触发事务逻辑,这使得它不需求关怀事务逻辑的完成细节。Presenter 经过 view 层接口回来响应以辅导界面改写,这使得它不需求关怀界面制作的细节。
- 有限的解耦:由于 View 层接口的存在,迫使 Presenter 得了解该把哪个数据塞给哪个 View 层接口。这是一种耦合,Presenter 和这个具体的 View 层接口耦合,较难复用于其他事务。
- 有限内聚的界面制作:MVP 并未向界面供给仅有 Model,而是将描绘一个完整界面的 Model 涣散在若干 View 层接口回调中。这使得界面的制作无法内聚到一点,增加了界面制作逻辑保护的杂乱度。
- 困难重重的复用:理论上,界面和事务分层之后,各自都愈加单纯,为复用供给了或许性。但不管是事务接口的复用,仍是View层接口的复用都相当别扭。
- Presenter 与界面共存亡:这个特性使得 MVP 无法应对反正屏切换的场景。
- 无内建跨界面(粘性)通讯机制:MVP 无法高雅地完成跨界面通讯,也未内建粘性通讯机制,得借助第三方库完成。
- 生命周期不友好:MVP 并未内建生命周期办理机制,易造成内存走漏、crash、资源浪费。
具体分析进程能够点击下面的链接:
-
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(一)
-
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(二)
-
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(三)
从这一篇开始,试着引进 MVVM 架构的思维进行查找事务场景的重构,看看是否能处理一些痛点。
在重构之前,再介绍下查找的事务场景,该功用示意图如下:
事务流程如下:在查找条中输入关键词并同步展现联想词,点联想词跳转查找成果页,若无匹配成果则展现引荐流,回来时查找前史以标签方法横向铺开。点击前史可直接建议查找跳转到成果页。
将查找事务场景的界面做了如下规划:
查找页用Activity
来承载,它被分成两个部分,头部是常驻在 Activity 的查找条。下面的“查找体”用Fragment
承载,它或许出现三种状况 1.查找前史页 2.查找联想页 3.查找成果页。
Fragment 之间的切换选用 Jetpack 的Navigation
。关于 Navigation 具体的介绍能够点击Navigation 组件运用入门 | Android 开发者 | Android Developers
有免死金牌的事务层
在运用 MVP 重构查找事务时,存在“Presenter 与界面共存亡”的问题,即 Presenter 在 Activity 实例内部构建,遂其生命周期与 Activity 同步。当 Activity 毁掉重建时,Presenter 也跟着一同毁掉重建。当 Presenter 初始化时存在耗时操作时,这样的从头来过就很浪费资源。(具体分析能够点击MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?)
MVVM 中的 VM 即ViewModel
,它是与 MVP 中 Presenter 相对应的概念,即事务逻辑层(它在此基础上又拓展出新的作用),它的引进处理了这个痛点。
ViewModel 是 JetPack 供给的一个类:
public abstract class ViewModel {
/**
* Construct a new ViewModel instance.
* You should never manually construct a ViewModel outside of a
* {@link ViewModelProvider.Factory}.
*/
public ViewModel() {}
}
ViewModel 虽然供给了公有的结构办法,但注解提示说“永久不要手动构建 ViewModel 实例,而是得经过ViewModelProvider.Factory
:
public interface Factory {
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}
Factory 是一个接口,是对怎么构建 ViewModel 的一个笼统。
之所以不允许直接构建而是必须经过 Factory,是由于体系期望掌控 ViewModel 的实例构建,在内部协助开发者构建 ViewModel 实例。若把 ViewModel 的构建办法铺开,则上层或许出现各式各样自定义的构建办法(比方在结构办法中出入不同的参数)。
那为啥体系要掌控 ViewModel 实例的构建?
由于体系对 ViewModel 实例的存取做了特别处理。
ViewModel 通常是这样声明的:
class SearchViewModel(
private val repository: SearchRepository
): ViewModel() { }
class SearchFactory(val repository: SearchRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SearchViewModel(searchRepository) as T
}
}
其间的 Repository 是对拜访数据的封装,比方网络恳求,关于它的具体解释能够点击MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?
而 ViewModel 通常在 Activity 中这样被构建:
class TemplateSearchActivity : AppCompatActivity() {
private val searchViewModel by lazy {
ViewModelProvider(
this,
SearchFactory(SearchRepository())).get(SearchViewModel::class.java)
}
}
构建 Presenter 是直接在 Activity 中 new,而构建 ViewModel 是经过ViewModelProvider().get()
:
public class ViewModelProvider {
// ViewModel 实例商铺
private final ViewModelStore mViewModelStore;
private Factory mFactory;
public <T extends ViewModel> T get(String key, Class<T> modelClass) {
// 从商铺获取 ViewModel实例
ViewModel viewModel = mViewModelStore.get(key);
// 若 ViewModel 匹配指定类型则直接回来
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
...
// 若商铺无 ViewModel 实例 则经过 Factory 构建
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
// 将 ViewModel 实例存入商铺
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
}
ViewModelProvider
是一个获取 ViewModel 实例的工具类,它屏蔽了经过拜访ViewModelStore
获取 ViewModel 实例的细节。
ViewModelStore 是真正存在 ViewModel 实例的当地:
// ViewModel 实例商铺
public class ViewModelStore {
// 存储 ViewModel 实例的 Map
private final HashMap<String, ViewModel> mMap = new HashMap<>();
// 存
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
// 取
final ViewModel get(String key) {
return mMap.get(key);
}
...
}
ViewModelStore
内部持有一个 HashMap,这是 ViewModel 实例的终究存放点。
而 ViewModelStore 的实例是经过ViewModelStoreOwner
获取:
public class ViewModelProvider {
// ViewModel 实例商铺
private final ViewModelStore mViewModelStore;
// 结构 ViewModelProvider 时需传入 ViewModelStoreOwner 实例
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
// 经过 ViewModelStoreOwner 获取 ViewModelStore
this(owner.getViewModelStore(), factory);
}
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
}
那ViewModelStoreOwner
实例又存储在哪?
// Activity 基类完成了 ViewModelStoreOwner 接口
public class ComponentActivity
extends androidx.core.app.ComponentActivity
implements LifecycleOwner, ViewModelStoreOwner{
// Activity 持有 ViewModelStore 实例
private ViewModelStore mViewModelStore;
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 获取装备无关实例
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// 从装备无关实例中康复 ViewModel商铺
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
// 静态的装备无关实例
static final class NonConfigurationInstances {
// 持有 ViewModel 商铺实例
ViewModelStore viewModelStore;
...
}
}
Activity 就是ViewModelStoreOwner
实例,且持有ViewModelStore
实例。
但当反正屏切换 Activity 毁掉重建时,作为成员变量的 ViewModelStore 仍然会被毁掉,为了避免它被重建,在装备产生改变时 onRetainNonConfigurationInstance(),ViewModelStore 实例还会被寄存在一个静态类NonConfigurationInstances
中,这样在康复时,就能够从中康复 ViewModelStore 实例。
终究的持有链如下:
Activity {
ViewModelStore {
HashMap<String, ViewModel>
}
NonConfigurationInstances { ViewModelStore }
}
阶段性总结:
- ViewModel 的构建被笼统为
ViewModelProvider.Factory
。- ViewModel 的实例被存储在
ViewModelStore
中的 HashMap 结构中 ViewModelStore 的构建被笼统为ViewModelStoreOwner
,Activity 和 Fragment 都完成了该接口且持有了 ViewModelStore 的实例,在装备产生改变时 onRetainNonConfigurationInstance(),ViewModelStore 实例还会被寄存在一个静态类NonConfigurationInstances
中,这样在康复时,就能够从中康复 ViewModelStore 实例。- ViewModel 的获取经过 ViewModelProvider 完成,它屏蔽了经过 ViewModelProvider.Factory 构建以及经过 ViewModelStore 缓存 ViewModel 实例的细节。
- ViewModel 的生命周期会在 Activity.onDestroy() 结束。此刻 Activity 会整理其持有的 ViewModelStore 中的一切 ViewModel 实例。
- 这套存储机制使得 ViewModel 在 Activity 装备产生改变被毁掉重建时获得了免死金牌,以确保不会从头触发事务逻辑。
数据持有者 & 数据驱动
假定 Presenter 也套用 ViewModel 这套构建机制,是否就能处理反正屏场景下的一切问题?
不能!
处理反正屏问题需求做到两点:
- 比 Activity 生命周期更长的事务逻辑层。
- 事务逻辑层持有数据并且具备数据重放能力。
即使 Presenter 做到了更长的生命周期也仅仅处理了第一个问题。由于 Presenter 它不是一个数据持有者,更别提数据重放了。
引用上一篇关于 MVP 数据流动的示意图:
Presenter 只持有了 Repository,它并不持有数据,即不存在一个叫 data 的成员变量。从 Repository 获取的数据是直接在事务接口中传递给了 View 层接口的。
也就是说,当触发了一个事务动作后,数据产生了一次从 Repository 到 Presenter 再到界面的流动。整个流动的进程中并没有一个当地把数据存下来。所以 Presenter 不是一个数据持有者。
已然 Presenter 不持有数据,那它也无法把前次流过的数据进行重放,即从头发送给界面。那在 MVP 架构中,当 Activity 毁掉重建时,怎么康复界面刚才的样子?答案是“无法康复!”,只能从头触发一遍事务动作,比方从头恳求网络,一切从头再来!
ViewModel 的出现一起把上述两个问题都处理了,总结为一句话便是“ViewModel 是生命周期更长的数据持有者。”
ViewModel 借助于LiveData
的协助完成了数据持有者的效果。
LiveData 也是 JetPack 的一员。它是能感知生命周期的,可调查的,粘性的,数据持有者。LiveData 用于以“数据驱动”办法更新界面。
关于 LiveData 的具体讲解能够点击LiveData 面试题库、回答、源码分析
在 MVP 架构中界面的改写是指令式编程,即界面改写是经过手动调用办法完成的。
指令式编程会产生耦合。
手动调用某个办法的条件是得先获取对应的目标,在 MVP 架构中,描绘界面怎么制作的目标叫“View 层接口”,Presenter 得先持有 View 层接口的实例,然后在内部根据事务逻辑手动调用相应的 View 层接口,即 Presenter 得知道要把哪个数据塞给哪个 View 层接口。这使得 Presenter 和 View 层接口耦合,或者说事务和界面耦合。耦合导致复用困难。具体分析能够点击MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(三)
愈加解耦的办法是数据驱动:让事务层只操纵数据,界面经过调查数据的办法完成改写。
这儿的数据指的是 MXX 架构中的 M,即 Model。
如此一来事务层不再持有任何和界面相关的东西,只和数据有关。不同的界面能够以任何喜爱的办法组合运用事务层供给的数据。(MVP 做不到这点,由于数据是经过 View 层接口给出去,组合运用略困难)
面向事务笼统Model
下面就以查找条为例,看看用 MVVM 架构重构之后会是什么样子。
查找条的事务场景如下:
当输入框键入内容后,显现X按钮并高亮查找按钮。点击查找跳转到查找成果页,一起查找条拉长并躲藏查找按钮。点击X时清空输入框并从查找成果页回来,查找条还原。
根据事务逻辑为 ViewModel 增加一系列动作及数据:
class SearchViewModel(private val searchRepository: SearchRepository) : ViewModel() {
// 事务数据持有者
val initLiveData = MutableLiveData<Boolean>()
val keywordLiveData = MutableLiveData<String>()
val searchLiveData = MutableLiveData<String>()
val clearLiveData = MutableLiveData<Boolean>()
val backToHistoryLiveData = MutableLiveData<Boolean>()
// 事务动作
fun init() { initLiveData.value = true }
fun search(keyword: String) { searchLiveData.value = keyword }
fun clear() { clearLiveData.value = true }
fun input(keyword: String) { keywordLiveData.value = keyword }
fun backToHistory(){ backToHistoryLiveData.value = true }
}
每一个函数代表着一个事务逻辑,并有与之对应的一个事务数据(以 LiveData 方法表达)
界面经过调查数据来更新视图:
class TemplateSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
searchViewModel.init()
observerData()
initView()
}
// 调查数据并改写界面
private fun observerData() {
searchViewModel.initLiveData.observe(this, Observer {
if (it) {
tvSearch.apply {
isEnabled = false
textColor = "#484951"
}
ivClear.visibility = gone
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
})
searchViewModel.searchLiveData.observe(this, Observer {
searchAndHideKeyboard(it)
})
searchViewModel.keywordLiveData.observe(this, Observer {
if (it.isNotEmpty()) {
tvSearch.textColor = "#F2F4FF"
tvSearch.isEnabled = true
ivClear.visibility = visible
} else {
tvSearch.textColor = "#484951"
tvSearch.isEnabled = false
ivClear.visibility = gone
}
})
searchViewModel.clearLiveData.observe(this, Observer {
if(it){
etSearch.text = null
etSearch.requestFocus()
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
backToHistory()
}
})
searchViewModel.backToHistoryLiveData.observe(this, Observer {
backToHistory()
})
}
private fun initView() {
etSearch.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?,s: Int, c: Int, a: Int) {}
override fun onTextChanged(char: CharSequence?, s: Int, b: Int, c: Int) {
val input = char?.toString() ?: ""
// 向 ViewModel 建议事务动作
searchViewModel.input(input)
}
override fun afterTextChanged(s: Editable?) {}
})
etSearch.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val input = etSearch?.text?.toString() ?: ""
if (input.isNotEmpty()) {
// 向 ViewModel 建议事务动作
searchViewModel.search(input)
}
true
} else false
}
tvSearch.setOnClickListener {
// 向 ViewModel 建议事务动作
searchViewModel.search(etSearch.text.toString())
}
ivClear.setOnClickListener {
// 向 ViewModel 建议事务动作
searchViewModel.clear()
}
etSearch.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
// 向 ViewModel 建议事务动作
searchViewModel.backToHistory()
}
false
}
}
}
这样的写法颇有脱裤子放屁的感觉,还引进了额外的杂乱度 ViewModel。但这裤子脱得仍是有价值的:
- 事务逻辑和界面展现别离:这使得界面展现和事务逻辑能够独立的改变而不会相互影响。(这一点MVP也能够做到)
- 更新视图的逻辑不再散落各处:界面经过调查数据较为集中地进行更新。但遗憾的是,同一个视图的更新逻辑仍是会散落在不同数据的调查者中。
现在看来和上一篇用 MVP 重构的效果没任何两样,反而由于引进了 ViewModel 和 LiveData 增加了杂乱度。
MVVM 的优点当然不止于此,后续章节会慢慢展开。(这一末节仅仅先展现 MVVM 的概貌)
面向界面笼统Model
用一张图来表达下上一末节 MVVM 的杂乱度:
它完结了界面展现与事务逻辑别离,但控件的改写逻辑散落在不同数据的调查者中,仍然无法将“界面应该长什么样子?”这个问题内聚于一点。
之所以会这样是由于“过错的 Model 笼统”。
上述代码是以事务逻辑作为笼统 Model 的依据。比方与“回来前史页”对应的数据是一个布尔值,用来表示是否触发了回来。这使得 Model 和事务强绑定,事务一变,原先的数据就没用了。Model 应该和事务无关,Model 应该只表达界面该长成什么样子。
依照这个思路,MVVM 的 Model 应该做如下改造:
class SearchViewModel : ViewModel() {
// 查找按钮色彩
val searchButtonColorLiveData = MutableLiveData<String>()
// 查找按钮是否可点击
val searchButtonClickableLiveData = MutableLiveData<Boolean>()
// 查找按钮是否可见
val searchButtonVisibilityLiveData = MutableLiveData<Boolean>()
// 清除按钮是否显现
val clearButtonVisibilityLiveData = MutableLiveData<Boolean>()
// 查找条是否拉深
val searchBarStretchLiveData = MutableLiveData<Boolean>()
// 键盘是否展现
val keyboardLiveData = MutableLiveData<Boolean>()
// 跳转到查找成果页
val gotoSearchLiveData = MutableLiveData<String>()
// 关键词
val keywordLiveData = MutableLiveData<String>()
// 从成果页回来
val popupLiveData = MutableLiveData<Boolean>()
fun init() {
keyboardLiveData.value = true
searchButtonColorLiveData.value = "#484951"
searchButtonClickableLiveData.value = false
searchButtonVisibilityLiveData.value = true
searchBarStretchLiveData.value = false
}
fun search(keyword: String) {
gotoSearchLiveData.value = keyword
keyboardLiveData.value = false
searchBarStretchLiveData.value = true
}
fun clear() {
clearButtonVisibilityLiveData.value = true
searchButtonClickableLiveData.value = false
searchButtonColorLiveData.value = "#484951"
keywordLiveData.value = ""
}
fun input(keyword: String) {
if (keyword.isNullOrEmpty()) {
searchButtonColorLiveData.value = "#484951"
searchButtonVisibilityLiveData.value = true
searchButtonClickableLiveData.value = false
clearButtonVisibilityLiveData.value = false
} else {
searchButtonColorLiveData.value = "#F2F4FF"
searchButtonVisibilityLiveData.value = true
searchButtonClickableLiveData.value = true
clearButtonVisibilityLiveData.value = true
}
}
fun popUp() {
searchButtonClickableLiveData.value = false
searchButtonColorLiveData.value = "#484951"
searchButtonVisibilityLiveData.value = true
clearButtonVisibilityLiveData.value = false
keywordLiveData.value = ""
searchBarStretchLiveData.value = false
popupLiveData.value = true
}
}
以控件的某个特点作为笼统 Model 的依据,不同的事务逻辑函数会修正相应的控件特点 Model,界面再调查 Model。
制作界面逻辑也相应地做如下修正:
// TemplateSearchActivity.kt
private fun observeData() {
searchViewModel.keyboardLiveData.observe(this){
if(it) KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
else KeyboardUtils.hideSoftInput(etSearch)
}
searchViewModel.clearButtonVisibilityLiveData.observe(this){
ivClear.visibility = if(it) visible else gone
}
searchViewModel.searchButtonVisibilityLiveData.observe(this){
tvSearch.visibility = if(it) visible else gone
}
searchViewModel.searchButtonColorLiveData.observe(this) {
tvSearch.textColor = it
}
searchViewModel.searchButtonClickableLiveData.observe(this){
tvSearch.isEnabled = it
}
searchViewModel.searchBarStretchLiveData.observe(this){
vInputBg.apply {
if (it) end_toEndOf = parent_id
else end_toStartOf = ID_SEARCH
}
}
searchViewModel.gotoSearchLiveData.observe(this){
findNavController(NAV_HOST_ID.toLayoutId()).navigate(
R.id.action_to_result,
bundleOf("keywords" to it)
)
}
searchViewModel.keywordLiveData.observe(this){
if(it.isNullOrEmpty()) {
etSearch.text = null
etSearch.requestFocus()
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
}
searchViewModel.popupLiveData.observe(this){
if(it){
findNavController(NAV_HOST_ID.toLayoutId()).popBackStack()
}
}
}
更内聚的Model
上面的这次重构处理了 Model 和事务强耦合的问题,但那个老问题仍然没有得到处理,乃至还加重了,即改写界面的逻辑散落在更多的数据调查者中,无法构成对界面制作统一的认知。
用一张图表达下此刻 MVVM 的杂乱度:
看上去挺杂乱的。之所以会这样是由于数据源不单一。比方查找按钮应该长什么样用了三个 Model 来表示:
- searchButtonColorLiveData
- searchButtonClickableLiveData
- searchButtonVisibilityLiveData
当 UI 产生改变,查找按钮要增加一个突变的背景色时,是不是还要新增一个 Model?
这样规划的话杂乱度就会猛增。
当前按钮有2种色彩,2种点击状况,2种可见状况。当把这三个维度分别用三个 Model 来表达时,意味着它们能够不受操控地独立改变,进而构成 2 * 2 * 2 = 8 种排列组合。但其间只要 3 种组合是契合预期的。怎么确保在改这块代码时不生成过错的排列组合(界面状况)?
进一步,查找按钮的可见状况是和查找条的长度联动的,即只要当查找条拉长时按钮才不可见。假如处理不好就会产生如下的界面状况不一致:
别的,清空按钮也会和查找按钮的色彩联动。
更好的规划应该是用一个 Model 表达一切相关的界面状况:
data class SearchBarModel(
val searchButtonColor: String,// 查找按钮色彩
val isSearchButtonClickable: Boolean, // 查找按钮是否可点击
val isSearchBarStretch: Boolean, // 查找条是否拉升
val isClearShow: Boolean, // 是否展现清空按钮
)
对应的 ViewModel 做相应的修正:
class SearchViewModel : ViewModel() {
// 查找按钮色彩
val searchBarLiveData = MutableLiveData<SearchBarModel>()
// 键盘是否展现
val keyboardLiveData = MutableLiveData<Boolean>()
// 跳转到查找成果页
val gotoSearchLiveData = MutableLiveData<String>()
// 关键词
val keywordLiveData = MutableLiveData<String>()
// 从成果页回来
val popupLiveData = MutableLiveData<Boolean>()
fun init() {
keyboardLiveData.value = true
searchBarLiveData.value = SearchBarModel(
"#484951",
false,
false,
false
)
}
fun search(keyword: String) {
gotoSearchLiveData.value = keyword
keyboardLiveData.value = false
searchBarLiveData.value = SearchBarModel(
"#484951",
false,
true,
true
)
}
fun clear() {
keywordLiveData.value = ""
searchBarLiveData.value = SearchBarModel(
"#484951",
false,
false,
false
)
}
fun input(keyword: String) {
if (keyword.isNullOrEmpty()) {
searchBarLiveData.value = SearchBarModel(
"#484951",
false,
false,
false
)
} else {
searchBarLiveData.value = SearchBarModel(
"#F2F4FF",
true,
false,
true
)
}
}
fun popUp() {
keywordLiveData.value = ""
popupLiveData.value = true
searchBarLiveData.value = SearchBarModel(
"#484951",
false,
false,
false
)
}
}
当任何一个影响查找条状况改变的事情产生时,你都得构建一个 SearchBarModel 并为其间的四个参数赋值。这迫使你将一切的状况都考虑在内,避免留传。这样的规划极大的降低了代码的杂乱度。
界面制作代码也得做相应修正:
// TemplateSearchActivity.kt
private fun observeData() {
searchViewModel.searchBarLiveData.observe(this) { model->
ivClear.visibility = if(model.isClearShow) visible else gone
tvSearch.apply {
textColor = model.searchButtonColor
visibility = if(model.isSearchBarStretch) gone else visible
isEnable = model.isSearchButtonClickable
}
vInputBg.apply {
if (model.isSearchBarStretch) end_toEndOf = parent_id
else end_toStartOf = ID_SEARCH
}
}
}
如此一来制作界面的代码也愈加内聚了,一切关于查找按钮长什么样的代码都内聚在一个数据调查者回调中,后期修正查找按钮款式的时候,不至于要满 Activity 地找控件。
再用一张图看下简化后的杂乱度:
LiveData 数量少了,事务逻辑和 LiveData 交互的逻辑也少了。
总结
这一篇首要引进了 MVVM 架构的两个重要概念 ViewModel 以及 LiveData。前者使得“有免死金牌的事务层”成为或许,后者使得事务层成为数据持有者,并以数据驱动改写界面。
下一篇会持续叙述 MVVM 并用实战代码展现它的痛点。尽请等待~
引荐阅览
写事务不必架构会怎么样?(一)
写事务不必架构会怎么样?(二)
写事务不必架构会怎么样?(三)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(一)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(二)
MVP 架构终究审判 —— MVP 处理了哪些痛点,又引进了哪些坑?(三)
“无架构”和“MVP”都救不了事务代码,MVVM能力挽狂澜?(一)