查看: 849|回复: 20

[Android教程] 我的Android重构之旅:框架篇

[复制链接]
  • TA的每日心情
    郁闷
    2019-7-5 14:08
  • 签到天数: 2 天

    [LV.1]初来乍到

    3

    主题

    5

    帖子

    123

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    123
    发表于 2018-11-19 11:49:45 | 显示全部楼层 |阅读模式
    内容
    在我这几年的学习和成长中,慢慢的意识到搭建一个优秀的 Android 开发框架是一件非常困难以及痛苦的事情,它不仅需要满足不断增长的业务需求,还要保证框架自身的整洁与扩展性,这让事情变得非常有挑战,但我们必须这样做,因为健壮的 Android 开发框架是一款优秀APP的基础。
    这是“我的Android重构之旅”的第二篇文章,在前面的文章中,我们介绍了常用的几种架构模式,本篇中将和大家分享下我们是如何搭建一个通用的项目框架

    Why do I need a framework?
    在我们开发的初期往往并不需要什么框架,因为 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 中定义了一切能想得到的子类变量等等,它现在确实成为了“上帝”,方便且无所不能的上帝! 随着项目的发展,它已经庞大到无法继续添加代码了,于是你写了很多很多的帮助类来帮助这个上帝瘦下来:

    我的Android重构之旅:框架篇

    我的Android重构之旅:框架篇
    不经意之间,你已经埋下了黑色炸弹 看起来,业务逻辑被帮助类消化解决了,BaseActivity 中的代码减少了,不再那么“胖”了,帮助类缓解了它的压力,但随着项目的成长,业务的扩大,同时这些帮助类也慢慢变多变大,这时候又要按照业务继续拆分它们,维护成本好像又增加了,那些混乱并且难以复用的程序又回来了,我们的努力好像都白费了。
    当然,一部分人会根据不同的业务功能分离出不同的抽象类,但相对那种业务场景下,它们仍是万能的。
    无论什么理由这种创造“上帝类”的方式都应该尽量避免,我们不应该把重点放在编写那些大而全的类,而是投入精力去编写那些易于维护和测试的低耦合类,如果可以的话,最好不要让业务逻辑进入纯净的Android世界,这也是我一直努力的目标。
    Clean architecture and The Clean rule
    这种看起来像“地壳”的环形图就是Clean Architecture,不同颜色的“环”代表了不同的系统结构,它们组成了整个系统,箭头则代表了依赖关系。

    我的Android重构之旅:框架篇

    我的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

    我的Android重构之旅:框架篇

    我的Android重构之旅:框架篇
    Util、Instance
    Util、Instance 本质上的定位都为工具、辅助类,一种为“即用即走”的 static 工具类,例如判断文字是否为空等,一种为“长时间使用”的 instance 形式,例如 Activity 管理栈等。
    Base
    Base 主要工作是赋予了 BaseActivity 与 BaseFragment 很多不同的能力,上面我们提到了要避免创造“上帝”,但是在项目开发过程中很难避免这种情况,在 Library 中我们将 BaseView 所有能力抽取了出来,BaseActivity 与 BaseFragment 将只负责 View 的展示。

    我的Android重构之旅:框架篇

    我的Android重构之旅:框架篇
    BaseActivity
    BaseActivity 主要功能被分为:
    • ActivityMvp 提供上下文
    • ViewResult 提供跨界面刷新
    • ActivityToolbarBase 提供顶部栏
    • ViewLayoutState 提供切换界面
    • LifecycleCallbackStrategy 生命周期回调管理

      我的Android重构之旅:框架篇

      我的Android重构之旅:框架篇

    我们这里可以看到 BaseActivity 实现出的全部能力都与 View 相关,可能这会感到奇怪,不是有实现 ViewResult 跨界面刷新这个业务能力吗?我们来看下它是如何实现的。
    1. /**
    2. * 全局刷新
    3. */
    4. @Override
    5. public void resultAll() {
    6. presenter.resultAll();
    7. }
    8. /**
    9. * 部分刷新
    10. *
    11. * @param resultData
    12. */
    13. @Override
    14. public void result(Map resultData) {
    15. presenter.result(resultData);
    16. }
    复制代码
    这里可以看到,我们委托了 presenter 去实现,保证了 BaseActivity 只存在 View 相关的操作。
    BaseListActivity
    1. public abstract class ActivityListBase extends ActivityBase implements ActivityRecyclerMvp {
    2. private RecyclerView rvIndexRecycler = null;
    3. private SmartRefreshLayout srlRefresh = null;
    4. private MultiTypeAdapter adapter = null;
    5. private PresenterListBase presenter = null;
    6. @Override
    7. protected final int getLayout() {
    8. return R.layout.activity_recycler_base;
    9. }
    10. @Override
    11. protected final void onBeforeInit(Bundle savedInstanceState, Intent intent) {
    12. presenter = getPresenter();
    13. presenter.onCreate(savedInstanceState);
    14. }
    15. @Override
    16. protected final void onInitComponent() {
    17. rvIndexRecycler = findViewById(R.id.rv_index_recycler);
    18. srlRefresh = findViewById(R.id.srl_index_refresh);
    19. onInitRecycler();
    20. onInitListComponent();
    21. }
    22. @Override
    23. protected final void onInitViewListener() {
    24. onInitRefresh();
    25. }
    26. @Override
    27. protected final void onLoadHttpData() {
    28. presenter.getData(PresenterListBase.INIT);
    29. }
    30. /**
    31. * 初始化刷新布局
    32. */
    33. protected final void onInitRefresh() {
    34. srlRefresh.setOnLoadMoreListener(new OnLoadMoreListener() {
    35. @Override
    36. public void onLoadMore(RefreshLayout refreshLayout) {
    37. presenter.getData(PresenterListBase.LOAD_MORE);
    38. }
    39. });
    40. srlRefresh.setOnRefreshListener(new OnRefreshListener() {
    41. @Override
    42. public void onRefresh(RefreshLayout refreshLayout) {
    43. srlRefresh.setEnableLoadMore(true);
    44. srlRefresh.setNoMoreData(false);
    45. presenter.getData(PresenterListBase.REFRESH);
    46. }
    47. });
    48. }
    49. /**
    50. * 初始化Recycler
    51. */
    52. protected final void onInitRecycler() {
    53. RecyclerView.LayoutManager layoutManager = getLayoutManager();
    54. rvIndexRecycler.setLayoutManager(layoutManager);
    55. rvIndexRecycler.setHasFixedSize(false);
    56. adapter = new MultiTypeAdapter(presenter.providerData());
    57. addRecyclerItem(adapter);
    58. rvIndexRecycler.setAdapter(adapter);
    59. }
    60. }
    复制代码
    PresenterViewListImpl
    1. public abstract class PresenterViewListImpl implements PresenterListBase {
    2. protected ActivityRecyclerMvp viewBase = null;
    3. // 布局内容
    4. protected List data = null;
    5. // 布局起点
    6. protected int pageStart = 1;
    7. // 加载更多
    8. protected final int pageSize = PAGE_MAX_SIZE;
    9. // 加载数据类型
    10. protected @LoadDataState
    11. int loadState;
    12. public PresenterViewListImpl(ActivityListBase activityListBase) {
    13. viewBase = activityListBase;
    14. data = new ArrayList<>();
    15. }
    16. @Override
    17. public void onCreate(Bundle savedInstanceState) {
    18. }
    19. @Override
    20. public void result(Map resultData) {
    21. RunTimeUtil.runTimeException("未实现result接口");
    22. }
    23. @Override
    24. public void resultAll() {
    25. RunTimeUtil.runTimeException("未实现resultAll接口");
    26. }
    27. @Override
    28. public void getData(int state) {
    29. loadState = state;
    30. switch (loadState) {
    31. case INIT: {
    32. processPreInitData();
    33. break;
    34. }
    35. case REFRESH: {
    36. pageStart = 1;
    37. break;
    38. }
    39. case LOAD_MORE: {
    40. pageStart = pageStart + 1;
    41. break;
    42. }
    43. }
    44. // 加载网络数据
    45. loadData(new OnLoadDataListener() {
    46. @Override
    47. public void loadDataComplete(T t) {
    48. handleLoadData(loadState, t);
    49. }
    50. @Override
    51. public void loadDataError(@StringRes int errorInfo) {
    52. handleLoadDataError(loadState, errorInfo);
    53. }
    54. @Override
    55. public void loadDataEnd() {
    56. handleLoadDataEnd();
    57. }
    58. });
    59. }
    60. /**
    61. * 开始加载
    62. */
    63. protected final void processPreInitData() {
    64. pageStart = 1;
    65. viewBase.switchLoadLayout();
    66. }
    67. /**
    68. * 处理加载完成的数据
    69. *
    70. * @param loadState
    71. * @param t
    72. */
    73. protected void handleLoadData(int loadState, T t) {
    74. switch (loadState) {
    75. case INIT: {
    76. viewBase.switchContentLayout();
    77. initView(t);
    78. break;
    79. }
    80. case REFRESH: {
    81. viewBase.finishRefresh();
    82. initView(t);
    83. break;
    84. }
    85. case LOAD_MORE: {
    86. viewBase.finishRefreshLoadMore();
    87. break;
    88. }
    89. }
    90. }
    91. /**
    92. * 处理加载错误的情况
    93. *
    94. * @param loadState
    95. * @param errorInfo
    96. */
    97. protected void handleLoadDataError(int loadState, int errorInfo) {
    98. switch (loadState) {
    99. case INIT: {
    100. viewBase.switchReLoadLayout(errorInfo);
    101. break;
    102. }
    103. case REFRESH: {
    104. ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo));
    105. viewBase.finishRefresh();
    106. break;
    107. }
    108. case LOAD_MORE: {
    109. pageStart = pageStart - 1;
    110. ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo));
    111. viewBase.finishRefreshLoadMore();
    112. break;
    113. }
    114. }
    115. }
    116. protected void handleLoadDataEnd() {
    117. }
    118. @Override
    119. public void onDestroy() {
    120. viewBase = null;
    121. data = null;
    122. }
    123. @Override
    124. public List providerData() {
    125. return data;
    126. }
    127. public abstract void loadData(OnLoadDataListener loadDataListener);
    128. public abstract void initView(T t);
    129. public void presenterLoadMoreData(T t) {
    130. }
    131. public interface OnLoadDataListener {
    132. public void loadDataComplete(Q q);
    133. public void loadDataError(@StringRes int errorInfo);
    134. public void loadDataEnd();
    135. }
    136. }
    复制代码
    由于篇幅有限,对本框架感兴趣的同学可以来这里查看
    Show Code
    下面我们来针对一个简单的数据列表,使用全新的框架开发试试。
    1. public class InformationListActivity extends BaseListActivity {
    2. @Inject
    3. InformationActivityContract.Presenter mPresenter;
    4. @Override
    5. public void injectAndInit() {
    6. // 接口适配层
    7. DaggerInformationListActivityComponent.builder().activeInformationActivityModule(new InformationModule(this)).build().inject(this);
    8. }
    9. @Override
    10. public BaseListPresenter getBaseListPresenter() {
    11. return mPresenter;
    12. }
    13. @Override
    14. protected void registerItem(MultiTypeAdapter adapter) {
    15. // 展示多 RecyclerView
    16. adapter.register(ActiveDetailInfo.class,new ActiveAllListProvider(mActivity));
    17. adapter.register(NoMoreDataBean.class,new NoMoreDataProvider());
    18. }
    19. }
    复制代码
    可以看到,我们很干净的抽离出了 View,接下来我们看看 Presenter 是如何实现的
    1. public class InformationActivityPresenterImpl extends BaseListPresenterImpl<responsebean> implements InformationActivityContract.Presenter {
    2. @Inject
    3. InformationActivityContract.View mView;
    4. @Inject
    5. ZoneApiService mZoneApiService;
    6. @Inject
    7. public InformationActivityPresenterImpl() {
    8. super();
    9. }
    10. @Override
    11. public Observable getObservable(@Constant.RequestType int requestType) {
    12. return mZoneApiService.zoneActiveData(mView.getUserId(), pageNo, pageSize);
    13. }
    14. @Override
    15. public void initView(ResponseBean responseBean) {
    16. ZoneActiveBean data = responseBean.getData();
    17. if (data != null && data.activityInfo.activityList != null && data.activityInfo.activityList.size() > 0) {
    18. mData.clear();
    19. for (ActiveDetailInfo item : data.activityInfo.activityList){
    20. mData.add(item);
    21. }
    22. mView.setLoadMore(data.activityInfo.activityList.size() == pageSize);
    23. pageNo++;
    24. mView.notifyDataSetChanged();
    25. } else {
    26. mView.setNodata();
    27. }
    28. }
    29. @Override
    30. public void processLoadMoreData(ResponseBean responseBean) {
    31. ZoneActiveBean data = responseBean.getData();
    32. if (data != null && data.activityInfo.activityList != null && data.activityInfo.activityList.size() > 0) {
    33. for (ActiveDetailInfo item : data.activityInfo.activityList){
    34. mData.add(item);
    35. }
    36. if (mData.size() == data.activityInfo.total) {
    37. mData.add(new NoMoreDataBean(false));
    38. mView.setLoadMore(mData.size() == data.activityInfo.total);
    39. }
    40. pageNo ++;
    41. }else{
    42. mView.setLoadMore(false);
    43. mData.add(new NoMoreDataBean(false));
    44. }
    45. mView.notifyDataSetChanged();
    46. }
    47. }
    复制代码
    由于我们已经规定了,事件引导层只处理 View 相关的操作,这样我们的 Activity 变得十分整洁,并且 Activity 只作为数据与事件的一个走向,Presenter 帮我们处理事件的具体细节。
    总结
    作为公司内部通用的开发框架,功能的选择上应保持最小原则只使用有必然需要的功能, 在架构上应该保持良好的扩展性。
    我相信你和我一样,在搭建框架的过程中遭遇着各式各样的挑战,从错误中吸取教训,不断优化代码,调整依赖关系,甚至重新组织模块结构,这些你做出的改变都是想让架构变得更健壮,我们一直希望应用程序能够变得易开发易维护,这才是真正意义上的团队受益。
    不得不说,搭建应用架构的方式多种多样,而且我认为,没有万能的,一劳永逸的架构,它应该是不断迭代更新,适应业务的。所以说,你可以按照文中提供的思路,尝试着结合业务来构建你的应用程序。
    最后,希望这篇文章能够对你有所帮助,如果你有其他更好的架构思路,欢迎分享或与我交流。
    作者:杀鱼能手小耗子 链接:https://www.jianshu.com/p/435d49a357f8 ,转载请注明原创
    关于我的更多

    </responsebean

    帖子地址: 

  • TA的每日心情
    难过
    前天 17:17
  • 签到天数: 3 天

    [LV.2]偶尔看看I

    0

    主题

    9

    帖子

    129

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    129
    发表于 2018-11-19 13:01:44 | 显示全部楼层
    啊啊啊啊啊啊啊啊啊啊啊
    回复

    使用道具 举报

    该用户从未签到

    0

    主题

    8

    帖子

    113

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    113
    发表于 2018-11-19 13:09:19 | 显示全部楼层
    广告位,,坐下看看
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2019-9-5 10:05
  • 签到天数: 2 天

    [LV.1]初来乍到

    0

    主题

    10

    帖子

    129

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    129
    发表于 2018-11-19 13:10:42 | 显示全部楼层
    老内容了,懒得回复了都~
    回复

    使用道具 举报

    该用户从未签到

    0

    主题

    13

    帖子

    113

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    113
    发表于 2018-11-19 14:25:24 | 显示全部楼层
    锄禾日当午,发帖真辛苦。谁知坛中餐,帖帖皆辛苦!
    回复

    使用道具 举报

    该用户从未签到

    0

    主题

    4

    帖子

    113

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    113
    发表于 2018-11-19 14:26:03 | 显示全部楼层
    1v1飘过
    回复

    使用道具 举报

  • TA的每日心情

    1567649121
  • 签到天数: 2 天

    0

    主题

    11

    帖子

    113

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    113
    发表于 2018-11-19 14:47:51 | 显示全部楼层
    确实不错,顶先
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    前天 20:08
  • 签到天数: 1 天

    [LV.1]初来乍到

    0

    主题

    17

    帖子

    133

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    133
    发表于 2018-11-19 14:48:24 | 显示全部楼层
    :lol
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2019-7-18 16:46
  • 签到天数: 1 天

    [LV.1]初来乍到

    0

    主题

    11

    帖子

    128

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    128
    发表于 2018-11-19 14:48:26 | 显示全部楼层
    我了个去,顶了
    回复

    使用道具 举报

    该用户从未签到

    0

    主题

    7

    帖子

    113

    积分

    Lv 2:论坛新兵

    Rank: 1

    积分
    113
    发表于 2018-11-19 14:48:58 | 显示全部楼层
    大人,此事必有蹊跷!
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    关闭

    站长推荐上一条 /2 下一条

    快速回复 返回顶部 返回列表