ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

WanAndroid——探索Android应用架构的一次实践

2021-09-27 23:00:33  阅读:226  来源: 互联网

标签:MVP 架构 Observer MVVM LiveData DataBinding Android WanAndroid 页面


在 《也谈Android应用架构》 和 《再谈Android应用架构——Jetpack VS 生命周期》 两篇文章中,我们详细论述了MVC、MVP、MVVM架构的思想、优缺点以及使用注意事项,并阐述了借助Jetpack强大的生命周期管控能力解决架构“本地化”的问题。但没有实践的论述不仅不直观,也应了那句Talk is cheap, show me the code.的经典名言,因此这个基于 玩Android (https://www.wanandroid.com/) 网站提供的开放API实现的客户端便应运而生了。

项目地址:https://github.com/LtLei/wanandroid

Wan Android

在1.0版本中,涵盖了首页,分类页,搜索页,以及登录账号并对文章进行收藏,查看收藏列表等功能。总体来说功能并不复杂,但作为演示已经足够了。项目整体采用了MVP+Lifecycle+ViewModel+LiveData,结合Room进行数据缓存,使用Retrofit+Coroutines进行网络请求。为什么没有DataBinding?请听我稍后解释。

下面是几个我认为在实践中比较重要的点,特此列出来以便大家思考以及对我进行指点。

为什么是MVP而不是MVVM+DataBinding?

说为什么不是MVVM,不如说为什么要是MVVM。最近看到许多文章都在大力“推销”MVVM和DataBinding,听起来好像MVP已经过时了,Jetpack+DataBinding已经君临天下,成为了Android开发的唯一范式。且不说MVVM有没有全面超越MVP,单就这样的“吹捧”本身就值得我们反思。

在《也谈Android应用架构》 中,我曾说过MVP最大的问题就是VP相互纠缠,即使利用了单一职责原则对P进一步分化,采用了MVP-R技术,最后因为P要通知V的顽疾不得不留有遗憾。让我们再次对这个问题进行更深入的探究,出现问题的部分如下:

 1public class SharedVipPresenter{
2    private VipRepository mVipRepository;
3    private SharedVipView mSharedVipView;
4    // ...
5    public void getVipInfo(){
6        VipInfo vi = mVipRepository.getVipInfo();
7        mSharedVipView.getVipInfoSuccess(vi);
8    }
9}
10
11public class VipPresenter{
12    private VipRepository mVipRepository;
13    private VipView mVipView;
14    // ...
15    public void getVipInfo(){
16        VipInfo vi = mVipRepository.getVipInfo();
17        mVipView.getVipInfoSuccess(vi);
18    }
19}

 

可以看到为了反馈到不同的V,我们写了几乎一样的代码,且很难优化它,于是这就成了MVP的一个显著缺点,也反向证明了MVVM是多么优越。但是难以处理不代表无法处理,这时候反而可以从MVVM中吸取一点经验了,要做的事情其实只有一件:让P把结果通知给V。说到通知,至少有三种方案可以考虑:

    • P持有V,直接调用V的方法,也就是经典的MVP实现方式

 

    • P不再持有V,但可以通过注册回调方式由V自行接收结果,类似Callback方式

 

  • P不持有V,但可以使用订阅模式,类似DataBinding

 

让我们暂时抛开成见来仔细琢磨下这几种方案的异同,你会发现它们只是三种不同的表现形式而已。不管是LiveData还是DataBinding,不都只是A通知B这一核心问题的一种解决方式而已吗?只不过我们使用MVP时选择了耦合的这一种方式而已,这并不是LiveData或DataBinding本身优秀的原因。仅在A通知B这一方面,没有对错,也没有输赢,因为回调也好,订阅也好,本就不是它们的专利。

所以,如果我们在P和V之间使用订阅或回调,也不会对DataBinding等产生任何“侵权”行为,但DataBinding给我们以启示,使得我们能跳出表现层而看出更深层次的含义,这一点倒不得不给它“颁奖”了。有了这一层认知,现在可以非常肯定的说,Jetpack和MVVM根本就是两个方向的东西,唯一看起来有些相似的不过是都用了订阅模式而已,因而没有必要绑定在一起,Jetpack和MVP一样可以无缝衔接

没有了后顾之忧,我们就可以好好观察一番MVVM了,在Android中实现MVVM主要靠DataBinding。这无疑是一个伟大的框架,是会让无数人爱不释手的框架,但如果你还没有使用它倒也不必惊慌。DataBinding的核心是数据绑定,也就是把Model绑定到UI组件上,同时也负责Model和UI之间的数据同步问题。在使用上的体验就是样板代码大大减少了,这应该是最主要最明显的感受了。

这种体验确实是无法拒绝的,但因“一腔热血”而全身心投入是不够理智的行为。经过仔细分析,DataBinding还是有一大一小两个问题:

问题1 UI复用

这应该是再小不过的问题了,使用DataBinding后Layout将很难复用,虽然这和UI优化有一定的冲突,但不是所有的都不能复用,也不是所有人都会严格地遵守复用原则(如果没有极强的规范,难道不是直接写布局最简便?)。所以和它的优势相比,这个问题的影响可以小到不计了。

问题2 设计模式上的打击

我认为这是相对较大的一个问题,DataBinding把原本仅在Activity/Fragment中的UI逻辑部分地搬到了Layout中。原本的Layout文件和Model毫不相干,是纯粹的UI组件,它的数据绑定完全由Activity来操作,DataBinding把这个步骤搬到了Layout里,但并不彻底,一些操作还是需要Activity+Layout组合完成。这个现象破坏了一些原则,我们经常强调单一职责,但没怎么提过一件事应该有始有终,已经不可再分的一个职责明显没必要由两个组件一起完成。

所以说,如果你已经选择了DataBinding,那么继续使用也没有什么问题,一个优点和一个缺点互相抵消,至少表明这样做是不会错的。但是如果你还没有打算使用它,也没必要因为热度而急于切换,尽管我们相信未来MVVM一定会大放异彩,但能够一起出道的是不是DataBinding还犹未可知,所以保持观望也未免不是一种明智的选择。另外,Google近期又推出了ViewBinding大杀器,很久就会到来的还有Compose,这一切不都说明了战争才刚刚开始吗?

如果你也希望直达战争的结果,那就和我一样作“冷眼旁观”状吧,硝烟结束的一刻,才是MVP光荣退役的真正时刻。

 

关于依赖注入(DI)

关于DI,早就有一个家喻户晓的框架叫做Dagger2,但它出场的概率和其他框架相比实在差距太远了。我想不外乎两个原因,一是Dagger本身太复杂了,学习成本奇高,二就是DI本身并没有那么被重视(当然我是讲在广泛的范围下)。DI已经算是非常基础的思想了,想来大家都很了解,这里不多作介绍了。不过不使用Dagger照样可以做好DI,如果你被Dagger折磨过,那么手动DI一定会让你爱不释手的。

 

LiveData使用的注意事项

当屏幕旋转或页面被回收,或其他原因导致页面生命周期变化后,由于LiveData被很好地保持了下来,当页面重建后通过新的Observer与之关联时,必定会触发onChange方法把卖二手游戏平台地图数据同步到页面中(如果有数据的话),从添加Observer的流程就可以看出这一点:

 1// 添加Observer的过程
2public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
3    assertMainThread("observe");
4    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
5        // ignore
6        return;
7    }
8    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
9    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
10    // ...
11}
12
13// LifecycleBoundObserver实现了LifecycleEventObserver,当生命周期变化时会回调 onStateChanged 方法
14class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
15    // ...
16
17    @Override
18    public void onStateChanged(@NonNull LifecycleOwner source,
19            @NonNull Lifecycle.Event event) {
20        if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
21            removeObserver(mObserver);
22            return;
23        }
24        activeStateChanged(shouldBeActive());
25    }
26}
27
28// 经过一系列操作,当生命周期变化时会调用dispatchingValue方法
29void activeStateChanged(boolean newActive) {
30    // ...
31    if (mActive) {
32        dispatchingValue(this);
33    }
34}
35
36// 通过considerNotify决定是否需要同步数据
37void dispatchingValue(@Nullable ObserverWrapper initiator) {
38    if (mDispatchingValue) {
39        mDispatchInvalidated = true;
40        return;
41    }
42    mDispatchingValue = true;
43    do {
44        mDispatchInvalidated = false;
45        if (initiator != null) {
46            considerNotify(initiator);
47            initiator = null;
48        } else {
49            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
50                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
51                considerNotify(iterator.next().getValue());
52                if (mDispatchInvalidated) {
53                    break;
54                }
55            }
56        }
57    } while (mDispatchInvalidated);
58    mDispatchingValue = false;
59}
60
61// 通过对比LiveData的mVersion字段和Observer的mLastVersion字段,决定是否需要同步数据
62private void considerNotify(ObserverWrapper observer) {
63    // ...
64
65    if (observer.mLastVersion >= mVersion) {
66        return;
67    }
68    observer.mLastVersion = mVersion;
69    observer.mObserver.onChanged((T) mData);
70}

 

