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

來源:互聯網
上載者:User

標籤:pen   ram   領域   資料類型   應用程式   使用外部   全域   bundle   protected   

在我這幾年的學習和成長中,慢慢的意識到搭建一個優秀的 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重構之旅:架構篇

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.