我10年的Android重构之旅:框架篇

在我这几年的学习和成长中,慢慢的意识到搭建一个优秀的 Android 开发框架是一件非常困难以及痛苦的事情,它不仅需要满足不断增长的业务需求,还要保证框架自身的整洁与扩展性,这让事情变得非常有挑战,但我们必须这样做,因为健壮的 Android 开发框架是一款优秀APP的基础。
在我们开发的初期往往并不需要什么框架,因为 Android Framework 良好的容错性帮助我们避免了很多问题,甚至你不需要深入的学习就可以写出一个较为完善的 APP,几个简单Material Design 风格界面加上一些数据这让人人都能成为 Android 开发者,但是真的这样就够了吗?

当然不够!!

随着我们的项目越来越庞大,各种问题接踵而至,混乱的数据存储、获取,灵活性不够高的代码,会成为我们项目中、后期最大的阻碍,任由其自由发展的后果就是,导致项目狼藉一片,我们将很难加入新的功能,只能对它进行重构甚至推翻重做。在开始编程前,我们不应该低估一个应用程序的复杂性。

另外,在软件工程领域,始终都有一些值得我们学习和遵守的原则,比如:单一职责原则,依赖倒置原则,避免副作用等等。 Android Framework 不会强制我们遵守这些原则,或者说它对我们没有任何限制,试想那些耦合紧密的实现类,处理大量业务逻辑的 Activity 或 Fragment ,随处可见的EventBus,难以阅读的数据流传递和混乱的回调地狱等等,它们虽然不会导致系统马上崩溃,但随着项目的发展,它们会变得难以维护,甚至很难添加新的代码,这无疑会成为业务增长的可怕障碍。

所以说,对于开发者们来讲,一个好的架构指导规范,至关重要。

架构的选择

现在网上关于 MVVM、MVP、MVC、AndroidFlux 的选择与分析的文章已经非常多了,这里我就不过多描述了,感兴趣的同学可以看 我的Android重构之旅:架构篇 ,在这里我们最终选择了 MVP 作为我们的开发架构,MVP 的好处有很多,但最终使我们选择它的是因为看中了它对于普通开发者简单容易上手,并同时能将我们的 Activity 的业务边界规划清晰。

Refused God Activity

在这些年的开发过程中,经常能够看到上千行代码的 Activity ,它无所不能:

重新定义的生命周期
处理Intent
数据更新
线程切换
基础业务逻辑
……
更有甚者在 BaseActivity 中定义了一切能想得到的子类变量等等,它现在确实成为了“上帝”,方便且无所不能的上帝!
随着项目的发展,它已经庞大到无法继续添加代码了,于是你写了很多很多的帮助类来帮助这个上帝瘦下来:
我10年的Android重构之旅:框架篇
不经意之间,你已经埋下了黑色×××

看起来,业务逻辑被帮助类消化解决了,BaseActivity 中的代码减少了,不再那么“胖”了,帮助类缓解了它的压力,但随着项目的成长,业务的扩大,同时这些帮助类也慢慢变多变大,这时候又要按照业务继续拆分它们,维护成本好像又增加了,那些混乱并且难以复用的程序又回来了,我们的努力好像都白费了。

当然,一部分人会根据不同的业务功能分离出不同的抽象类,但相对那种业务场景下,它们仍是万能的。

无论什么理由这种创造“上帝类”的方式都应该尽量避免,我们不应该把重点放在编写那些大而全的类,而是投入精力去编写那些易于维护和测试的低耦合类,如果可以的话,最好不要让业务逻辑进入纯净的Android世界,这也是我一直努力的目标。

Clean architecture and The Clean rule

这种看起来像“地壳”的环形图就是Clean Architecture,不同颜色的“环”代表了不同的系统结构,它们组成了整个系统,箭头则代表了依赖关系。

我10年的Android重构之旅:框架篇
我们已经选用 MVP 作为框架开发的架构了,这里就不深入的细说 Clean Architecture 架构了,Clean Architecture 的一些优势我们将揉入框架中,我们在框架的设计时应该遵从以下三个原则:

分层原则
依赖原则
抽象原则
接下来我就分别阐述一下,我对这些原则的理解,以及背后的原因。

分层原则

首先,框架应不去限制应用的具体分层,但是从多人协作开发的角度来说,通常我会将 Android 分为三层:

外层:事件引导层(View)
中间层:接口适配层(一般由 Dagger2 生成)
内层:业务逻辑层
看上面的三层我们很容易的就联想到 MVP 结构,下面我就来说一说这三层所包含的内容。

