如何用 Clean 架構開發 Android 應用
自我開始開發 Android 應用以來就有一種感覺——我可以把它做得更好。在我的職業生涯中,我看到過不少爛代碼,其中一些還是我自己寫的。Android 的復雜性和爛代碼勢必造成大問題。所以,從錯誤中汲取教訓并持續改善十分重要。在多次嘗試尋找更好的開發方式后,我遇到了 Clean 架構( 簡潔架構) 。于是我將其應用在了 Android 開發中,并結合我的開發經驗做了調整,寫出了這篇我覺得較為實用、值得分享的文章。
最近我用 Clean 架構為客戶構建了 app,并收到了很好的反饋。因此,在這篇文章中我會手把手教你如何用 Clean 架構開發 Android 應用。
什么是 Clean 架構?
有許多文章已經對 Clean 架構的概念做過介紹。在此我講一講 Clean 架構的核心內容。
通常所說的 Clean,是指代碼被分為像洋蔥狀的多個層,其 規則基礎 :內層不需要知道外層在干什么。即 向內依賴 。
這是上一段內容的直觀呈現:
簡潔架構極佳的視覺表現。圖片來自 Uncle Bob 。
文中提到的 Clean 架構會給代碼提供一下屬性:
-
不依賴框架。
-
可測試。
-
不依賴 UI。
-
不依賴數據庫。
-
不依賴其它外部力量
我希望你能理解這幾點在下面的示例中是如何體現的。更多關于 Clean 架構的解釋,我推薦你看看這篇 文章 和這個 視頻 。
這在 Anroid 中意味著什么
一般來說,你的應用可以有任意數量的層,除非你的 Android 應用包含企業級的業務邏輯,最常見的是3層:
-
外層:實現層
-
中間層:接口適配層
-
內層:業務邏輯層
實現層是框架要求所有事情發生的地方。構架代碼 包括每行代碼都不是在解決你要解決的問題 ,比如所有 Android 開發者都喜歡創建的 Activity 和 Fragment,發送 Intent,以及其它網絡和數據庫相關的框架代碼。
接口適配層的目標是連接業務邏輯和框架代碼。
最重要的問題是 業務邏輯層 。這里是你的應用中實際解決問題的地方。這里不會有框架代碼,你 應該能在沒有模擬器支持下運行這部分代碼 。這樣你的業務邏輯代碼才 容易測試、開發和維護 。這是 Clean 架構的主要優勢。
核心層之上的每一層都需要為下一層轉換模型結構。內層不會引用外層的模型,但外層可以使用內層的模型。這也是前面提到的 依賴規則 。雖然這樣做會導致更大的開銷,但能確保各層代碼之間的解耦。
為什么需要模型轉換? 舉個例子,當邏輯層的模型不能直接很優雅地展現給用戶,或是需要同時展示多個邏輯層的模型時,最好創建一個 ViewModel 類來更好的進行 UI 展示。這樣可以在外層使用轉換器類將業務模型轉換成合適的 ViewModel。
另一個例子:假設你要從外部數據層的 ContentProvider 得到一個 Cursor 對象,外層要先把它轉換成內層的業務模型,再送給你的業務邏輯層進行處理。
文末我會給出更多相關資源,以便你了解更多相關信息。現在我們已經了解 Clean 架構的基本原理,接下來我們需要用代碼示例進行說明:用 Clean 架構構建一個示例功能。
怎樣開始構建一個 Clean 應用?
我做了一個 樣板項目 ,它為你提供了所有的底層命令。這是一個 Clean 啟動包 ,在設計之初就包含最常用的一些工具包。你可 免費 下載和修改,還能用它建立自己的應用程序。
你可以在這里找到入門項目: Android Clean Boilerplate
開始編寫新用例
本節將解釋所有需要編寫的代碼,你可通過上一節提供的樣板文件使用 Clean 方法創建一個示例。 一個示例只代表應用程序中的部分獨立功能。 用戶(例如,在點擊時)可以選擇啟用或不啟用。
首先我們來解釋這種方法的結構和術語。這里要說的是我如何構建應用程序,其方法并不固定,你可根據你的需求組織不同的結構。
結構
一般的 Android 應用結構如下:
-
外層包:UI、Storage、Network 等。
-
中層包:Presenters, Converters
-
內層包:Interactors、Models、Repositories、Executor
外層
上面已經提到過,這里是框架的細節。
UI —包括 Activite、Fragment、Adapter 和其它用戶界面相關的代碼。
Storage —數據庫相關代碼,實現 Interactor 需要使用的接口,用于訪問和存儲數據。包含如 ContentProviders 或者像 DBFlow 這樣的 ORM。
Network —類似 Retrofit 的網絡操作。
中層
粘合代碼層,將實現細節與業務邏輯連接起來。
Presenters —處理來自 UI 的事件(比如用戶單擊)或者常用作內層(Interactor)的回調。
Converters —轉換器對象負責把內部模型轉換為外部模型,反之亦然。
內層
核心層包含大部分高等級代碼。 這里的所有類都是 POJO 。這一層中的類和對象都不是特定運行在 Android 應用中,可以非常容易的移植到其它 JVM 運行。
Interactors- 這些是實際 包含業務邏輯代碼 的類。這些類在后臺運行,并使用回調向上層傳遞事件。在一些項目中,它們也被稱為用例(可能是一個合適的名稱)。在您的項目中可能有很多小的用于解決特定問題 Interactor 類,這屬正常現象。可以說,它符合 單一責任原則 ,而且這樣的理解更容易讓人接受。
Models- 這些是您在業務邏輯中處理的業務模型。
Repositories - 此包僅包含數據庫或其他外層實現的接口。Interactors 使用這些接口來訪問和存儲數據。也稱為 倉庫模式 。
Executor- 此包包含用于調用工作線程執行器在后臺執行 Interactors 的代碼。這個包一般不需要你修改任何部分。
一個簡單的示例
在這個示例中,我們的用例是: “在 app 啟動時讀取存儲在數據庫中的消息并展示。“ 此示例將會展示如何使用下面三個程序包來完成用例的功能:
-
presentation 包(展示包)
-
storage 包(存儲包)
-
domain 包(主包)
前兩個屬于外層實現,最后一個屬于內部/核心層實現。
Presentation 包主要負責所有與屏幕顯示相關的部分——包括全部的 MVP 棧,即包括 UI 和 presenter 這兩個不同層的組件。
編寫新的 Interactor (內部/核心層)
事實上你可以從架構的任意層開始編碼,但是我還是推薦你首先從核心業務邏輯開始。因為邏輯代碼寫好之后可以測試,不需要 activity 也可以正常運行。
所以我們先從創建一個 Interactor 開始。Interactor 是用例主邏輯實現的地方。 所有的 Interactors 都運行在后臺線程,因此應該不會對 UI 展示造成影響。 我們在這里新建一個 Interactor,叫做 WelcomingInteractor 。
public interface WelcomingInteractor extends Interactor {
interface Callback {
void onMessageRetrieved(String message);
void onRetrievalFailed(String error);
}
}
Callback 負責和主線程中的 UI 交互,我們之所以將其放在 Interactor 接口中是因為我們不需要將其重新命名為 WelcomingInteractorCallback——用于將其與其他回調區分。下面讓我們實現取回消息的邏輯。假設我們有一個 Interactor 的 MessageRepository ,可以給我們發送歡迎消息。
MessageRepository {
String getWelcomeMessage();
}
下面讓我們參考業務邏輯實現 Interactor 接口。 我們的實現必須擴展自 AbstractInteractor,這樣代碼就能在后臺執行了。
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() { // retrieve the message
final String message = mMessageRepository.getWelcomeMessage(); // check if we have failed to retrieve our message
if (message == null || message.length() == 0) { // notify the failure on the main thread
notifyError(); return;
} // we have retrieved our message, notify the UI on the main thread
postMessage(message);
}
WelcomingInteractor 運行方法。
這里嘗試獲取了數據,并發送消息或者錯誤碼到 UI 層用于顯示。我們通過 Callback 通知 UI,這個 Callback 扮演的是 presenter 的角色。 這段代碼是我業務邏輯的關鍵。其他框架都是依賴于框架本身。
讓我們看一下 Interactor 究竟有哪些依賴:
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 或數據庫的細節,我們只是調用外層實現的接口方法。
測試 Interactor
現在我們可以 脫離仿真器運行并測試 Interator 。來寫個簡單的 JUnit 測試確保它有效。
... @Test
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 應用中。這證明了上面提到的第二點——我們的業務邏輯是 可測試的 。
編寫展現層
展現代碼屬于簡潔框架的 外層 。它由向用戶呈現界面的框架代碼組成。我們使用 MainActivity 類在用戶回到應用的時候向用戶顯示歡迎信息。
我們從 Presenter 和 View 開始寫界面。視圖需要干的唯一一件事情就是顯示歡迎信息:
public interface MainPresenter extends BasePresenter {
interface View extends BaseView {
void displayWelcomeMessage(String msg);
}
}
那么,用戶回到應用的時候,應該如何開始 Interactor 呢?一切不嚴格相關的東西都應該放在 Presenter 類中。這有助于組織 離散的關系 并防止 Activity 變得臃腫。這包括所有用 Interator 運行的代碼。
在 MainActivity 類中重載 onResume() 方法:
@Override
protected void onResume() {
super.onResume();
// let's start welcome message retrieval when the app resumes
mPresenter.resume();
}
所有 Presenter 對象都要在實現 BasePresenter 的時候實現 resume() 方法。
注意: 有些敏銳的讀者會發現我在 BasePresenter 接口中添加了 Android 的生命周期方法,即使 Presenter 在較低層。Presenter 不會獲知 UI 層的任何內容——比如它的生命周期。然而,我并沒有指定 Android 特定的 * 事件 * ,因為每個 UI 都需要向用戶展示。想像一下,我調用的是 onUIShow() 而不是 onResumt(), 結果會怎么樣呢。一切運行良好,不是嗎?:)
所有的 Presenter 在繼承 BasePresenter 時都要實現 resume() 方法。我們在 MainPresenter 的 Resume() 方法中啟動 Interactor。
@Override
public void resume() {
mView.showProgress();
// initialize the interactor
WelcomingInteractor interactor = new WelcomingInteractorImpl(
mExecutor,
mMainThread,
this,
mMessageRepository
);
// run the interactor
interactor.execute();
}
execute() 方法會在后臺線程中執行 WelcomingInteractorImpl 的 run() 方法。而 run() 方法在 編寫新的 I nteractor 一節中會有介紹。
你可能注意到 Interactor 的行為與 AsyncTask 相類似,都是在提供所需東西后運行。那為什么不使用 AsyncTask 呢?因為這是 Android 代碼 ,需要模擬器才能運行或測試。
我們為 Interfactor 提供下列屬性:
-
ThreadExecutor實例負責在后臺線程中執行 Interactor。我通常會使用單例模式。這個類實際駐留在 域 包中,不需要在外層實現。
-
MainThreadImpl實例負責在主線程上從 Interactor 發送可運行對象。主線程可以使用框架代碼訪問,因此這個類需要在外層實現。
-
你可能注意到我們向 Interactor 提供了 this , 因為 MainPresenter 也是一個 Callback 對象,Interactor 會用它在事件回調中更新 UI。
-
WelcomeMessageRepository實現了 Interactor 用到的 MessageRepository 接口,所以我們提供了它的實例。 WelcomeMessageRepository 會在 編寫存儲層 一節中詳述。
注意: 因為每次都需要向 Interactor 提供許多屬性,將 Dagger 2 依賴注入框架會提供不少幫助。簡明起見,此處沒有將其注入。你可根據實際情況選擇使用。
為什么 this 也是 Callback 呢?,因為 MainActivity 的 MainPresenter 實現了 Callback 接口:
public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback {
我們監聽的事件來自于 Interactor 。這段代碼來自于 MainPresenter:
@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 :
public class MainActivity extends AppCompatActivity implements MainPresenter.View {
它負責顯示歡迎信息:
@Override
public void displayWelcomeMessage(String msg) {
mWelcomeTextView.setText(msg);
}
這差不多就是表示層的內容了。
編寫存儲層
repository 中的接口就在存儲層實現。所有數據庫相關的代碼都在這里。倉庫模式只是表達數據來源。但我們的主要業務邏輯不在乎首數據的來源——不管它是來自數據庫、服務器還是文本文件。
對于復雜的數據,你可以使用 ContentProviders 或者像 DBFlow 這樣的 ORM 工具處理。如果你需要從 Web 接收數據,那就會用到 Retrofit 。如果你需要簡單的鍵值對存儲,那你會用到 SharedPreferences 。不管怎樣,你需要選擇正確的工具。
我們的數據庫并不是真正的數據庫,它只是一個簡單的類,通過延遲來模擬:
public class WelcomeMessageRepository implements MessageRepository {
@Override
public String getWelcomeMessage() {
String msg = "Welcome, friend!"; // let's be friendly
// let's simulate some network/database lag
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return msg;
}
}
就 WelcomingInteractor 而言,延遲的原因可能是由真實網絡或其他原因造成的,但它并不在乎,它只需要數據提供者實現 MessageRepository 接口就好。
這個示例已經 放在 GitHub上。各個類之間的調用關系總結如下:
MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity
注意這個控制流程,這非常重要:
Outer — Mid — Core — Outer — Core — Mid — Outer
在一個用例中多次訪問外層是很常見的事情。如果你要顯示點什么,存儲點什么并從 Web 訪問些什么,控制流至少需要訪問外層三次。
對于我來說,這是迄今為止開發應用程序的最佳方式。解耦的代碼能讓人把注意力放在具體的問題上,而不受其他事件干擾。這是一個不錯的 SOLID 方法,但我們還需要一些時間適應。希望這篇文章的示例能讓你對該內容有進一步了解。
我還使用 Clean 架構建立了一個開源的成本跟蹤應用,它能展示一項應用的編碼。
來自:https://www.oschina.net/translate/developing-android-apps-using-the-clean-architecture-pattern