You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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) }
        }
    }
}

问题原因及修复方案

问题核心原因

  1. 调用顺序错误:在Composable顶层直接调用checkUserLogin()时,LaunchedEffect还未启动流收集,MutableSharedFlow默认是冷流且不会重播历史事件,导致初始发送的Loading等事件直接丢失。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 02:45:02