項(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)直接使用 ToaststartActivity,狀態(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í)行:

  1. 抽取該模塊的數(shù)據(jù)操作代碼到獨(dú)立的 Repository 類(lèi),保留原有調(diào)用接口不變,先用單元測(cè)試驗(yàn)證數(shù)據(jù)邏輯。
  2. 為該模塊創(chuàng)建 ViewModel,將 Activity/Fragment 中的網(wǎng)絡(luò)調(diào)用和線程切換代碼逐步移入 ViewModel,View 層僅保留 observe 語(yǔ)句。
  3. 引入 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)。
  4. 移除模塊內(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)替換”。

← 上一篇 網(wǎng)站前端重構(gòu)需要注意什么:一份從危機(jī)到落地的執(zhí)行手冊(cè) 下一篇 → 如何檢查企業(yè)網(wǎng)站的移動(dòng)端體驗(yàn):一份可落地的審核框架