自從開始開發安卓應用,我一直感覺我可以做得更好。我看過不少爛程式碼,其中當然有我寫的。安卓系統的複雜性加上爛程式碼勢必釀成災禍,所以從錯誤中成長就很重要。我Google瞭如何更好地開發應用,發現了這個叫做Clean架構的東西。於是我嘗試將它應用於安卓開發,根據我在類似專案中的經驗做了一些改善,寫出了這篇我覺得較為實用、值得分享的文章。
我會在這篇文章中手把手教你在Android應用中使用Clean架構。我最近一直用這種方式優雅地編寫應用。
什麼是Clean架構?
有許多文章已經很好地回答了這個問題。我在這裡講一講Clean架構的核心概念。
一般來說,在Clean架構中,程式碼被分層成洋蔥形,層層包裹,其中有一個依賴性規則:內層不能依賴外層,即內層不知道有關外層的任何事情,所以這個架構是向內依賴的。看個圖感受一下:
圖片由Bob大叔提供
Clean架構可以使你的程式碼有如下特性:
- 獨立於架構
- 易於測試
- 獨立於UI
- 獨立於資料庫
- 獨立於任何外部類庫
我將通過下面的例子解釋這些特性是怎麼來的。如果你想深入瞭解Clean架構,不妨看這篇文章和這個視訊
Clean在Android中如何表現
一般來說,一個應用可以有任意數目的層,但除非你的應用到處是企業級功能邏輯,一般需要這三層:
- 外層:實現層
- 中層:介面適配層
- 內層:邏輯層
介面實現層是體現架構細節的地方。實現架構的程式碼是所有不用來解決問題的程式碼,這包括所有與安卓相關的東西,比如建立Activity和Fragment,傳送Intent以及其他聯網與資料庫的架構相關的程式碼。
新增介面適配層的目的就是橋接邏輯層和架構層的程式碼。
最重要的是邏輯層,這裡包含了真正解決問題的程式碼。這一層不包含任何實現架構的程式碼,不用模擬器也應能執行這裡的程式碼。這樣一來你的邏輯程式碼就有了易於測試、開發和維護的優點。這就是Clean架構的一個主要的好處。
每一個位於核心層外部的層都應能將外部模型轉成可以被內層處理的內部模型。內層不能持有屬於外層的模型類的引用。這也是由於剛才說的依賴性規則,這樣內外層可以很好地分離。
為什麼要進行模型轉換呢?舉個例子,當邏輯層的模型不能直接很優雅地展現給使用者,或是需要同時展示多個邏輯層的模型時,最好建立一個ViewModel類來更好的進行UI展示。這樣一來,你就需要一個屬於外層的Converter類來將邏輯層模型轉換成合適的ViewModel。
再舉一個例子:你從外部資料庫層獲得了ContentProvider的Cursor物件,外層首先要將這個物件轉換成內層模型,再將它傳給內層處理。
在文章的最後我還提供了一些學習資源。我們已經知道了Clean架構的基本原則,現在我們來實踐一下。我會在下一部分中使用Clean架構構建一個示例功能。
如何開始寫Clean應用?
我已經寫好了一個樣板專案,裡面把準備工作做好了。這相當於是一個Clean的底層包,可以直接在它的基礎上進行開發。請隨意下載、修改。專案包:Android Clean Boilerplate
開始寫用例
這一部分會詳細說明如何用在樣例專案的基礎之上以Clean方式進行開發。首先讓我們看一下應用的結構,當這只是我的習慣,不需要完全按這個進行。
結構
一般來說一個安卓應用的結構如下:
- 外層專案包:UI,Storage,Network等等。
- 中層專案包:Presenter,Converter。
- 內層專案包:Interactor,Model,Repository,Executor。
看不懂不要緊,下面有具體解釋。
外層
外層體現了框架的細節。
UI – 包括所有的Activity,Fragment,Adapter和其他UI相關的Android程式碼。
Storage – 用於讓互動類獲取和儲存資料的介面實現類,包含了資料庫相關的程式碼。包括瞭如ContentProvider或DBFlow等元件。
Network – 網路操作。
中層
橋接實現程式碼與邏輯程式碼的Glue Code。
Presenter – presenter處理UI事件,如單擊事件,通常包含內層Interactor的回撥方法。
Converter – 負責將內外層的模型互相轉換。
內層
內層包含了最高階的程式碼,裡面都是POJO類,這一層的類和物件不知道外層的任何資訊,且應能在任何JVM下執行。
Interactor – Interactor中包含了解決問題的邏輯程式碼。這裡的程式碼在後臺執行,並通過回撥方法向外層傳遞事件。在其他專案中這個模組被稱為用例Use Case。一個專案中可能有很多小Interactor,這符合單一職責原則,而且這樣更容易讓人接受。
Model – 在業務邏輯程式碼中操作的業務模型。
Repository – 包含介面讓外層類實現,如運算元據庫的類等。Interactor用這些介面的實現類來讀取和儲存資料。這也叫資源庫模式Repository Pattern。
Executor – 通過Worker Thread Executor讓Interactor在後臺執行。一般不需要修改這個包裡的程式碼。
以下是例子
在這個簡單例子中,我們的use case是在應用啟動時讀取資料庫中的歡迎語句並展示。下面演示如何編寫程式碼包讓use case執行起來。
- presentation包
- storage包
- domain包
前兩個包屬於外層,最後一個包屬於內層(核心層)。
presentation包負責將資訊展示在螢幕上,而且包含整個MVP棧,即同時包含UI和presenter這兩個屬於不同層的元件。下面上碼。
寫一個內層的Interactor
你可以從任何一層開始編寫,我建議從內層的邏輯程式碼寫起。因為邏輯程式碼寫好之後可以測試,不需要activity也可以正常執行。
所以我們先寫一個Interactor,這個Interactor包含了處理業務邏輯的程式碼。**所有的Interactor都應該在後臺執行,而不應影響UI展示。**我在這裡先編寫一個WelcomingInteractor。
1 2 3 4 5 6 |
public interface WelcomingInteractor extends Interactor { interface Callback { void onMessageRetrieved(String message); void onRetrievalFailed(String error); } } |
Callback負責與主執行緒的UI元件聯通。將它放在WelcomingInteractor中可以避免給所有Callback介面起不同的名字而又能將它們有效區分。而後我們要實現獲取訊息的邏輯。現在已經有一個介面MessageRepository用於獲取資料:
1 2 3 |
public interface MessageRepository { String getWelcomeMessage(); } |
現在我們可以用業務邏輯程式碼來實現Interactor介面了。注意要實現AbstractInteractor介面,這樣程式碼就會在後臺執行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor { ... private void notifyError() { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onRetrievalFailed("Nothing to welcome you with :("); } }); } private void postMessage(final String msg) { mMainThread.post(new Runnable() { @Override public void run() { mCallback.onMessageRetrieved(msg); } }); } @Override public void run() { // 獲取訊息 final String message = mMessageRepository.getWelcomeMessage(); // 檢查是否獲取失敗 if (message == null || message.length() == 0) { // 在主執行緒中通知錯誤 notifyError(); return; } // 已成功獲取訊息,通知UI postMessage(message); } } |
這段程式碼獲取了資料,並向UI層傳送資料或報錯。這裡通過Callback向UI傳送資訊,這個Callback扮演的是presenter的角色。這段程式碼是邏輯的核心,其他程式碼都是依賴框架的。看一下這個類的引用:
1 2 3 4 5 |
import com.kodelabs.boilerplate.domain.executor.Executor; import com.kodelabs.boilerplate.domain.executor.MainThread; import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor; import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor; import com.kodelabs.boilerplate.domain.repository.MessageRepository; |
可以看到,沒有和Android相關的類庫,這就是Clean架構的好處。還有就是寫邏輯程式碼時不需要關心UI或資料庫,只需要呼叫外層實現的Callback的回撥方法。
測試Interactor
現在不需要模擬器也可以執行這段程式碼了,我們編寫一個JUnit測試來確保這段程式碼執行正常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<a href="http://www.jobbole.com/members/madao" data-mce-href="http://www.jobbole.com/members/madao">@Test</a> public void testWelcomeMessageFound() throws Exception { String msg = "Welcome, friend!"; when(mMessageRepository.getWelcomeMessage()).thenReturn(msg); WelcomingInteractorImpl interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, mMockedCallback, mMessageRepository ); interactor.run(); Mockito.verify(mMessageRepository).getWelcomeMessage(); Mockito.verifyNoMoreInteractions(mMessageRepository); Mockito.verify(mMockedCallback).onMessageRetrieved(msg); } |
重複一遍,Interactor根本不知道它在Android環境下執行。
編寫presentation層
Presentation層在Clean架構中屬於外層的範圍,它依賴於框架,包含了UI展示的程式碼。我們用MainActivity類在應用啟動時展示歡迎資訊。
首先編寫Presenter和View的介面。View只需要展示歡迎資訊。
1 2 3 4 5 |
public interface MainPresenter extends BasePresenter { interface View extends BaseView { void displayWelcomeMessage(String msg); } } |
那怎麼在App啟動時執行Interactor呢?所有和View無關的程式碼都寫進Presenter類中。這樣可以實現關注分離(Separation of Concerns)並能避免Activity過於複雜。這些程式碼包括和Interactor互動的程式碼。
在MainActivity中重寫onResume()方法。
1 2 3 4 5 6 7 |
@Override protected void onResume() { super.onResume(); // 在活動resume時開始獲取資料 mPresenter.resume(); } |
所有的Presenter在繼承BasePresenter時都要實現resume()方法。我們在MainPresenter的onResume()方法中啟動Interactor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public void resume() { mView.showProgress(); // 初始化Interactor WelcomingInteractor interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, this, mMessageRepository ); // 執行interactor interactor.execute(); } |
execute()方法會在後臺執行緒中呼叫WelcomingInteractorImpl類的run()方法。run()方法的實現可以看上文寫一個內層的Interactor部分。
你可能已經發現Interactor很像AsyncTask,都是提供所有需要的東西然後執行。那為什麼不用AsyncTask呢?因為AsyncTask是Android的程式碼,需要模擬器來執行與測試。
在上面的程式碼中我們給Interactor傳入了下列屬性:
- ThreadExecutor物件:用於在後臺執行緒執行Interactor。我喜歡將這個類設計成單例。這個類屬於domain包,不需要在外層實現。
- MainThreadImpl物件:用於在主執行緒中執行Interactor的Runnable物件。在依賴框架的外層程式碼中我們可以訪問主執行緒,所以這個類要在外層實現。
- 我們傳入this是因為MainPresenter也是一個Callback物件,Interactor要通過Callback來更新UI。
- 我們傳入實現了MessageRepository介面的WelcomMessageRepository物件讓Interactor使用。下面會講到WelcomMessageRepository。
為什麼this也是Callback呢?因為MainActivity的MainPresenter實現了Callback介面:
1 2 |
public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback { |
我們就是這麼監聽Interactor的事件的。下面是MainPresenter的程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
@Override public void onMessageRetrieved(String message) { mView.hideProgress(); mView.displayWelcomeMessage(message); } @Override public void onRetrievalFailed(String error) { mView.hideProgress(); onError(error); } |
在程式碼段中我們看到的View其實就是實現了MainPresenter.View介面的MainActivity:
1 |
public class MainActivity extends AppCompatActivity implements MainPresenter.View { |
View用於展示訊息:
1 2 3 4 |
@Override public void displayWelcomeMessage(String msg) { mWelcomeTextView.setText(msg); } |
Presentation層的東西就這麼多了。
編寫Storage層
repository中的介面就在storage層實現。所有與資料庫相關的程式碼都在這裡。資源庫模式下資料的來源是不確定的,意思是邏輯程式碼不關心資料的來源,不論是資料庫、伺服器還是檔案。
你可以用ContentProvider或DBFlow等ORM工具處理更復雜的資料。如果你需要從網路獲取資料那你可以用Retrofit。如果你只需要基本的鍵值對儲存那你可以用SharedPreferences。不管怎樣,一定要選對工具。
這裡我們的資料庫不是真正的資料庫,只是一個模擬了延遲的一個很簡單的類。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class WelcomeMessageRepository implements MessageRepository { @Override public String getWelcomeMessage() { String msg = "Welcome, friend!"; // 模擬網路/資料庫延遲 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return msg; } } |
WelcomingInteractor可能以為延遲是網路或其他原因造成的,但它並不關心,它只需要資料提供者實現了MessageRepository介面。
總結
詳細程式碼請看這個git repo。總結一下各個類的觸發順序:
1 |
MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity |
控制流的順序:
1 |
Outer — Mid — Core — Outer — Core — Mid — Outer |
在一個use case中多次訪問外層很正常。比如當你要顯示、儲存加訪問網路,你的控制流會訪問外層至少三次。