![]() |
VOOZH | about |
自我開始開發 Android 應用以來就有一種感覺——我可以把它做得更好。在我的職業生涯中,我看到過不少爛代碼,其中一些還是我自己寫的。Android 的複雜性和爛代碼勢必造成大問題。所以,從錯誤中汲取教訓並持續改善十分重要。在多次嘗試尋找更好的開發方式後,我遇到了 Clean 架構(簡潔架構)。於是我將其應用在了 Android 開發中,並結合我的開發經驗做了調整,寫出了這篇我覺得較為實用、值得分享的文章。
最近我用 Clean 架構為客戶構建了 app,並收到了很好的反饋。因此,在這篇文章中我會手把手教你如何用 Clean 架構開發 Android 應用。
有許多文章已經對 Clean 架構的概念做過介紹。在此我講一講 Clean 架構的核心內容。
通常所說的 Clean,是指代碼被分為像洋蔥狀的多個層,其規則基礎:內層不需要知道外層在幹什麼。即向內依賴。
這是上一段內容的直觀呈現:
簡潔架構極佳的視覺表現。圖片來自Uncle Bob。
文中提到的 Clean 架構會給代碼提供一下屬性:
不依賴框架。
可測試。
不依賴 UI。
不依賴資料庫。
不依賴其它外部力量
我希望你能理解這幾點在下面的示例中是如何體現的。更多關於 Clean 架構的解釋,我推薦你看看這篇文章和這個視頻。
一般來說,你的應用可以有任意數量的層,除非你的 Android 應用包含企業級的業務邏輯,最常見的是3層:
外層:實現層
中間層:接口適配層
內層:業務邏輯層
實現層是框架要求所有事情發生的地方。構架代碼包括每行代碼都不是在解決你要解決的問題,比如所有 Android 開發者都喜歡創建的 Activity 和 Fragment,發送 Intent,以及其它網絡和資料庫相關的框架代碼。
接口適配層的目標是連接業務邏輯和框架代碼。
最重要的問題是業務邏輯層。這裡是你的應用中實際解決問題的地方。這裡不會有框架代碼,你應該能在沒有模擬器支持下運行這部分代碼。這樣你的業務邏輯代碼才容易測試、開發和維護。這是 Clean 架構的主要優勢。
核心層之上的每一層都需要為下一層轉換模型結構。內層不會引用外層的模型,但外層可以使用內層的模型。這也是前面提到的依賴規則。雖然這樣做會導致更大的開銷,但能確保各層代碼之間的解耦。
為什麼需要模型轉換?舉個例子,當邏輯層的模型不能直接很優雅地展現給用戶,或是需要同時展示多個邏輯層的模型時,最好創建一個 ViewModel 類來更好的進行 UI 展示。這樣可以在外層使用轉換器類將業務模型轉換成合適的 ViewModel。
另一個例子:假設你要從外部數據層的 ContentProvider 得到一個 Cursor 對象,外層要先把它轉換成內層的業務模型,再送給你的業務邏輯層進行處理。
文末我會給出更多相關資源,以便你了解更多相關信息。現在我們已經了解 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 - 這些是您在業務邏輯中處理的業務模型。
在這個示例中,我們的用例是: 「在 app 啟動時讀取存儲在資料庫中的消息並展示。「 此示例將會展示如何使用下面三個程序包來完成用例的功能:
presentation 包(展示包)
storage 包(存儲包)
domain 包(主包)
前兩個屬於外層實現,最後一個屬於內部/核心層實現。
Presentation 包主要負責所有與屏幕顯示相關的部分——包括全部的 MVP 棧,即包括 UI 和 presenter 這兩個不同層的組件。
事實上你可以從架構的任意層開始編碼,但是我還是推薦你首先從核心業務邏輯開始。因為邏輯代碼寫好之後可以測試,不需要 activity 也可以正常運行。
所以我們先從創建一個 Interactor 開始。Interactor 是用例主邏輯實現的地方。所有的 Interactors 都運行在後台線程,因此應該不會對 UI 展示造成影響。 我們在這裡新建一個 Interactor,叫做 WelcomingInteractor。
Callback 負責和主線程中的 UI 交互,我們之所以將其放在 Interactor 接口中是因為我們不需要將其重新命名為 WelcomingInteractorCallback——用於將其與其他回調區分。下面讓我們實現取回消息的邏輯。假設我們有一個 Interactor 的 MessageRepository,可以給我們發送歡迎消息。
下面讓我們參考業務邏輯實現 Interactor 接口。我們的實現必須擴展自 AbstractInteractor,這樣代碼就能在後台執行了。
WelcomingInteractor 運行方法。
這裡嘗試獲取了數據,並發送消息或者錯誤碼到 UI 層用於顯示。我們通過 Callback 通知 UI,這個 Callback 扮演的是 presenter 的角色。這段代碼是我業務邏輯的關鍵。其他框架都是依賴於框架本身。
讓我們看一下 Interactor 究竟有哪些依賴:
正如你所看到的,這裡沒有提到任何 Android 代碼,這就是 Clean 架構的主要好處。你可以看到框架的獨立性。 另外,我們不需要關注 UI 或資料庫的細節,我們只是調用外層實現的接口方法。
測試 Interactor現在我們可以脫離仿真器運行並測試 Interator。來寫個簡單的 JUnit 測試確保它有效。
這個 Interactor 代碼並不知道它會用在 Android 應用中。這證明了上面提到的第二點——我們的業務邏輯是可測試的。
編寫展現層展現代碼屬於簡潔框架的外層。它由向用戶呈現界面的框架代碼組成。我們使用 MainActivity 類在用戶回到應用的時候向用戶顯示歡迎信息。
我們從 Presenter 和 View 開始寫界面。視圖需要乾的唯一一件事情就是顯示歡迎信息:
那麼,用戶回到應用的時候,應該如何開始 Interactor 呢?一切不嚴格相關的東西都應該放在 Presenter 類中。這有助於組織離散的關係並防止 Activity 變得臃腫。這包括所有用 Interator 運行的代碼。
在 MainActivity 類中重載 onResume 方法:
所有 Presenter 對象都要在實現 BasePresenter的時候實現 resume 方法。
注意:有些敏銳的讀者會發現我在 BasePresenter 接口中添加了 Android 的生命周期方法,即使 Presenter 在較低層。Presenter 不會獲知 UI 層的任何內容——比如它的生命周期。然而,我並沒有指定 Android 特定的 * 事件* ,因為每個 UI 都需要向用戶展示。想像一下,我調用的是 onUIShow 而不是 onResumt,結果會怎麼樣呢。一切運行良好,不是嗎?:)
所有的 Presenter 在繼承 BasePresenter 時都要實現 resume 方法。我們在 MainPresenter 的 Resume 方法中啟動 Interactor。
execute 方法會在後台線程中執行 WelcomingInteractorImpl的 run 方法。而 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 呢?,因為 MainActivity的 MainPresenter 實現了 Callback 接口:
我們監聽的事件來自於 Interactor 。這段代碼來自於 MainPresenter:
在代碼段中我們看到的 View 其實就是實現了 MainPresenter.View 接口的 MainActivity:
它負責顯示歡迎信息:
這差不多就是表示層的內容了。
repository 中的接口就在存儲層實現。所有資料庫相關的代碼都在這裡。倉庫模式只是表達數據來源。但我們的主要業務邏輯不在乎首數據的來源——不管它是來自資料庫、伺服器還是文本文件。
對於複雜的數據,你可以使用 ContentProviders 或者像 DBFlow 這樣的 ORM 工具處理。如果你需要從 Web 接收數據,那就會用到 Retrofit。如果你需要簡單的鍵值對存儲,那你會用到 SharedPreferences。不管怎樣,你需要選擇正確的工具。
我們的資料庫並不是真正的資料庫,它只是一個簡單的類,通過延遲來模擬:
就 WelcomingInteractor 而言,延遲的原因可能是由真實網絡或其他原因造成的,但它並不在乎,它只需要數據提供者實現 MessageRepository 接口就好。
這個示例已經放在GitHub上。各個類之間的調用關係總結如下:
MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity
注意這個控制流程,這非常重要:
Outer — Mid — Core — Outer — Core — Mid — Outer
在一個用例中多次訪問外層是很常見的事情。如果你要顯示點什麼,存儲點什麼並從 Web 訪問些什麼,控制流至少需要訪問外層三次。
對於我來說,這是迄今為止開發應用程式的最佳方式。解耦的代碼能讓人把注意力放在具體的問題上,而不受其他事件干擾。這是一個不錯的 SOLID 方法,但我們還需要一些時間適應。希望這篇文章的示例能讓你對該內容有進一步了解。
我還使用 Clean 架構建立了一個開源的成本跟蹤應用,它能展示一項應用的編碼。此應用並無創新內容,你若感興趣,可查看:成本跟蹤應用示例