假设我们有一个 Android-Kotlin 单活动应用程序,其中每个屏幕都是使用 MVVM 模式实现的片段。该应用程序具有以下层:
在Repository层,我有一个像这样的apiCall函数:
inline fun <T> apiCall(crossinline apiCall: suspend () -> Response<T>) = flow {
emit(State.Progress(0))
val response = apiCall()
val responseBody = response.body()
if (response.isSuccessful && responseBody != null)
emit(State.Success(responseBody))
else
emit(State.Error(HttpException(response)))
}.catch {
emit(State.Error(it))
}.flowOn(Dispatchers.IO)
此函数在存储库中以这种方式使用,例如:
fun checkOtpCode(phoneNumber: String, code: String) =
apiCall { remoteDataSource.checkOtp(phoneNumber,code) }
在 apiCall 函数中,我需要通知视图层,如果响应代码在 5xx 范围内,则应用程序应导航到服务器错误片段,或者如果是 401 或 403 状态代码,则应导航到登录片段—无需在所有片段中重复导航和状态代码检查逻辑,同时仍然遵循 MVVM 架构模式并且不会使代码变得如此难看。
有人建议有一个全局的 SharedFlow 并将其收集在 MainActivity 中。例如,如果状态代码在 5xx 范围内,则将调用 globalFlow.emit(NavDest.ServerError),并在 MainActivity 中处理全局导航。
但我对这个解决方案不太满意:
object SharedEventBus {
private val _globalEvent = MutableSharedFlow<Event>()
val globalEvent: SharedFlow<Event> = _globalEvent
suspend fun sendEvent(event: Event) {
_globalEvent.emit(event)
}
sealed class Event {
object NavigateToLogin : Event()
object NavigateToServerError : Event()
}
}
在具有单活动、多片段 Android 应用程序的 MVVM 架构中,可以通过利用 ViewModel 和导航事件的密封类来实现集中 HTTP 响应处理和导航,同时避免跨片段的冗余代码。下面是一种遵循 MVVM 原则的干净且可扩展的方法。
创建一个密封类来表示导航事件,允许 ViewModel 将它们传达给 View 层
sealed class NavigationEvent {
object NavigateToLogin : NavigationEvent()
object NavigateToServerError : NavigationEvent()
}
ApiCall函数
内联有趣的 apiCall( crossinline apiCall:挂起()->响应, crossinline onNavigationEvent: (NavigationEvent) -> 单位 ) = 流量 { 发出(状态.进度(0)) val 响应 = apiCall() val responseBody = response.body()
when {
response.isSuccessful && responseBody != null -> emit(State.Success(responseBody))
response.code() in 500..599 -> {
onNavigationEvent(NavigationEvent.NavigateToServerError)
emit(State.Error(HttpException(response)))
}
response.code() == 401 || response.code() == 403 -> {
onNavigationEvent(NavigationEvent.NavigateToLogin)
emit(State.Error(HttpException(response)))
}
else -> emit(State.Error(HttpException(response)))
}
}.catch { 异常 -> 发出(状态。错误(异常)) }.flowOn(调度程序.IO)
从存储库发出导航事件
在 API 调用期间将导航事件传递到 ViewModel:
class ExampleRepository {
fun checkOtpCode(
phoneNumber: String,
code: String,
onNavigationEvent: (NavigationEvent) -> Unit
) = apiCall(
{ remoteDataSource.checkOtp(phoneNumber, code) },
onNavigationEvent
)
}
将导航事件的 SharedFlow 或 LiveData 暴露给 Fragment:
class ExampleViewModel(private val repository: ExampleRepository) : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent
fun checkOtpCode(phoneNumber: String, code: String) = repository.checkOtpCode(
phoneNumber,
code
) { event ->
viewModelScope.launch {
_navigationEvent.emit(event)
}
}
}
收集 Fragment 中的导航事件并执行导航:
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchWhenStarted {
viewModel.navigationEvent.collect { event ->
when (event) {
is NavigationEvent.NavigateToLogin -> findNavController().navigate(R.id.loginFragment)
is NavigationEvent.NavigateToServerError -> findNavController().navigate(R.id.serverErrorFragment)
}
}
}
// Api call here
viewModel.checkOtpCode("1234567890", "1234")
}
}
该解决方案遵循 MVVM,将导航逻辑保留在 ViewModel 中,使其集中、可重用且可测试。它通过将导航事件范围限定到特定的 ViewModel 实例来避免全局状态,从而确保干净的架构并减少紧密耦合。为了进一步改进,可以将共享导航逻辑移至 BaseViewModel,并且可以使用 SingleLiveEvent 与 LiveData 进行一次性导航。这种方法确保了代码的可维护性、模块化和可扩展性。
我希望这对您有帮助。谢谢你:)