ViewModel中MutableSharedFlow事件无法在Jetpack Compose组件中收集
问题描述
我在Composable函数第一行调用ViewModel的checkUserLogin()方法,尝试在LaunchedEffect代码块中收集ViewModel的MutableSharedFlow事件,但事件无法正常被收集。不过当我在Composable中添加Button组件,点击按钮调用该方法时,所有事件都能正常生成并被收集。但我的需求不需要UI元素,相关代码如下:
MyComposable 函数
@Composable fun LaunchApp( navController: NavHostController, appLauncherViewModel: AppLauncherViewModel = get() ) { var showProgress by rememberSaveable { mutableStateOf(false) } val context = LocalContext.current appLauncherViewModel.checkUserLogin() LaunchedEffect(key1 = context) { appLauncherViewModel.responseEvent.collect { event -> println("Event is $event") when (event) { is Response.Login -> { navigateToLogin(navController) showProgress = false } is Response.Success -> { val user = event.data as? User println("Login User is $user") user?.userEmail?.let { email -> navigateToStore(navController, email) } showProgress = false } is Response.Error -> { val errorMessage = event.errorMessage showProgress = false Toast.makeText(context, "Something went wrong Re-Launch app", Toast.LENGTH_LONG) .show() } is Response.Loading -> { showProgress = true } else -> { println("Else") } } } } if (showProgress) { ProgressBar() } }
ViewModel 代码
class AppLauncherViewModel(private val myCartAuthenticationRepository: MyCartAuthenticationRepository) : ViewModel(), LifecycleObserver { var responseEvent = MutableSharedFlow<Response<Any>>() fun checkUserLogin() { viewModelScope.launch { try { responseEvent.emit(Response.Loading) val result = myCartAuthenticationRepository.isUserLoggedIn() result?.let { val receivedUser = myCartAuthenticationRepository.getCurrentUser() receivedUser.collect { user -> user?.let { responseEvent.emit(Response.Success(it)) } ?: run { responseEvent.emit(Response.Login) } } } ?: run { responseEvent.emit(Response.Login) } } catch (e: Exception) { responseEvent.emit(Response.Error(e.message.toString())) } } } }
Response 密封类
sealed class Response<out T> { object Loading:Response<Nothing>() data class Success<out T>(val data: T) : Response<T>() data class SuccessList<out T>(val dataList: List<T>,val dataType: DataType) : Response<T>() data class Error(val errorMessage:String):Response<Nothing>() data class SuccessConfirmation(val successMessage:String):Response<Nothing>() object SignOut : Response<Nothing>() object Login: Response<Nothing>() } enum class DataType { A, B, C, D }
Repository 实现
class MyCartAuthenticationRepositoryImpl() : MyCartAuthenticationRepository{ private val auth = FirebaseAuth.getInstance() override fun getCurrentUser(): Flow<User?> = flow { val currentUser = auth.currentUser emit(currentUser?.toUser()) } override suspend fun signIn(email: String, password: String): FirebaseUser? = try { val signInResult = auth.signInWithEmailAndPassword(email, password).await() signInResult.user } catch (e: Exception) { println("${e.printStackTrace()}") null } override suspend fun signUp(email: String, password: String): FirebaseUser? = try { val signUpResult = auth.createUserWithEmailAndPassword(email, password).await() signUpResult.user }catch (e: Exception) { null } override suspend fun isUserLoggedIn(): FirebaseUser? = auth.currentUser override fun signOut() : Boolean { auth.signOut() auth.currentUser?.let { return false }?:run{ return true } } private fun FirebaseUser?.toUser(): User? { return this?.let { User(userEmail = it.email ?: "") } } }
导航代码
@Composable fun Navigator(navHostController: NavHostController) { NavHost(navController = navHostController, startDestination = "appLauncherScreen") { composable("appLauncherScreen") { LaunchApp(navController = navHostController) } composable("loginScreen") { LoginScreen(navController = navHostController) } composable("registrationScreen") { Register(navController = navHostController) } composable("forgotPasswordScreen") { ForgotPassword(navHostController) } composable( "store/{emailId}", arguments = listOf(navArgument("emailId") { type = NavType.StringType }) ) { backStackEntry -> backStackEntry.arguments?.getString("emailId") ?.let { email -> StoreList(email, navController = navHostController) } } } }
问题原因及修复方案
问题核心原因
- 调用顺序错误:在Composable顶层直接调用
checkUserLogin()时,LaunchedEffect还未启动流收集,MutableSharedFlow默认是冷流且不会重播历史事件,导致初始发送的Loading等事件直接丢失。 LaunchedEffect的Key不合适:使用context作为Key,不会触发重复执行,但无法保证收集逻辑先于事件发送启动。
修复方案
方案1:将登录检查逻辑移到LaunchedEffect内部(推荐)
确保流收集启动后再触发事件发送,避免事件丢失:
@Composable fun LaunchApp( navController: NavHostController, appLauncherViewModel: AppLauncherViewModel = get() ) { var showProgress by rememberSaveable { mutableStateOf(false) } val context = LocalContext.current LaunchedEffect(Unit) { // 使用Unit作为Key,确保Composable首次启动时执行一次 appLauncherViewModel.checkUserLogin() // 先启动收集,再触发事件 appLauncherViewModel.responseEvent.collect { event -> println("Event is $event") when (event) { is Response.Login -> { navigateToLogin(navController) showProgress = false } is Response.Success -> { val user = event.data as? User println("Login User is $user") user?.userEmail?.let { email -> navigateToStore(navController, email) } showProgress = false } is Response.Error -> { val errorMessage = event.errorMessage showProgress = false Toast.makeText(context, "Something went wrong Re-Launch app", Toast.LENGTH_LONG) .show() } is Response.Loading -> { showProgress = true } else -> { println("Else") } } } } if (showProgress) { ProgressBar() } }
方案2:修改MutableSharedFlow配置
如果需要保留原有调用时机,可以给MutableSharedFlow设置重播历史事件的数量,让新收集者能收到最近一次事件:
// ViewModel中修改 var responseEvent = MutableSharedFlow<Response<Any>>(replay = 1)
额外优化:监听实时用户状态
当前getCurrentUser()只获取一次用户状态,若需要实时监听登录状态变化,可改为使用callbackFlow封装Firebase的AuthStateListener:
override fun getCurrentUser(): Flow<User?> = callbackFlow { val listener = FirebaseAuth.AuthStateListener { auth -> trySend(auth.currentUser?.toUser()) } auth.addAuthStateListener(listener) awaitClose { auth.removeAuthStateListener(listener) } }
内容的提问来源于stack exchange,提问作者Android_programmer_office
相关产品推荐
相关产品推荐

