項(xiàng)目開(kāi)始時(shí)壓縮工期堆砌代碼,三個(gè)月后每一次需求變更都像在拆雷——這不是你一個(gè)人的困局。Android 開(kāi)發(fā)缺乏強(qiáng)制架構(gòu)約束,Activity 和 Fragment 極易膨脹為承載網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)查詢(xún)、UI 邏輯的“上帝對(duì)象”,導(dǎo)致單元測(cè)試無(wú)法編寫(xiě)、代碼行數(shù)動(dòng)輒過(guò)千、Bug 定位全靠運(yùn)氣。
問(wèn)題的根源不是開(kāi)發(fā)速度,而是沒(méi)有在 UI 組件(View)與業(yè)務(wù)數(shù)據(jù)(Model)之間建立清晰的職責(zé)邊界。View 既負(fù)責(zé)展示又負(fù)責(zé)拉取數(shù)據(jù),任何需求變化都可能引發(fā)連串修改,最終形成“無(wú)頭蠅式”的代碼跳轉(zhuǎn)。
解決這個(gè)問(wèn)題的核心思路是 將“界面如何展示”與“數(shù)據(jù)從哪里來(lái)、怎么處理”拆分成獨(dú)立層次。Android 官方 Jetpack 體系給出了明確的實(shí)現(xiàn)路徑:ViewModel 持有 UI 所需數(shù)據(jù)并對(duì)外暴露狀態(tài),View 只負(fù)責(zé)觀察狀態(tài)并渲染;Repository 作為唯一數(shù)據(jù)源,負(fù)責(zé)協(xié)調(diào)本地?cái)?shù)據(jù)庫(kù)與遠(yuǎn)程網(wǎng)絡(luò);LiveData 或 StateFlow 充當(dāng)可觀察的數(shù)據(jù)容器,在生命周期內(nèi)自動(dòng)管理訂閱關(guān)系。
搭建可落地的 MVVM 分層結(jié)構(gòu)
你應(yīng)該將項(xiàng)目拆分出至少四個(gè)包層級(jí),而非將所有類(lèi)塞進(jìn)一個(gè) package。以一款“任務(wù)清單”應(yīng)用為例,核心路徑如下:
1. 數(shù)據(jù)層(data)
TaskEntity:使用 Room 定義的實(shí)體,映射到 tasks 表。TaskDao:定義@Query、@Insert等數(shù)據(jù)庫(kù)操作。TaskApi:Retrofit 接口,包含fun getTasks(): List<TaskDto>。TaskRepository:實(shí)現(xiàn)interface TaskRepository { fun getTasks(): Flow<List<Task>> },內(nèi)部決定從網(wǎng)絡(luò)獲取還是從本地?cái)?shù)據(jù)庫(kù)讀取,并對(duì)調(diào)用方完全隱藏細(xì)節(jié)。
Repository 返回 Flow 而非 LiveData 的理由在于:數(shù)據(jù)層不應(yīng)關(guān)心生命周期的存在。ViewModel 中通過(guò) .asLiveData() 將 Flow 轉(zhuǎn)變?yōu)樯芷诟兄臄?shù)據(jù)類(lèi)型。
2. 業(yè)務(wù)狀態(tài)層(ViewModel)
MainViewModel不持有 Context、不引用 View。它通過(guò)構(gòu)造參數(shù)接收TaskRepository實(shí)例。- 使用 Kotlin StateFlow 或 LiveData 暴露 UI 狀態(tài):
class MainViewModel(private val repository: TaskRepository) : ViewModel() {
private val _tasks = MutableLiveData<List<Task>>()
val tasks: LiveData<List<Task>> = _tasks
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun loadTasks() {
viewModelScope.launch {
repository.getTasks()
.catch { e -> _error.value = e.message ?: "未知錯(cuò)誤" }
.collect { _tasks.value = it }
}
}
}
關(guān)鍵細(xì)節(jié):不要將 LiveData 直接暴露成可變類(lèi)型。通過(guò) private val 持有 MutableLiveData,對(duì)外暴露只讀的 LiveData,防止 View 層意外篡改數(shù)據(jù)。
3. View 層(UI)
MainFragment只做三件事:聲明布局、觀察 ViewModel 的狀態(tài)、將用戶(hù)事件轉(zhuǎn)發(fā)給 ViewModel。
class MainFragment : Fragment() {
private val viewModel: MainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.tasks.observe(viewLifecycleOwner) { tasks ->
adapter.submitList(tasks)
}
viewModel.error.observe(viewLifecycleOwner) { errorMsg ->
showToast(errorMsg)
}
binding.swipeRefresh.setOnRefreshListener {
viewModel.loadTasks()
}
}
}
使用 viewLifecycleOwner 而非 this,確保 LiveData 觀察在 Fragment 視圖銷(xiāo)毀時(shí)被移除,避免內(nèi)存泄漏或重復(fù)監(jiān)聽(tīng)。
三個(gè)最易踩坑的邊界條件
1. Repository 不是“萬(wàn)能中轉(zhuǎn)站”
不要將 Repository 設(shè)計(jì)成對(duì)所有 API 調(diào)用都“先進(jìn)緩存再發(fā)網(wǎng)絡(luò)”的同質(zhì)化邏輯。某些場(chǎng)景(如支付確認(rèn))必須穿透網(wǎng)絡(luò),某些場(chǎng)景(如用戶(hù)偏好)只應(yīng)該操作本地?cái)?shù)據(jù)。解決方法是為不同用例定義不同的策略方法,例如 loadFromNetworkIfNeeded() 和 getLocalOnly(),并在接口中明確標(biāo)注行為語(yǔ)義,而不是把所有邏輯塞進(jìn)一個(gè) getData() 里靠布爾值判斷。
2. ViewModel 持有 View 或 Context 是架構(gòu)污染
一旦 ViewModel 內(nèi)部出現(xiàn) Context、View 的引用,旋轉(zhuǎn)屏幕導(dǎo)致的重新創(chuàng)建就會(huì)失控——ViewModel 存活在更長(zhǎng)生命周期中,卻被舊有的已銷(xiāo)毀 View 引用牽連。任何需要 Context 的操作(獲取字符串、使用資源)都應(yīng)該在 View 層完成并傳入整理后的數(shù)據(jù),或者使用 Application 級(jí)別的 Context(通過(guò) AndroidViewModel 謹(jǐn)慎獲取)。避免在 ViewModel 內(nèi)直接使用 Toast 或 startActivity,狀態(tài)反饋通過(guò) LiveData 下發(fā)。
3. 過(guò)度使用 LiveData 合并復(fù)雜狀態(tài)
單一 LiveData 承載所有 UI 狀態(tài)字段會(huì)導(dǎo)致 UI 難以區(qū)分“加載中”“空數(shù)據(jù)”“內(nèi)容更新”等情形。更好的做法是定義一個(gè)密封類(lèi)表示狀態(tài):
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
ViewModel 提供單一 LiveData<UiState<List<Task>>>,View 層根據(jù)狀態(tài)分支渲染對(duì)應(yīng)界面元素。這比維護(hù)多個(gè)獨(dú)立 LiveData 變量(isLoading、tasks、errorMessage)更能保證狀態(tài)一致性,避免某次賦值遺漏導(dǎo)致界面停留在中間態(tài)。
遷移現(xiàn)有項(xiàng)目的行動(dòng)建議
你不需要一次性重寫(xiě)整個(gè)應(yīng)用。將改造目標(biāo)鎖定在 “修改頻率最高、Bug 復(fù)現(xiàn)最密的”一個(gè)模塊,按以下步驟執(zhí)行:
- 抽取該模塊的數(shù)據(jù)操作代碼到獨(dú)立的 Repository 類(lèi),保留原有調(diào)用接口不變,先用單元測(cè)試驗(yàn)證數(shù)據(jù)邏輯。
- 為該模塊創(chuàng)建 ViewModel,將 Activity/Fragment 中的網(wǎng)絡(luò)調(diào)用和線程切換代碼逐步移入 ViewModel,View 層僅保留
observe語(yǔ)句。 - 引入 Hilt 或者 Koin 實(shí)現(xiàn)依賴(lài)注入,讓 ViewModel 通過(guò)構(gòu)造參數(shù)拿到 Repository,而不是手動(dòng) new 或使用靜態(tài)單例。這直接關(guān)系到可測(cè)試性——你可以輕松替換一個(gè)假的 Repository 來(lái)驗(yàn)證 ViewModel 的狀態(tài)流轉(zhuǎn)。
- 移除模塊內(nèi)所有與 View 線程切換相關(guān)的
AsyncTask或裸Thread調(diào)用,全部使用viewModelScope.launch(內(nèi)置協(xié)程支持)統(tǒng)一管理。
每完成一個(gè)模塊的遷移,立即補(bǔ)充一次針對(duì) ViewModel 狀態(tài)的單元測(cè)試(使用 InstantTaskExecutorRule 讓 LiveData 同步執(zhí)行)。如果測(cè)試寫(xiě)起來(lái)很痛苦,說(shuō)明 ViewModel 依賴(lài)了不該依賴(lài)的外部對(duì)象,這時(shí)你應(yīng)該檢查構(gòu)造參數(shù)中是否有 Context 或 SystemService。
架構(gòu)投資不會(huì)立即增加功能點(diǎn),但它能讓你在后續(xù)十二個(gè)月中不再因?yàn)椤案?A 處導(dǎo)致 B 處崩潰”而加班排錯(cuò)。Android APP 開(kāi)發(fā)的長(zhǎng)期生產(chǎn)力,不取決于你多快寫(xiě)出第一版,而取決于第二版、第十版需求到來(lái)時(shí),修改路徑是清晰可控的,還是需要再次“全局搜索 + 手動(dòng)替換”。