事件引导层

事引导层,它在框架中作为 View 层的另一展现,它主要负责 View 事件上的走向,例如 onClick、onTouch、onRefresh 等,负责将事件传递至业务逻辑层。

接口适配层

接口适配层的目的是连接业务逻辑与框架特定代码,担任外层与内层之间的桥梁,一般我们使用 Dagger2 进行生成。

业务逻辑层

业务逻辑层是框架中最重要的一部分,我们在这里解决所有业务逻辑,这一层不应该包含事件走向的代码,应该能够独立使用 Espresso 进行测试,也就是说我们的业务逻辑能够被独立测试、开发和维护,这是我们框架架构的主要好处。

依赖规则

依赖规则与 Clean Architecture 箭头方向保持一致,外层”依赖“内层,这里所说的“依赖”并不是指你在gradle中编写的那些 Dependency 语句,应该将它理解成“看到”或者“知道”,外层知道内层,相反内层不知道外层,或者说外层知道内层是如何定义抽象的,而内层却不知道外层是如何实现的。如前所述,内层包含业务逻辑,外层包含实现细节,结合依赖规则就是:业务逻辑既看不到也不知道实现细节。

对于项目工程来讲,具体的依赖方式完全取决于你。你可以将他们划入不同的包,通过包结构来管理它们,需要注意的是不要在内部包中使用外部包的代码。使用包来进行管理十分的简单,但同时也暴露了致命的问题,一旦有人不知道依赖规则,就可能写出错误的代码,因为这种管理方式不能阻止人们对依赖规则的破坏,所以我更倾向将他们归纳到不同的 Android module 中,调整 Module 间的依赖关系,使内层代码根本无法知道外层的存在。

抽象原则

所谓”抽象原则”,就是指从具体问题中,提取出具有共性的模式,再使用通用的解决方法加以处理。

