整洁架构下如何优雅实现应用启动阶段的串行请求链?
安卓启动串行请求链最佳实践
方案1:一次性启动任务(推荐大多数启动场景)
你第一种UseCase方案的嵌套问题本质是没有利用Result的链式转换能力,同时可以把整条启动链路封装成独立的组合UseCase,避免逻辑散在ViewModel中:
- 首先定义启动状态密封类,方便UI感知全链路进度和错误:
sealed interface AppStartupState { object Idle : AppStartupState object Locating : AppStartupState object FetchingServers : AppStartupState object FetchingConfig : AppStartupState object Success : AppStartupState data class Failed(val step: String, val throwable: Throwable) : AppStartupState }
- 封装组合启动UseCase,用flatMap消除嵌套回调:
如果Kotlin版本标准库Result没有内置flatMap,可以先加个扩展:
inline fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> { return if (isSuccess) transform(getOrThrow()) else Result.failure(exceptionOrNull()!!) }
组合UseCase实现:
class AppStartupUseCase( private val locateUseCase: LocateUseCase, private val retrieveServersUseCase: RetrieveServersUseCase, private val retrieveRemoteConfigUseCase: RetrieveRemoteConfigUseCase ) { suspend operator fun invoke(): Result<Unit> { return locateUseCase() .flatMap { location -> retrieveServersUseCase(location) } .flatMap { servers -> retrieveRemoteConfigUseCase(servers) } } }
- ViewModel中调用并下发状态:
class MainViewModel( private val startupUseCase: AppStartupUseCase ) : ViewModel() { private val _startupState = MutableStateFlow<AppStartupState>(AppStartupState.Idle) val startupState: StateFlow<AppStartupState> = _startupState.asStateFlow() fun startInit() = viewModelScope.launch { // 细粒度状态上报可拆分每步执行逻辑,执行完一步发一次状态 _startupState.value = AppStartupState.Locating val result = startupUseCase() result.onSuccess { _startupState.value = AppStartupState.Success }.onFailure { // 可给每个步骤的异常包自定义类型,这里判断异常返回对应错误步骤 _startupState.value = AppStartupState.Failed("初始化失败", it) } } }
该方案逻辑简单直观,取消只需终止viewModelScope即可,重试直接重新调用startInit()即可,适合绝大多数一次性启动初始化场景。
方案2:响应式流式链路(适合后续需要自动更新的场景)
如果后续定位、服务器列表变化时需要自动更新配置,可以用Flow构建链式链路,不建议你通过Hilt注入Flow的实现,会导致依赖耦合度过高,正确实现如下:
class AppStartupFlowUseCase( private val locationRepository: LocationRepository, private val serverRepository: ServerRepository, private val remoteConfigRepository: RemoteConfigRepository ) { fun execute(): Flow<AppStartupState> = flow { emit(AppStartupState.Locating) val location = locationRepository.locate().getOrThrow() emit(AppStartupState.FetchingServers) val servers = serverRepository.retrieve(location) emit(AppStartupState.FetchingConfig) remoteConfigRepository.retrieve(servers) emit(AppStartupState.Success) }.catch { throwable -> // 捕获全链路异常,映射对应错误步骤 val step = when(throwable) { is LocationException -> "定位失败" is ServerFetchException -> "服务器列表获取失败" is ConfigFetchException -> "配置获取失败" else -> "初始化失败" } emit(AppStartupState.Failed(step, throwable)) }.retryWhen { cause, attempt -> // 自定义重试规则,例如定位失败重试2次,其他错误重试1次,间隔1秒 val maxRetry = if (cause is LocationException) 2 else 1 if (attempt < maxRetry) { delay(1000) true } else false } }
ViewModel中直接收集Flow即可:
fun startInit() = viewModelScope.launch { appStartupFlowUseCase.execute().collect { state -> _startupState.value = state } }
该方案原生支持取消、重试逻辑,状态自动流式下发,后续如果把定位改成热流,还能实现定位更新后自动刷新服务器列表和配置的能力。
原有方案问题说明
你第二种通过Hilt注入Flow的实现会把依赖关系隐式化,后续排查问题、调整链路的成本会非常高,不推荐使用。
内容的提问来源于stack exchange,提问作者Kennir
相关产品推荐
相关产品推荐

