本文以一個真實項目的業務場景爲載體,描述了經歷一次次重構後,代碼變得愈來愈複雜(you ya)的過程。java
本篇 Demo 的業務場景是:從服務器拉取新聞並在列表展現。android
剛接觸 Android 時,我是這樣寫業務代碼的(省略了和主題無關的 Adapter 和 Api 細節):git
class GodActivity : AppCompatActivity() {
private var rvNews: RecyclerView? = null private var newsAdapter = NewsAdapter() // 用 retrofit 拉取數據 private val retrofit = Retrofit.Builder() .baseUrl("https://api.apiopen.top") .addConverterFactory(MoshiConverterFactory.create()) .client(OkHttpClient.Builder().build()) .build() private val newsApi = retrofit.create(NewsApi::class.java) // 數據庫操做異步執行器 private var dbExecutor = Executors.newSingleThreadExecutor() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.news_activity) initView() fetchNews() } private fun initView() { rvNews = findViewById(R.id.rvNews) rvNews?.layoutManager = LinearLayoutManager(this) } // 列表展現新聞 private fun showNews(news : List<News>) { newsAdapter.news = news rvNews?.adapter = newsAdapter } // 獲取新聞 private fun fetchNews() { // 1. 先從數據庫讀老新聞以快速展現 queryNews().let{ showNews(it) } // 2. 再從網絡拉新聞替換老新聞 newsApi.fetchNews( mapOf("page" to "1","count" to "4") ).enqueue(object : Callback<NewsBean> { override fun onFailure(call: Call<NewsBean>, t: Throwable) { Toast.makeText(this@GodActivity, "network error", Toast.LENGTH_SHORT).show() } override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) { response.body()?.result?.let { // 3. 展現新新聞 showNews(it) // 4. 將新聞入庫 dbExecutor.submit { insertNews(it) } } } }) } // 從數據庫讀老新聞(僞代碼) private fun queryNews() : List<News> { val dbHelper = NewsDbHelper(this, ...) val db = dbHelper.getReadableDatabase() val cursor = db.query(...) var newsList = mutableListOf<News>() while(cursor.moveToNext()) { ... newsList.add(news) } db.close() return newsList } // 將新聞寫入數據庫(僞代碼) private fun insertNews(news : List<News>) { val dbHelper = NewsDbHelper(this, ...) val db = dbHelper.getWriteableDatabase() news.foreach { val cv = ContentValues().apply { ... } db.insert(cv) } db.close() } } 複製代碼
畢竟當時的關注點是實現功能,首要解決的問題是「如何繪製佈局」、「如何操縱數據庫」、「如何請求並解析網絡數據」、「如何將數據填充在列表中」。待這些問題解決後,也沒時間思考架構,因此就產生了上面的God Activity
。Activity 管的太多了!Activity 知道太多細節:github
若是大量 「細節」 在同一個層次被鋪開,就顯得囉嗦,增長理解成本。
拿說話打個比方:web
你問 「晚飯吃了啥?」數據庫
「我用勺子一口一口地吃了雞生下的蛋和番茄再加上油一塊兒炒的菜。」編程
聽了這樣地回答,你還會和他作朋友嗎?其實你並不關心他吃的工具、吃的速度、食材的來源,以及烹飪方式。api
與 「細節」 相對的是 「抽象」,在編程中 「細節」 易變,而 「抽象」 相對穩定。
好比 「異步」 在 Android 中就有好幾種實現方式:線程池、HandlerThread
、協程、IntentService
、RxJava
。緩存
「細節」 增長耦合。
GodActivity 引入了大量本和它無關的類:Retrofit
、Executors
、ContentValues
、Cursor
、SQLiteDatabase
、Response
、OkHttpClient
。Activity 本應該只和界面展現有關。服務器
既然 Activity 知道太多,那就讓Presenter
來爲它分擔:
// 構造 Presenter 時傳入 view 層接口 NewsView
class NewsPresenter(var newsView: NewsView): NewsBusiness { private val retrofit = Retrofit.Builder() .baseUrl("https://api.apiopen.top") .addConverterFactory(MoshiConverterFactory.create()) .client(OkHttpClient.Builder().build()) .build() private val newsApi = retrofit.create(NewsApi::class.java) private var executor = Executors.newSingleThreadExecutor() override fun fetchNews() { // 將數據庫新聞經過 view 層接口通知 Activity queryNews().let{ newsView.showNews(it) } newsApi.fetchNews( mapOf("page" to "1", "count" to "4") ).enqueue(object : Callback<NewsBean> { override fun onFailure(call: Call<NewsBean>, t: Throwable) { newsView.showNews(null) } override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) { response.body()?.result?.let { // 將網絡新聞經過 view 層接口通知 Activity newsView.showNews(it) dbExecutor.submit { insertNews(it) } } } }) } // 從數據庫讀老新聞(僞代碼) private fun queryNews() : List<News> { // 經過 view 層接口獲取 context 構造 dbHelper val dbHelper = NewsDbHelper(newsView.newsContext, ...) val db = dbHelper.getReadableDatabase() val cursor = db.query(...) var newsList = mutableListOf<News>() while(cursor.moveToNext()) { ... newsList.add(news) } db.close() return newsList } // 將新聞寫入數據庫(僞代碼) private fun insertNews(news : List<News>) { val dbHelper = NewsDbHelper(newsView.newsContext, ...) val db = dbHelper.getWriteableDatabase() news.foreach { val cv = ContentValues().apply { ... } db.insert(cv) } db.close() } } 複製代碼
無非就是複製 + 粘貼,把 GodActivity 中的「異步」、「訪問數據庫」、「訪問網絡」、放到了一個新的Presenter
類中。這樣 Activity 就變簡單了:
class RetrofitActivity : AppCompatActivity(), NewsView {
// 在界面中直接構造業務接口實例 private val newsBusiness = NewsPresenter(this) private var rvNews: RecyclerView? = null private var newsAdapter = NewsAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.news_activity) initView() // 觸發業務邏輯 newsBusiness.fetchNews() } private fun initView() { rvNews = findViewById(R.id.rvNews) rvNews?.layoutManager = LinearLayoutManager(this) } // 實現 View 層接口以更新界面 override fun showNews(news: List<News>?) { newsAdapter.news = news rvNews?.adapter = newsAdapter } override val newsContext: Context get() = this } 複製代碼
Presenter
的引入還增長了通訊成本:
interface NewsBusiness {
fun fetchNews() } 複製代碼
這是MVP
模型中的業務接口
,描述的是業務動做。它由Presenter
實現,而界面類持有它以觸發業務邏輯。
interface NewsView {
// 將新聞傳遞給界面 fun showNews(news:List<News>?) // 獲取界面上下文 abstract val newsContext:Context } 複製代碼
在MVP
模型中,這稱爲View 層接口
。Presenter
持有它以觸發界面更新,而界面類實現它以繪製界面。
這兩個接口的引入,意義非凡:
接口把 作什麼(抽象) 和 怎麼作(細節) 分離。這個特性使得 關注點分離 成爲可能:接口持有者只關心 作什麼,而 怎麼作 留給接口實現者關心。
Activity 持有業務接口
,這使得它不須要關心業務邏輯的實現細節。Activity 實現View 層接口
,界面展現細節都內聚在 Activity 類中,使其成爲MVP
中的V
。
Presenter 持有View 層接口
,這使得它不須要關心界面展現細節。Presenter 實現業務接口
,業務邏輯的實現細節都內聚在 Presenter 類中,使其成爲MVP
中的P
。
這樣作最大的好處是下降代碼理解成本,由於不一樣細節再也不是在同一層次被鋪開,而是被分層了。閱讀代碼時,「淺嘗輒止」或「不求甚解」的閱讀方式極大的提升了效率。
這樣作還能縮小變動成本,業務需求發生變動時,只有Presenter
類須要改動。界面調整時,只有V
層須要改動。同理,排查問題的範圍也被縮小。
這樣還方便了自測,若是想測試各類臨界數據產生時界面的表現,則能夠實現一個PresenterForTest
。若是想覆蓋業務邏輯的各類條件分支,則能夠方便地給Presenter
寫單元測試(和界面隔離後,Presenter 是純 Kotlin 的,不含有任何 Android 代碼)。
但NewsPresenter
也不單純!它除了包含業務邏輯,還包含了訪問數據的細節,應該用一樣的思路,抽象出一個訪問數據的接口,讓Presenter
持有,這就是MVP
中的M
。它的實現方式能夠參考下一節的Repository
。
即便將訪問數據的細節剝離出Presenter
,它依然不單純。由於它持有View 層接口
,這就要求Presenter
需瞭解 該把哪一個數據傳遞給哪一個接口方法,這就是 數據綁定,它在構建視圖時就已經肯定(無需等到數據返回),因此這個細節能夠從業務層剝離,歸併到視圖層。
Presenter
的實例被 Activity 持有,因此它的生命週期和 Activiy 同步,即業務數據和界面同生命週期。在某些場景下,這是一個缺點,好比橫豎屏切換。此時,若是數據的生命週期不依賴界面,就能夠免去從新獲取數據的成本。這勢必 須要一個生命週期更長的對象(ViewModel)持有數據。
上一節的例子中,構建Presenter
是直接在Activity
中new
,而構建ViewModel
是經過ViewModelProvider.get()
:
public class ViewModelProvider {
// ViewModel 實例商店 private final ViewModelStore mViewModelStore; public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) { // 從商店獲取 ViewModel實例 ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { return (T) viewModel; } else { ... } // 若商店無 ViewModel 實例 則經過 Factory 構建 if (mFactory instanceof KeyedFactory) { viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass); } else { viewModel = (mFactory).create(modelClass); } // 將 ViewModel 實例存入商店 mViewModelStore.put(key, viewModel); return (T) viewModel; } } 複製代碼
ViewModel
實例經過ViewModelStore
獲取:
// ViewModel 實例商店
public class ViewModelStore { // 存儲 ViewModel 實例的 Map private final HashMap<String, ViewModel> mMap = new HashMap<>(); // 存 final void put(String key, ViewModel viewModel) { ViewModel oldViewModel = mMap.put(key, viewModel); if (oldViewModel != null) { oldViewModel.onCleared(); } } // 取 final ViewModel get(String key) { return mMap.get(key); } ... } 複製代碼
ViewModelStore
將ViewModel
實例存儲在HashMap
中。
而ViewModelStore
經過ViewModelStoreOwner
獲取:
public class ViewModelProvider {
// ViewModel 實例商店 private final ViewModelStore mViewModelStore; // 構造 ViewModelProvider 時需傳入 ViewModelStoreOwner 實例 public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) { // 經過 ViewModelStoreOwner 獲取 ViewModelStore this(owner.getViewModelStore(), factory); } public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) { mFactory = factory; mViewModelStore = store; } } 複製代碼
那ViewModelStoreOwner
實例又存儲在哪?
// Activity 基類實現了 ViewModelStoreOwner 接口
public class ComponentActivity extends androidx.core.app.ComponentActivity implements LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, OnBackPressedDispatcherOwner { // Activity 持有 ViewModelStore 實例 private ViewModelStore mViewModelStore; public ViewModelStore getViewModelStore() { if (mViewModelStore == null) { // 獲取配置無關實例 NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // 從配置無關實例中恢復 ViewModel商店 mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } return mViewModelStore; } // 靜態的配置無關實例 static final class NonConfigurationInstances { // 持有 ViewModel商店實例 ViewModelStore viewModelStore; ... } } 複製代碼
Activity
就是ViewModelStoreOwner
實例,且持有ViewModelStore
實例,該實例還會被保存在一個靜態類中,因此ViewModel
生命週期比Activity
更長。這樣 ViewModel 中存放的業務數據就能夠在Activity
銷燬重建時被複用。
MVVM
中Activity 屬於V
層,佈局構建以及數據綁定都在這層完成:
class MvvmActivity : AppCompatActivity() {
private var rvNews: RecyclerView? = null private var newsAdapter = NewsAdapter() // 構建佈局 private val rootView by lazy { ConstraintLayout { TextView { layout_id = "tvTitle" layout_width = wrap_content layout_height = wrap_content textSize = 25f padding_start = 20 padding_end = 20 center_horizontal = true text = "News" top_toTopOf = parent_id } rvNews = RecyclerView { layout_id = "rvNews" layout_width = match_parent layout_height = wrap_content top_toBottomOf = "tvTitle" margin_top = 10 center_horizontal = true } } } // 構建 ViewModel 實例 private val newsViewModel by lazy { // 構造 ViewModelProvider 實例, 經過其 get() 得到 ViewModel 實例 ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(rootView) initView() bindData() } // 將數據綁定到視圖 private fun bindData() { newsViewModel.newsLiveData.observe(this, Observer { newsAdapter.news = it rvNews?.adapter = newsAdapter }) } private fun initView() { rvNews?.layoutManager = LinearLayoutManager(this) } } 複製代碼
其中構建佈局 DSL 的詳細介紹能夠點擊這裏。它省去了原先V
層( Activity + xml )中的xml
。
代碼中的數據綁定是經過觀察ViewModel
中的LiveData
實現的。這不是數據綁定的徹底體,因此還需手動地觀察observe
數據變化(只有當引入data-binding
包後,才能把視圖和控件的綁定都靜態化到 xml 中)。但至少它讓ViewModel
無需主動推數據了:
在 MVP 模式中,
Presenter
持有View 層接口
並主動向界面推數據。
MVVM模式中,
ViewModel
再也不持有View 層接口
,也不主動給界面推數據,而是界面被動地觀察數據變化。
這使得ViewModel
只需持有數據並根據業務邏輯更新之便可:
// 數據訪問接口在構造函數中注入
class NewsViewModel(var newsRepository: NewsRepository) : ViewModel() { // 持有業務數據 val newsLiveData by lazy { newsRepository.fetchNewsLiveData() } } // 定義構造 ViewModel 方法 class NewsFactory(context: Context) : ViewModelProvider.Factory { // 構造 數據訪問接口實例 private val newsRepository = NewsRepositoryImpl(context) override fun <T : ViewModel?> create(modelClass: Class<T>): T { // 將數據接口訪問實例注入 ViewModel return NewsViewModel(newsRepository) as T } } // 而後就能夠在 Activity 中這樣構造 ViewModel 了 class MvvmActivity : AppCompatActivity() { // 構建 ViewModel 實例 private val newsViewModel by lazy { ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) } } 複製代碼
ViewModel
只關心業務邏輯和數據,不關心獲取數據的細節,因此它們都被數據訪問接口
隱藏了。
Demo 業務場景中,ViewModel
只有一行代碼,那它還有存在的價值嗎?
有!即便在業務邏輯如此簡單的場景下仍是有!由於ViewModel
生命週期比 Activity 長,其持有的數據能夠在 Activity 銷燬重建時複用。
真實項目中的業務邏輯複雜度遠高於 Demo,應該將業務邏輯的細節隱藏在ViewModel
中,讓界面類無感知。好比 「將服務器返回的時間戳轉化成年月日」 就應該寫在ViewModel
中。
// 業務數據訪問接口
interface NewsRepository { // 拉取新聞並以 LiveData 方式返回 fun fetchNewsLiveData():LiveData<List<News>?> } // 實現訪問網絡和數據庫的細節 class NewsRepositoryImpl(context: Context) : NewsRepository { // 使用 Retrofit 構建請求訪問網絡 private val retrofit = Retrofit.Builder() .baseUrl("https://api.apiopen.top") .addConverterFactory(MoshiConverterFactory.create()) // 將返回數據組織成 LiveData .addCallAdapterFactory(LiveDataCallAdapterFactory()) .client(OkHttpClient.Builder().build()) .build() private val newsApi = retrofit.create(NewsApi::class.java) private var executor = Executors.newSingleThreadExecutor() // 使用 room 訪問數據庫 private var newsDatabase = NewsDatabase.getInstance(context) private var newsDao = newsDatabase.newsDao() private var newsLiveData = MediatorLiveData<List<News>>() override fun fetchNewsLiveData(): LiveData<List<News>?> { // 從數據庫獲取新聞 val localNews = newsDao.queryNews() // 從網絡獲取新聞 val remoteNews = newsApi.fetchNewsLiveData( mapOf("page" to "1", "count" to "4") ).let { Transformations.map(it) { response: ApiResponse<NewsBean>? -> when (response) { is ApiSuccessResponse -> { val news = response.body.result news?.let { // 將網絡新聞入庫 executor.submit { newsDao.insertAll(it) } } news } else -> null } } } // 將數據庫和網絡響應的 LiveData 合併 newsLiveData.addSource(localNews) { newsLiveData.value = it } newsLiveData.addSource(remoteNews) { newsLiveData.value = it } return newsLiveData } } 複製代碼
這就是MVVM
中的M
,它定義了如何獲取數據的細節。
Demo 中 數據庫和網絡都返回 LiveData 形式的數據,這樣合併兩個數據源只須要一個MediatorLiveData
。因此使用了 Room 來訪問數據庫。而且定義了LiveDataCallAdapterFactory
用於將 Retrofit 返回結果也轉化成 LiveData。(其源碼能夠在這裏找到)
這裏也存在耦合:Repository
須要瞭解 Retrofit 和 Room 的使用細節。
當訪問數據庫和網絡的細節愈來愈複雜,甚至又加入內存緩存時,再增長一層抽象,分別把訪問內存、數據庫、和網絡的細節都隱藏起來,也是常見的作法。這樣Repository
中的邏輯就變成: 「運用什麼策略將內存、數據庫和網絡的數據進行組合並返回給業務層」。
經屢次重構,代碼結構不斷衍化,最終引入了ViewModel
和Repository
。層次變多了,表面上看是愈來愈複雜了,但其實理解成本愈來愈低。由於 全部複雜的細節並非在同一層次被展開。
最後用 Clean architecture 再審視一下這套架構:
它是業務實體對象,對於 Demo 來講 Entities 就是新聞實體類News
。
它是業務邏輯,Entities 是名詞,Use Cases 就是用它造句。對於 Demo 來講 Use Cases 就是 「展現新聞列表」 在 Clean Architecture 中每個業務邏輯都會被抽象成一個 UseCase 類,它被Presenters
持有,詳情能夠去這裏瞭解
它是業務數據訪問接口,抽象地描述獲取和存儲 Entities。和 Demo 中的 Repository 如出一轍,但在 Clean Architecture 中,它由 UseCase 持有。
它和MVP
模型中 Presenter 幾乎同樣,由它觸發業務邏輯,並把數據傳遞給界面。惟一的不一樣是,它持有 UseCase。
它是抽象業務數據訪問接口的實現,和 Demo 中的NewsRepositoryImpl
如出一轍。
它是構建佈局的細節,就像 Demo 中的 Activity。
它是和設備相關的細節,DB 和 UI 的實現細節也和設備有關,這裏的 Device是指除了數據和界面以外的和設備相關的細節,好比如何在通知欄展現通知。
洋蔥圈的內三層都是抽象,而只有最外層才包含實現細節(和 Android 平臺相關的實現細節。好比訪問數據庫的細節、繪製界面的細節、通知欄提醒消息的細節、播放音頻的細節)
洋蔥圈向內的箭頭意思是:外層知道相鄰內層的存在,而內層不知道外層的存在。即外層依賴內層,內層不依賴外層。也就說應該儘量把業務邏輯抽象地實現,業務邏輯只須要關心作什麼,而不應關心怎麼作。這樣的代碼對擴展友好,當實現細節變化時,業務邏輯不須要變。
本文使用 mdnice 排版