移动应用架构设计:MVVM实战,一个海归码农的血泪踩坑日记

杨刚_程序员
2025-12-15 10:29
阅读 530

上周五晚上九点半,我瘫在工位上盯着屏幕上那串红得发紫的 Crashlytics 报错日志,心里一万只羊驼奔腾而过。
“又是因为 ViewModel 生命周期没对齐 Activity 重建……” 我叹了口气,顺手抓起桌上已经凉透的瑞幸——这已经是今天第三杯了。

大家好,我是 Alex,刚从英国读完分布式系统硕士回来半年,目前在北京一家做数字身份认证的创业公司当 Android 开发(没错,就是那个天天喊着要“赋能 Web3”的团队)。通勤一小时,每天在国贸和回龙观之间穿梭,地铁上刷 LeetCode 的时间比吃饭还长。

回国前我以为自己会去搞高并发、搞共识算法,结果入职第一天就被 PM 拉进会议室:“Alex 啊,我们这个新 App 要支持链上身份核验,但 UI 得像微信一样丝滑,下周 demo 给投资人看,你来搭架构吧。”

我?一个连 Jetpack 都还没摸熟的“海归理论派”?当时真想掏出护照原地买张机票回伦敦。


为什么非得用 MVVM?

其实一开始我偷偷用了 MVP —— 老实说,在学校做课程项目时 MVP 写起来简单粗暴,逻辑清晰。但上线第一周就翻车了。

那天是去年双11前夕,产品突然要求加一个“区块链交易状态实时同步”功能。用户发起链上操作后,App 要轮询节点,一旦交易上链成功,立即更新 UI 并推送通知。
我吭哧吭哧在 Presenter 里塞了一堆协程 + Retrofit + WebSocket 逻辑,结果一测试:旋转屏幕一次,回调监听多注册一次;退出再进,内存泄漏直接 OOM。

Crashlytics 后台警报响得跟火警似的。运维小哥半夜打电话过来:“兄弟,你们 Android 端是不是在挖矿?CPU 占用率 98%!”

那一刻我悟了:在现代移动开发里,光有“能跑”远远不够,还得“稳如老狗”。

而 MVVM(Model-View-ViewModel)正好能解决这些问题:

  • 生命周期感知:ViewModel 自动绑定 Activity/Fragment 生命周期,避免内存泄漏
  • 数据驱动 UI:通过 LiveData 或 StateFlow,UI 自动响应数据变化,告别手动 setText() 堆砌
  • 可测试性强:业务逻辑集中在 ViewModel,单元测试覆盖率蹭蹭涨(虽然我们组测试覆盖率还是只有 30%,别问,问就是 deadline 逼的)

更重要的是,我们产品要做的不是普通 App,而是要和区块链交互的“可信入口”。用户每点一次“授权”,背后都是一笔真实的 Gas 费。如果因为架构混乱导致重复提交、状态不一致,那可真是“代码一运行,ETH 就不见”。


实战:从零搭建一个链上身份验证模块

我们的场景很简单:用户点击“连接钱包” → 弹出二维码 → 手机扫码确认 → 监听链上交易 → 成功后跳转主页。

第一步:定义数据模型(Model)

首先抽象出核心数据结构。注意,这里不要直接把 API 返回的 JSON 对象当 Model!我吃过这亏——后端某天改个字段名,整个 App 崩溃。

// domain/model/IdentityVerification.kt
data class IdentityVerification(
    val requestId: String,
    val status: VerificationStatus, // PENDING, CONFIRMED, FAILED
    val blockchainTxHash: String? = null,
    val timestamp: Long
)

enum class VerificationStatus {
    PENDING, CONFIRMED, FAILED
}

接着写 Repository 层,负责和链上节点 & 后端 API 交互:

// data/repository/VerificationRepository.kt
class VerificationRepository(
    private val apiService: ApiService,
    private val blockchainClient: BlockchainClient // 这是我们封装的 Web3j 客户端
) {
    suspend fun requestVerification(): String {
        return apiService.createVerificationRequest().requestId
    }

    fun observeTransactionStatus(requestId: String): Flow<IdentityVerification> {
        return blockchainClient.observeTransactionByRequestId(requestId)
    }
}

这里有个坑:区块链交易确认可能需要几十秒甚至几分钟。如果用 Callback,很容易因为页面重建丢失监听。所以必须用 Kotlin Flow + SharedFlow / StateFlow 来保持状态。


第二步:ViewModel 是灵魂

ViewModel 负责协调 Model 和 View,绝不持有任何 View 引用(这是 MVP 的典型反模式)。

