一名優秀的Android開發,需要一份完備的 知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。
本文以一個真實項目的業務場景爲載體,描述了經歷一次次重構後,代碼變得越來越複雜(you ya)的過程。
本篇 Demo 的業務場景是:從服務器拉取新聞並在列表展示。
GodActivity
剛接觸 Android 時,我是這樣寫業務代碼的(省略了和主題無關的 Adapter 和 Api 細節):
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 知道太多細節:
- 異步細節
- 訪問數據庫細節
- 訪問網絡細節
- 如果大量 “細節” 在同一個層次被鋪開,就顯得囉嗦,增加理解成本。
拿說話打個比方:
你問 “晚飯吃了啥?”
“我用勺子一口一口地吃了雞生下的蛋和番茄再加上油一起炒的菜。”
聽了這樣地回答,你還會和他做朋友嗎?其實你並不關心他喫的工具、喫的速度、食材的來源,以及烹飪方式。
- 與 “細節” 相對的是 “抽象”,在編程中 “細節” 易變,而 “抽象” 相對穩定。
比如 “異步” 在 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)持有數據。
生命週期更長的 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;
}
}
ViewModelStore
將ViewModel
實例存儲在HashMap
中。
而ViewModelStore
通過ViewModelStoreOwner
獲取:
// 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);
}
...
}
那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;
}
}
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
中的邏輯就變成: “運用什麼策略將內存、數據庫和網絡的數據進行組合並返回給業務層”。
Clean Architecture
經多次重構,代碼結構不斷衍化,最終引入了ViewModel
和Repository
。層次變多了,表面上看是越來越複雜了,但其實理解成本越來越低。因爲 所有複雜的細節並不是在同一層次被展開。
最後用 Clean architecture 再審視一下這套架構:
Entities
它是業務實體對象,對於 Demo 來說 Entities 就是新聞實體類News
。
Use Cases
它是業務邏輯,Entities 是名詞,Use Cases 就是用它造句。對於 Demo 來說 Use Cases 就是 “展示新聞列表” 在 Clean Architecture 中每一個業務邏輯都會被抽象成一個 UseCase 類,它被Presenters
持有,詳情可以去這裏瞭解
Repository
它是業務數據訪問接口,抽象地描述獲取和存儲 Entities。和 Demo 中的 Repository 一模一樣,但在 Clean Architecture 中,它由 UseCase 持有。
Presenters
它和MVP
模型中 Presenter 幾乎一樣,由它觸發業務邏輯,並把數據傳遞給界面。唯一的不同是,它持有 UseCase。
DB & API
它是抽象業務數據訪問接口的實現,和 Demo 中的NewsRepositoryImpl
一模一樣。
UI
它是構建佈局的細節,就像 Demo 中的 Activity。
Device
它是和設備相關的細節,DB 和 UI 的實現細節也和設備有關,這裏的 Device是指除了數據和界面之外的和設備相關的細節,比如如何在通知欄展示通知。
依賴方向
洋蔥圈的內三層都是抽象,而只有最外層才包含實現細節(和 Android 平臺相關的實現細節。比如訪問數據庫的細節、繪製界面的細節、通知欄提醒消息的細節、播放音頻的細節)
洋蔥圈向內的箭頭意思是:外層知道相鄰內層的存在,而內層不知道外層的存在。即外層依賴內層,內層不依賴外層。也就說應該儘可能把業務邏輯抽象地實現,業務邏輯只需要關心做什麼,而不該關心怎麼做。這樣的代碼對擴展友好,當實現細節變化時,業務邏輯不需要變。