VOOZH about

URL: https://read01.com/MEGMoa.html

⇱ 如何用 Clean 架構開發 Android 應用 - 壹讀


Sunday, Apr 12, 2026

如何用 Clean 架構開發 Android 應用

2016/12/23 來源:伊甸園開源社區

自我開始開發 Android 應用以來就有一種感覺——我可以把它做得更好。在我的職業生涯中,我看到過不少爛代碼,其中一些還是我自己寫的。Android 的複雜性和爛代碼勢必造成大問題。所以,從錯誤中汲取教訓並持續改善十分重要。在多次嘗試尋找更好的開發方式後,我遇到了 Clean 架構(簡潔架構)。於是我將其應用在了 Android 開發中,並結合我的開發經驗做了調整,寫出了這篇我覺得較為實用、值得分享的文章。

最近我用 Clean 架構為客戶構建了 app,並收到了很好的反饋。因此,在這篇文章中我會手把手教你如何用 Clean 架構開發 Android 應用。

什麼是 Clean 架構?

有許多文章已經對 Clean 架構的概念做過介紹。在此我講一講 Clean 架構的核心內容。

通常所說的 Clean,是指代碼被分為像洋蔥狀的多個層,其規則基礎:內層不需要知道外層在幹什麼。即向內依賴

這是上一段內容的直觀呈現:

👁 Image
...

簡潔架構極佳的視覺表現。圖片來自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) new@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 類在用戶回到應用的時候向用戶顯示歡迎信息。

我們從 PresenterView 開始寫界面。視圖需要乾的唯一一件事情就是顯示歡迎信息:

public interface MainPresenter extends BasePresenter { interface View extends BaseView { void displayWelcomeMessage(String msg); } }

那麼,用戶回到應用的時候,應該如何開始 Interactor 呢?一切不嚴格相關的東西都應該放在 Presenter 類中。這有助於組織離散的關係並防止 Activity 變得臃腫。這包括所有用 Interator 運行的代碼。

MainActivity 類中重載 onResume 方法:

@Overrideprotected 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 方法。我們在 MainPresenterResume 方法中啟動 Interactor。

@Overridepublic void resume { mView.showProgress; // initialize the interactor WelcomingInteractor interactor = new WelcomingInteractorImpl( mExecutor, mMainThread, this, mMessageRepository ); // run the interactor interactor.execute; }

execute 方法會在後台線程中執行 WelcomingInteractorImplrun 方法。而 run 方法在編寫新的 Interactor 一節中會有介紹。

你可能注意到 Interactor 的行為與 AsyncTask 相類似,都是在提供所需東西後運行。那為什麼不使用 AsyncTask 呢?因為這是 Android 代碼,需要模擬器才能運行或測試。

我們為 Interfactor 提供下列屬性:

  • ThreadExecutor 實例負責在後台線程中執行 Interactor。我通常會使用單例模式。這個類實際駐留在包中,不需要在外層實現。

  • MainThreadImpl 實例負責在主線程上從 Interactor 發送可運行對象。主線程可以使用框架代碼訪問,因此這個類需要在外層實現。

  • 你可能注意到我們向 Interactor 提供了 this ,因為 MainPresenter 也是一個 Callback 對象,Interactor 會用它在事件回調中更新 UI。

  • WelcomeMessageRepository 實現了 Interactor 用到的 MessageRepository 接口,所以我們提供了它的實例。 會在編寫存儲層 一節中詳述。

注意:因為每次都需要向 Interactor 提供許多屬性,將 Dagger 2 依賴注入框架會提供不少幫助。簡明起見,此處沒有將其注入。你可根據實際情況選擇使用。

為什麼 this 也是 Callback 呢?,因為 MainActivityMainPresenter 實現了 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 架構建立了一個開源的成本跟蹤應用,它能展示一項應用的編碼。此應用並無創新內容,你若感興趣,可查看:成本跟蹤應用示例

您可能感興趣
免責聲明:本文內容來源于伊甸園開源社區,文章觀點不代表壹讀立場,如若侵犯到您的權益,或涉不實謠言,敬請向我們提出檢舉
最新文章 / 服務條款 / 私隱保護 / DMCA / 聯絡我們

壹讀/READ01.COM