当Observer刚添加进来时,它的mLastVersion是-1,而LiveData除非没操作过否则一定不是-1,这时候就必然回调onChange了。

这对我们有什么影响呢?假如LiveData中存储的是页面需要的数据,例如一个列表,当页面重建后恢复列表的数据,这正是我们想要的。但假如我们进行的是单次操作,例如点击按钮进行收藏,LiveData用来说明收藏结果,这时重建后LiveData的值会再次反映到页面中,就会看到类似旋转一次屏幕就弹出一次“收藏成功”的怪诞现象了。

一种方式是对于单次操作,每次使用完数据后手动置为null,null通常会被我们过滤掉,所以即使onChange回调了也不会有问题。不过手动置空给我们增加了负担,必须在想到这是一次单次操作的同时想到置空,所以最好是我们知道它是单次操作后,可以自动处理这种情况。这里提供一种较为合理,侵入性又较小的方式,使用Event包装返回数据,使用EventObserver代替Observer即可:

 1open class Event<out T>(private val content: T) {
2
3    var hasBeenHandled = false
4        private set // Allow external read but not write
5
6    fun getContentIfNotHandled(): T? {
7        return if (hasBeenHandled) {
8            null
9        } else {
10            hasBeenHandled = true
11            content
12        }
13    }
14
15    fun peekContent(): T = content
16}
17
18class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
19    override fun onChanged(event: Event<T>?) {
20        event?.getContentIfNotHandled()?.let { value ->
21            onEventUnhandledContent(value)
22        }
23    }
24}

 

