1. 程式人生 > >我10年的Android重構之旅:框架篇

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

指導 findview dsta 阻止 不同界面 需求 相對 turn 取出

在我這幾年的學習和成長中,慢慢的意識到搭建一個優秀的 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 幫我們處理事件的具體細節。

總結

作為公司內部通用的開發框架,功能的選擇上應保持最小原則只使用有必然需要的功能,

在架構上應該保持良好的擴展性。

我相信你和我一樣,在搭建框架的過程中遭遇著各式各樣的挑戰,從錯誤中吸取教訓,不斷優化代碼,調整依賴關系,甚至重新組織模塊結構,這些你做出的改變都是想讓架構變得更健壯,我們一直希望應用程序能夠變得易開發易維護,這才是真正意義上的團隊受益。

不得不說,搭建應用架構的方式多種多樣,而且我認為,沒有萬能的,一勞永逸的架構,它應該是不斷叠代更新,適應業務的。所以說,你可以按照文中提供的思路,嘗試著結合業務來構建你的應用程序。

最後,希望這篇文章能夠對你有所幫助,如果你有其他更好的架構思路,歡迎分享或與我交流

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