我的Android重构之旅:框架篇

我的Android重构之旅:架构篇
我的Android重构之旅:框架篇
我的Android重构之旅:插件化篇

在我这几年的学习和成长中,慢慢的意识到搭建一个优秀的 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 中定义了一切能想得到的子类变量等等,它现在确实成为了“上帝”,方便且无所不能的上帝!
    随着项目的发展,它已经庞大到无法继续添加代码了,于是你写了很多很多的帮助类来帮助这个上帝瘦下来:


    god activity

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

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

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

Clean architecture and The Clean rule

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

Clean Architecture

我们已经选用 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


    Library

Util、Instance

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

Base

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


Base Module
BaseActivity

BaseActivity 主要功能被分为:

  • ActivityMvp 提供上下文

  • ViewResult 提供跨界面刷新

  • ActivityToolbarBase 提供顶部栏

  • ViewLayoutState 提供切换界面

  • LifecycleCallbackStrategy 生命周期回调管理

  • BaseActivity

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

/**
 * 全局刷新
 */
@Override
public void resultAll() {
    presenter.resultAll();
}

/**
 * 部分刷新
 *
 * @param resultData
 */
@Override
public 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 帮我们处理事件的具体细节。

总结

作为公司内部通用的开发框架,功能的选择上应保持最小原则只使用有必然需要的功能,
在架构上应该保持良好的扩展性。

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,421评论 25 707
  • 在我这几年的学习和成长中,深刻的意识到搭建一个Android应用架构是件非常痛苦的事,它不仅要满足不断增长的业务需...
    小鄧子阅读 11,723评论 24 110
  • 国际惯例先从Uncle Bob的文章开始谈起: Bob提取出来大部分架构所需要的准则: 框架独立。架构不依赖于一些...
    谷歌清洁工阅读 10,608评论 17 42
  • 11一21,2017年6月10日,让爱醒来践行 1,恭敬一切缘,感恩一切缘。出发东莞,路上无论遇到谁,都能心怀恭敬...
    为_福慧阅读 187评论 0 0
  • 自己作了快一个月了,每天头都是晕乎乎的。总是无精打采,其实只是自己为自己的懒惰找理由。想要遇到更好的人之前努力把自...
    丢丢明阅读 165评论 0 0