关于LiveDataBus

本项目中并没有使用LiveDataBus,但还是有必要谈一下关于消息总线问题的一些想法。现阶段消息总线主要有EventBus,RxBus以及这个LiveDataBus三种实现,前两种早已在漫长的时间里经过了无数检验,我们主要谈一谈LiveDataBus。

当一个页面的状态改变,需要通知到许多页面时,就需要消息总线了。不管是EventBus还是RxBus,以及现在的LiveDataBus,都是基于订阅模式实现的,所不同的是LiveData具备了生命周期安全的优势。但正如上面所说的,LiveDataBus也会有一订阅就收到数据的问题,而且由于这是一对多的关系,不能通过类似Event那样的方式解决。因此当你看到LiveDataBus时,基本上都是通过反射修改了Observer的mLastVersion字段值,使之与LiveData当前的mVersion一致,来变相达到目的。

其实我并不认为这是一种好的解决方案,LiveData并不是为了消息总线而设计的,它的Observer也仅仅是页面级的组件,不应该处理跨页面级的事务,这种反射实际上给LiveData增加了不属于它的能力,破坏了原结构的完整性。在LiveData本身不具备这个机制前,保守地使用专业的、经过检验的EventBus和RxBus等,也是一种合情合理的态度。

虽然LiveDataBus算不得脱颖而出,LiveData本身还是可以处理一些和数据更新相关的事情的。例如很多页面离不开User信息,那么维护一个公用的UserLiveData就可以保证任何时候取得的信息都是最新的,这也是LiveData服务相当周到的一点体现吧。

关于Repository的小优化

在M层的优化中,有一步骤是使用Repository来处理全部的业务,包含纯粹的M和经数据加工后的M两部分。但是伴随M增大,需要处理的数据也会变多,Repository也会发生体积暴涨问题。对其进行优化我觉得有一个很重要的前提,那就是要M之外的组件对此零感知,ViewModel永远仅依赖Repository本身。在这个前提下可以进行的优化空间并不大,主要是把业务进行分组,由一系列的小Repository分别处理一部分问题,分组要选择合适的粒度,太细的话就会发生类的数量暴涨了。

 

总结

理论总是空洞的,使人觉得似懂非懂,这次的实践在一些方面对其进行了很好的诠释。当然它不是也不会是Android开发的最佳实践,技术会不断进步,思想本身也会不断演进,而且APP还有非常多的方面需要我们注意,我们需要的是保持活跃的思维跟进技术思想变革,同时也要保持理智,要对事物有自己的分辨。

在这次实践里,我认为最大的启示是不要过于贪心,不能执着于把每一部分都做到完美,反而是把目标变小一些,仅在一个范围内做到最好,然后逐渐扩展到全局,也就是实践出真知,眼高手低是无法把事情做到卓越的。

 

后续规划

1.0版本仅仅是开始,我们演示了架构最基础的结构,在一些类上做了一定的优化,但APP的开发还有更多的知识需要探索。所以接下来让我们继续扬帆起航,探索更多的奥秘吧。

后续我会先介绍单元测试的作用,并展示更多组件的使用方式,以及当项目变大时使用组件化优化等方面的问题,接下来是对一些基础知识的深入探索,最后会对一些面向未来的新事物略窥一二。

学无止境,希望能和热爱学习的你,共勉。

标签:MVP,架构,Observer,MVVM,LiveData,DataBinding,Android,WanAndroid,页面
来源: https://www.cnblogs.com/ludongguoa/p/15345765.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有