例如,在我们开发中往往会碰到切换无网络、无数据界面,我们在框架中定义一个 ViewLayoutState`接口,一方面业务逻辑层可以直接使用它来切换界面,另一方面我们也可以在 View 层实现该接口,来重写切换不同界面的样式,业务逻辑层只是通知接口,它不清楚实现细节,也不用知道是如何实现的,甚至不知道面的载体是一个 Activity 或是一个 View。

这很好演示了如何使用抽象原则,当抽象与依赖结合后,就会发现使用抽象通知的业务逻辑看不到也不知道 ViewLayoutState 的具体实现,这就是我们想要的:业务逻辑不会注意到具体的实现细节,更不知道它何时会改变。抽象原则很好的帮我们做到了这一点。

Build this library

上面介绍了这么多设计准则,现在就来介绍下 Library 的设计,Library 只分为以下三个模块:

Instance
Util
Base
我10年的Android重构之旅:框架篇
Util、Instance

Util、Instance 本质上的定位都为工具、辅助类,一种为“即用即走”的 static 工具类,例如判断文字是否为空等,一种为“长时间使用”的 instance 形式,例如 Activity 管理栈等。

Base

Base 主要工作是赋予了 BaseActivity 与 BaseFragment 很多不同的能力,上面我们提到了要避免创造“上帝”,但是在项目开发过程中很难避免这种情况,在 Library 中我们将 BaseView 所有能力抽取了出来,BaseActivity 与 BaseFragment 将只负责 View 的展示。

我10年的Android重构之旅:框架篇
BaseActivity

BaseActivity 主要功能被分为:

ActivityMvp 提供上下文
ViewResult 提供跨界面刷新
ActivityToolbarBase 提供顶部栏
ViewLayoutState 提供切换界面
LifecycleCallbackStrategy 生命周期回调管理
我10年的Android重构之旅:框架篇

我们这里可以看到 BaseActivity 实现出的全部能力都与 View 相关,可能这会感到奇怪,不是有实现 ViewResult 跨界面刷新这个业务能力吗?我们来看下它是如何实现的。

/* 全局刷新/@Overridepublic void resultAll() { presenter.resultAll();}/ 部分刷新* @param resultData/@Overridepublic void result(Map<String, String> resultData) { presenter.result(resultData);}
这里可以看到,我们委托了 presenter 去实现,保证了 BaseActivity 只存在 View 相关的操作。

BaseListActivity

public abstract class ActivityListBase extends ActivityBase implements ActivityRecyclerMvp { private RecyclerView rvIndexRecycler = null; private SmartRefreshLayout srlRefresh = null; private MultiTypeAdapter adapter = null; private PresenterListBase presenter = null; @Override protected final int getLayout() { return R.layout.activity_recycler_base; } @Override protected final void onBeforeInit(Bundle savedInstanceState, Intent intent) { presenter = getPresenter(); presenter.onCreate(savedInstanceState); } @Override protected final void onInitComponent() { rvIndexRecycler = findViewById(R.id.rv_index_recycler); srlRefresh = findViewById(R.id.srl_index_refresh); onInitRecycler(); onInitListComponent(); } @Override protected final void onInitViewListener() { onInitRefresh(); } @Override protected final void onLoadHttpData() { presenter.getData(PresenterListBase.INIT); } / 初始化刷新布局 / protected final void onInitRefresh() { srlRefresh.setOnLoadMoreListener(new OnLoadMoreListener() { @Override public void onLoadMore(RefreshLayout refreshLayout) { presenter.getData(PresenterListBase.LOAD_MORE); } }); srlRefresh.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh(RefreshLayout refreshLayout) { srlRefresh.setEnableLoadMore(true); srlRefresh.setNoMoreData(false); presenter.getData(PresenterListBase.REFRESH); } }); } / 初始化Recycler / protected final void onInitRecycler() { RecyclerView.LayoutManager layoutManager = getLayoutManager(); rvIndexRecycler.setLayoutManager(layoutManager); rvIndexRecycler.setHasFixedSize(false); adapter = new MultiTypeAdapter(presenter.providerData()); addRecyclerItem(adapter); rvIndexRecycler.setAdapter(adapter); }}
PresenterViewListImpl

public abstract class PresenterViewListImpl<T extends RespBase> implements PresenterListBase { protected ActivityRecyclerMvp viewBase = null; // 布局内容 protected List<Object> data = null; // 布局起点 protected int pageStart = 1; // 加载更多 protected final int pageSize = PAGE_MAX_SIZE; // 加载数据类型 protected @LoadDataState int loadState; public PresenterViewListImpl(ActivityListBase activityListBase) { viewBase = activityListBase; data = new ArrayList<>(); } @Override public void onCreate(Bundle savedInstanceState) { } @Override public void result(Map<String, String> resultData) { RunTimeUtil.runTimeException("未实现result接口"); } @Override public void resultAll() { RunTimeUtil.runTimeException("未实现resultAll接口"); } @Override public void getData(int state) { loadState = state; switch (loadState) { case INIT: { processPreInitData(); break; } case REFRESH: { pageStart = 1; break; } case LOAD_MORE: { pageStart = pageStart + 1; break; } } // 加载网络数据 loadData(new OnLoadDataListener<T>() { @Override public void loadDataComplete(T t) { handleLoadData(loadState, t); } @Override public void loadDataError(@StringRes int errorInfo) { handleLoadDataError(loadState, errorInfo); } @Override public void loadDataEnd() { handleLoadDataEnd(); } }); } / 开始加载 / protected final void processPreInitData() { pageStart = 1; viewBase.switchLoadLayout(); } / 处理加载完成的数据 @param loadState @param t */ protected void handleLoadData(int loadState, T t) { switch (loadState) { case INIT: { viewBase.switchContentLayout(); initView(t); break; } case REFRESH: { viewBase.finishRefresh(); initView(t); break; } case LOAD_MORE: { viewBase.finishRefreshLoadMore(); break; } } } /* 处理加载错误的情况 @param loadState @param errorInfo / protected void handleLoadDataError(int loadState, int errorInfo) { switch (loadState) { case INIT: { viewBase.switchReLoadLayout(errorInfo); break; } case REFRESH: { ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo)); viewBase.finishRefresh(); break; } case LOAD_MORE: { pageStart = pageStart - 1; ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo)); viewBase.finishRefreshLoadMore(); break; } } } protected void handleLoadDataEnd() { } @Override public void onDestroy() { viewBase = null; data = null; } @Override public List<?> providerData() { return data; } public abstract void loadData(OnLoadDataListener loadDataListener); public abstract void initView(T t); public void presenterLoadMoreData(T t) { } public interface OnLoadDataListener<Q extends RespBase> { public void loadDataComplete(Q q); public void loadDataError(@StringRes int errorInfo); public void loadDataEnd(); }}
由于篇幅有限,对本框架感兴趣的同学可以来这里查看。

Show Code

下面我们来针对一个简单的数据列表,使用全新的框架开发试试。

public class InformationListActivity extends BaseListActivity { @Inject InformationActivityContract.Presenter mPresenter; @Override public void injectAndInit() { // 接口适配层 DaggerInformationListActivityComponent.builder().activeInformationActivityModule(new InformationModule(this)).build().inject(this); } @Override public BaseListPresenter getBaseListPresenter() { return mPresenter; } @Override protected void registerItem(MultiTypeAdapter adapter) { // 展示多 RecyclerView adapter.register(ActiveDetailInfo.class,new ActiveAllListProvider(mActivity)); adapter.register(NoMoreDataBean.class,new NoMoreDataProvider()); }}
可以看到,我们很干净的抽离出了 View,接下来我们看看 Presenter 是如何实现的

public class InformationActivityPresenterImpl extends BaseListPresenterImpl<ResponseBean<ZoneActiveBean>> implements InformationActivityContract.Presenter { @Inject InformationActivityContract.View mView; @Inject ZoneApiService mZoneApiService; @Inject public InformationActivityPresenterImpl() { super(); } @Override public Observable getObservable(@Constant.RequestType int requestType) { return mZoneApiService.zoneActiveData(mView.getUserId(), pageNo, pageSize); } @Override public void initView(ResponseBean<ZoneActiveBean> responseBean) { ZoneActiveBean data = responseBean.getData(); if (data != null && data.activityInfo.activityList != null && data.activityInfo.activityList.size() > 0) { mData.clear(); for (ActiveDetailInfo item : data.activityInfo.activityList){ mData.add(item); } mView.setLoadMore(data.activityInfo.activityList.size() == pageSize); pageNo++; mView.notifyDataSetChanged(); } else { mView.setNodata(); } } @Override public void processLoadMoreData(ResponseBean<ZoneActiveBean> responseBean) { ZoneActiveBean data = responseBean.getData(); if (data != null && data.activityInfo.activityList != null && data.activityInfo.activityList.size() > 0) { for (ActiveDetailInfo item : data.activityInfo.activityList){ mData.add(item); } if (mData.size() == data.activityInfo.total) { mData.add(new NoMoreDataBean(false)); mView.setLoadMore(mData.size() == data.activityInfo.total); } pageNo ++; }else{ mView.setLoadMore(false); mData.add(new NoMoreDataBean(false)); } mView.notifyDataSetChanged(); }}
由于我们已经规定了,事件引导层只处理 View 相关的操作,这样我们的 Activity 变得十分整洁,并且 Activity 只作为数据与事件的一个走向,Presenter 帮我们处理事件的具体细节。

总结

作为公司内部通用的开发框架,功能的选择上应保持最小原则只使用有必然需要的功能,

在架构上应该保持良好的扩展性。

我相信你和我一样,在搭建框架的过程中遭遇着各式各样的挑战,从错误中吸取教训,不断优化代码,调整依赖关系,甚至重新组织模块结构,这些你做出的改变都是想让架构变得更健壮,我们一直希望应用程序能够变得易开发易维护,这才是真正意义上的团队受益。

不得不说,搭建应用架构的方式多种多样,而且我认为,没有万能的,一劳永逸的架构,它应该是不断迭代更新,适应业务的。所以说,你可以按照文中提供的思路,尝试着结合业务来构建你的应用程序。

最后,希望这篇文章能够对你有所帮助,如果你有其他更好的架构思路,欢迎分享或与我交流

原文地址:http://blog.51cto.com/13807306/2129396

时间: 2024-10-09 15:18:15

我10年的Android重构之旅:框架篇的相关文章

我的Android进阶之旅------&gt;Android疯狂连连看游戏的实现之实现游戏逻辑(五)

在上一篇<我的Android进阶之旅------>Android疯狂连连看游戏的实现之加载界面图片和实现游戏Activity(四)>中提到的两个类: GameConf:负责管理游戏的初始化设置信息. GameService:负责游戏的逻辑实现. 其中GameConf的代码如下:cn\oyp\link\utils\GameConf.java package cn.oyp.link.utils; import android.content.Context; /** * 保存游戏配置的对象

我的Android进阶之旅------&gt;经典的大牛博客推荐(排名不分先后)!!

本文来自:http://blog.csdn.net/ouyang_peng/article/details/11358405 今天看到一篇文章,收藏了很多大牛的博客,在这里分享一下 谦虚的天下 柳志超博客 Android中文Wiki AndroidStudio-NDK开发-移动开发团队谦虚的天下 - 博客园gundumw100博客 - android进阶分类文章列表 - ITeye技术网站CSDN博文精选:Android系列开发博客资源汇总 - CSDN.NET - CSDN资讯Android笔

Android Demo之旅 ListView底部添加加载更多按钮实现数据分页

在我们的实际项目中,数据应该说是很多的,我们的ListView不可能一下子把数据全部加载进来,我们可以当滚动条滚动到ListView的底部的时候,给一个更多的提示,当我们点击它即加载下一页的数据,相当与我们的分页效果,参考网上的东西,写了一个小小的demo,并总结了一些知识点,功能图如下:    源代码下载地址:http://download.csdn.net/detail/harderxin/7762625 掌握知识点: 1)自定义Adapter,将数据和ListView绑定起来 2)理解La

【转】Android 开发之旅:view的几种布局方式及实践

引言 通过前面两篇: Android 开发之旅:又见Hello World! Android 开发之旅:深入分析布局文件&又是“Hello World!” 我们对Android应用程序运行原理及布局文件可谓有了比较深刻的认识和理解,并且用“Hello World!”程序来实践证明了.在继续深入Android开发之旅之前,有必要解决前两篇中没有介绍的遗留问题:View的几种布局显示方法,以后就不会在针对布局方面做过多的介绍.View的布局显示方式有下面几种:线性布局(Linear Layout).

我的Android进阶之旅------&gt;Android利用Sensor(传感器)实现水平仪功能的小例

这里介绍的水平仪,指的是比较传统的气泡水平仪,在一个透明圆盘内充满液体,液体中留有一个气泡,当一端翘起时,该气泡就会浮向翘起的一端. 利用方向传感器返回的第一个参数,实现了一个指南针小应用. 我的Android进阶之旅------>Android利用Sensor(传感器)实现指南针功能 (地址:http://blog.csdn.net/ouyang_peng/article/details/8801204) 接下来,我们利用返回的第二.三个参数实现该水平仪.因为第二个参数,反映底部翘起的角度(当

(转)Android项目重构之路:实现篇

前两篇文章Android项目重构之路:架构篇和Android项目重构之路:界面篇已经讲了我的项目开始搭建时的架构设计和界面设计,这篇就讲讲具体怎么实现的,以实现最小化可用产品(MVP)的目标,用最简单的方式来搭建架构和实现代码. IDE采用Android Studio,Demo实现的功能为用户注册.登录和展示一个券列表,数据采用我们现有项目的测试数据,接口也是我们项目中的测试接口. 项目搭建 根据架构篇所讲的,将项目分为了四个层级:模型层.接口层.核心层.界面层.四个层级之间的关系如下图所示:

我的Android学习之旅

去年大概在七月份的时候误打误撞接触了一阵子Android,之后由于工作时间比较忙,无暇顾及,九月份的时候自己空闲的时间比较多,公司相对来说加班情况没以前严重.开启了个人的Android学习之旅,初衷是想将Android的博客做个索引文章的,不过想想还可以分享一些学习中的历程,算是对自己的Android学习 有个交代吧.由于在公司有工作,学习的时间通常就是周一到周五晚上的时间和周末时间,周一到周五晚上的时间不确定,因此牺牲了大量的周末时间来学习Android,有点像苦行僧,时间段持续了三个多月.如

我的Android进阶之旅------&gt;Java字符串格式化方法String.format()格式化float型时小数点变成逗号问题

今天接到一个波兰的客户说有个APP在英文状态下一切运行正常,但是当系统语言切换到波兰语言的时候,程序奔溃了.好吧,又是我来维护. 好吧,先把系统语言切换到波兰语,切换到波兰语的方法查看文章 我的Android进阶之旅------>Android[设置]-[语言和输入法]-[语言]列表中找到相应语言所对应的列表项 地址:http://blog.csdn.net/ouyang_peng/article/details/50209789 ================================

css重构之旅(一)

css重构之旅 >前言: 今年我大一,马上就要大二了.从高三毕业暑假到大学的这一年马上过去,马上迎来大二生活.学习前端也有将近一年了.一昧去追求那些视觉的效果和相对高端和新颖的技术,反而忽略了最基础的布局技巧. 回味 2017年3月,百格教育的手机端网站,是我接到的第一个公司外包的项目.我和组长合作完成,现在项目也已经顺利完成,回想起来,自己也跟着组长学到了不少: 1)一个公告的列表(你应该提前考虑到,一则公告的字数一定有多有少的)多出的应该做处理,不然超出会排成两行,使布局陷入混沌的状态: t