// presentation/viewmodel/VerificationViewModel.kt
@HiltViewModel
class VerificationViewModel @Inject constructor(
    private val repository: VerificationRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<VerificationUiState>(VerificationUiState.Loading)
    val uiState: StateFlow<VerificationUiState> = _uiState.asStateFlow()

    fun startVerification() {
        viewModelScope.launch {
            try {
                val requestId = repository.requestVerification()
                // 切换到监听状态
                _uiState.value = VerificationUiState.WaitingForScan(requestId)
                
                // 监听链上状态变更
                repository.observeTransactionStatus(requestId)
                    .onEach { verification ->
                        when (verification.status) {
                            VerificationStatus.CONFIRMED -> 
                                _uiState.value = VerificationUiState.Success(verification)
                            VerificationStatus.FAILED -> 
                                _uiState.value = VerificationUiState.Error("Transaction failed")
                            else -> Unit
                        }
                    }
                    .launchIn(viewModelScope) // 注意:launchIn 绑定 viewModelScope
            } catch (e: Exception) {
                _uiState.value = VerificationUiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

sealed class VerificationUiState {
    object Loading : VerificationUiState()
    data class WaitingForScan(val requestId: String) : VerificationUiState()
    data class Success(val verification: IdentityVerification) : VerificationUiState()
    data class Error(val message: String) : VerificationUiState()
}

关键点来了:
✅ 用 StateFlow 替代 LiveData(Jetpack Compose 更推荐 Flow)
✅ 所有异步操作都在 viewModelScope 中启动,自动随 ViewModel 销毁而取消
✅ UI 状态用 Sealed Class 表达,避免 if-else 地狱


第三步:View 层只负责“展示”和“事件转发”

在 Activity/Fragment 中,我们只做两件事:观察状态 + 发送事件

// ui/VerificationActivity.kt
class VerificationActivity : AppCompatActivity() {
    private lateinit var binding: ActivityVerificationBinding
    private val viewModel by viewModels<VerificationViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVerificationBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 观察 UI 状态
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    render(state)
                }
            }
        }

        // 用户点击“开始验证”
        binding.btnStart.setOnClickListener {
            viewModel.startVerification()
        }
    }

    private fun render(state: VerificationUiState) {
        when (state) {
            is VerificationUiState.Loading -> showLoading()
            is VerificationUiState.WaitingForScan -> showQrCode(state.requestId)
            is VerificationUiState.Success -> navigateToHome()
            is VerificationUiState.Error -> showError(state.message)
        }
    }
}

这里特别注意 repeatOnLifecycle(Lifecycle.State.STARTED) —— 这是 Google 官方推荐的 Flow 收集方式,避免在后台收集数据浪费资源


踩过的坑 & 性能优化

坑 1:旋转屏幕导致重复请求

一开始我没用 viewModelScope,而是直接在 Activity 里 launch 协程。结果一旋转屏幕,ViewModel 被重建,又发起一次 requestVerification(),用户钱包里莫名其妙多了两笔待确认交易。

解决方案:所有副作用(side effect)放在 ViewModel,且用 viewModelScope 管理生命周期。

坑 2:链上监听没取消,内存泄漏

早期用 Callback 监听交易,退出页面后回调还在执行,导致 ViewModel 无法释放。

解决方案:改用 Flow,并通过 launchIn(viewModelScope) 绑定生命周期。

性能优化:减少不必要的重组(Compose 用户注意)

如果你用 Jetpack Compose,记得把状态拆细:

// ❌ 不要这样
val uiState by viewModel.uiState.collectAsState()

// ✅ 应该这样
val isLoading by viewModel.isLoading.collectAsState()
val qrCode by viewModel.qrCode.collectAsState()

或者用 derivedStateOf 避免频繁 recomposition。


MVVM vs 其他架构:一张表说清楚

架构 适合场景 生命周期管理 测试难度 与 Compose 兼容性
MVC 超小型 Demo 差(容易内存泄漏)
MVP 中小型项目 一般(需手动解绑) 一般
MVVM 中大型、需长期维护 优秀(自动感知) 极佳
MVI 状态极其复杂(如金融交易) 优秀 优秀

我们团队现在新项目一律 MVVM + Compose,老项目也在逐步迁移。虽然初期学习成本高点,但省下的 debug 时间足够你多喝十杯瑞幸


最后:关于“产品”、“区块链”和“综合能力”

回国这半年,我最大的感受是:纯技术思维行不通了

产品经理昨天又来找我:“Alex,能不能让用户扫完码后,加个‘正在上链’的动画?显得更可信。”
我说:“可以,但得等交易真正广播出去再显示,不然就是欺骗用户。”
他说:“那不行,投资人要看‘流畅体验’。”

你看,技术决策背后全是产品逻辑。而我们做的又是区块链相关产品——每一行代码都关联着真实资产。这时候,一个健壮、可追溯、状态清晰的架构,就不再是“炫技”,而是“责任”。

MVVM 让我能清晰回答这些问题:

  • 当前用户到底处于哪个验证阶段?
  • 如果 App 被杀后台,重新进入能否恢复状态?
  • 交易失败是因为网络问题,还是 Gas 不足?

这些,在传统 MVC 里可能要翻半天日志才能定位。


写在最后

现在我的 App 已经稳定运行三个月,线上 Crash 率从 5% 降到 0.2%,连测试妹子都说“你们 Android 组最近 bug 少多了”。

虽然每天还是被 PM 追着改需求,被运维吐槽包体积太大,但至少——我不用再担心旋转屏幕炸掉用户的钱包了

如果你也在做涉及区块链、金融、或任何“不能出错”的移动产品,真心建议你试试 MVVM。它可能不会让你一夜暴富,但至少能让你在周五晚上准点下班(梦想还是要有的)。

对了,下周我要去参加 GDG Beijing 的架构分享会,主题就是《MVVM 在 Web3 移动端的实践》,欢迎来现场一起吐槽 PM!


P.S. 回国找工作真的卷,但只要你能把“分布式系统理论”和“移动端实战”结合起来,offer 还是有的。共勉!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