22、MVVM架构实践:Repository模式、ViewModel管理认证状态、LiveData/Flow数据流
好,我们直接进入正题。虹膜认证这个功能,如果只是写个Demo,那怎么搞都行。但一旦要放到正式产品里,代码结构就得讲究了。我个人习惯用MVVM来组织这类生物识别逻辑,原因很简单——好维护、好测试、不容易写出“屎山”。
今天我们就来拆解一下,在虹膜认证场景下,Repository模式怎么用、ViewModel怎么管状态、LiveData和Flow怎么选。
一、为什么非得用Repository?
你想想看,虹膜认证的调用方是谁?可能是系统服务、可能是厂商SDK、甚至可能是你封装的一个底层库。如果Activity直接去调这些底层API,那画面太美我不敢看——耦合严重、没法单元测试、换一个厂商就得改一堆代码。
Repository的作用,说白了就是“中间人”。它把数据来源(比如系统BiometricManager、本地缓存、甚至远程配置)统一封装起来,上层ViewModel只管调用repository的方法,不用关心数据是从哪来的。
二、Repository层的设计
我在项目中遇到过一种情况:同一个App需要兼容不同厂商的虹膜SDK。有的厂商提供的是回调式API,有的是协程式。这时候Repository就派上大用场了——我在Repository内部做适配,对外暴露统一的接口。
来看一个简化版的结构:
// 定义接口,不依赖具体实现
interface IrisAuthRepository {
fun startAuth(activity: FragmentActivity): Flow<IrisAuthResult>
}
// 具体实现,封装系统BiometricManager
class SystemIrisAuthRepository @Inject constructor(
private val biometricManager: BiometricManager
) : IrisAuthRepository {
override fun startAuth(activity: FragmentActivity): Flow<IrisAuthResult> {
return callbackFlow {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("虹膜验证")
.setSubtitle("请正视屏幕")
.setAllowedAuthenticators(BIOMETRIC_IRIS)
.build()
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
trySend(IrisAuthResult.Success(result))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
trySend(IrisAuthResult.Error(errorCode, errString.toString()))
}
override fun onAuthenticationFailed() {
trySend(IrisAuthResult.Failed)
}
}
BiometricPrompt(activity, callback).authenticate(promptInfo)
awaitClose { /* 清理资源 */ }
}
}
}
三、ViewModel管理认证状态
ViewModel的核心职责就两个:持有UI状态、协调业务逻辑。对于虹膜认证来说,状态无非就是:空闲中、认证中、成功、失败、错误。
我建议用一个密封类来定义这些状态:
sealed class IrisAuthUiState {
object Idle : IrisAuthUiState()
object Authenticating : IrisAuthUiState()
data class Success(val biometricResult: BiometricPrompt.AuthenticationResult) : IrisAuthUiState()
data class Error(val code: Int, val message: String) : IrisAuthUiState()
object Failed : IrisAuthUiState()
}
然后在ViewModel里,我们这样管理:
@HiltViewModel
class IrisAuthViewModel @Inject constructor(
private val irisAuthRepository: IrisAuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<IrisAuthUiState>(IrisAuthUiState.Idle)
val uiState: StateFlow<IrisAuthUiState> = _uiState.asStateFlow()
private var authJob: Job? = null
fun startAuth(activity: FragmentActivity) {
// 防止重复点击
if (_uiState.value is IrisAuthUiState.Authenticating) return
authJob?.cancel()
authJob = viewModelScope.launch {
_uiState.value = IrisAuthUiState.Authenticating
irisAuthRepository.startAuth(activity)
.catch { e ->
_uiState.value = IrisAuthUiState.Error(-1, e.message ?: "未知错误")
}
.collect { result ->
_uiState.value = when (result) {
is IrisAuthResult.Success -> IrisAuthUiState.Success(result.data)
is IrisAuthResult.Error -> IrisAuthUiState.Error(result.code, result.message)
is IrisAuthResult.Failed -> IrisAuthUiState.Failed
}
}
}
}
fun resetState() {
authJob?.cancel()
_uiState.value = IrisAuthUiState.Idle
}
override fun onCleared() {
super.onCleared()
authJob?.cancel()
}
}
四、LiveData vs Flow,到底选哪个?
嗯,这个问题其实挺多人在问。我直接给结论:新项目无脑用Flow,老项目如果已经全面LiveData了,也别急着重构。
为什么我推荐Flow?三个理由:
- 协程原生支持: Flow天然和协程配合,viewModelScope.launch里直接collect,不用额外转换。
- 操作符丰富: map、filter、catch、combine……这些LiveData做起来很别扭,Flow信手拈来。
- 冷流特性: 每次有新的collector,都会重新执行上游代码。对于虹膜认证这种“每次认证都是新的一次”的场景,非常合适。
但LiveData也有它的优势:
- 自动感知生命周期,不用手动取消。
- 在XML绑定中直接使用,非常方便。
我个人做法是:Repository层统一返回Flow,ViewModel层对外暴露StateFlow,UI层用repeatOnLifecycle来收集。这样既享受了Flow的灵活性,又不会出现生命周期问题。
五、整体架构图
下面这张图展示了我们刚才讲的所有组件之间的关系。我特意把数据流向画清楚了,你一看就明白。
六、避坑指南
最后分享几个我实际踩过的坑:
- 不要用LiveData做一次性事件: 比如“认证成功跳转页面”,如果用LiveData,旋转屏幕后会再次触发。我改用Channel+SharedFlow来处理这类一次性事件。
- ViewModel里别做耗时操作: 虹膜认证本身是系统异步回调的,但如果你在ViewModel里做数据库写入或网络请求,记得切到Dispatchers.IO。我见过有人直接在main线程做加密运算,结果ANR了。
- 测试Repository时记得Mock系统服务: BiometricPrompt没法在单元测试里跑。我的做法是把Repository接口化,测试时注入一个假的实现,返回预设的Flow数据。
公众号:蓝海资料掘金营,微信deep3321