-2

I have a scenario where I make a call to a method in ViewModel as a first statement in my Composable Function. I try to collect events from ViewModel's MutableStateFlow in LaunchedEffect()block of my Composable Function. The problem is the events emitted by ViewModel are not collected in my Composable Function. But when I add an UI element (Button)in Composable function and call ViewModel's method on button click all events are generated without any issue. But in my requirement, there is no need for me to have UI element. I will share my code related to Composable function ,ViewModel and other required info below, plz help in resolving my issue:
MyComposable Function :

 @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()
        }
        /* AppScaffold(
        title = "Home",
        onLogoutClick = {

        }
    ) {
        if (showProgress) {
            ProgressBar()
        }

        Column() {
            Button(onClick = { appLauncherViewModel.checkUserLogin() }
            ) {
                Text(text = "Click me...")
            }
        }
    }*/


    }

MyViewModel:

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


    /* suspend fun override fun isUserLoggedIn(): FirebaseUser?  = try{
          auth.currentUser
      }catch (e: Exception) {
          null
      }
  */

    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 ?: "")
        }
    }
}

MainActivity:

@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) }
        }
  • Use `replay = 1` when you call `MutableStateFlow()` – Tenfour04 Aug 16 '23 at 20:08
  • It triggers continuously. I need only once – Android_programmer_office Aug 16 '23 at 20:17
  • If you use a Flow for emitting events, the flow type needs to be a mutable class where you can mark the items as "consumed" when the UI reacts to the event. Then the collector can ignore events that have already been acted upon. By the way, your `getCurrentUser()` function looks broken. It only emits the current user once, immediately when collected. Not much reason for it to be a flow, which would be expected to keep emitting when the login state changes. – Tenfour04 Aug 16 '23 at 20:55
  • Side note, that `?.let { } ?: run { } ` pattern is, aside from being hard to read, very error prone. Both scope functions can run if `let`'s lambda evaluates to null. `if (foo != null)`/`else` is much easier to read and safer. – Tenfour04 Aug 16 '23 at 20:56
  • I changed to MutableStateFlow instead of MutableSharedFlow, its working as expected. Also Thanks for your response – Android_programmer_office Aug 16 '23 at 21:27

2 Answers2

0

I think you have to use extraBufferCapacity = 10 or something different than 0, for the SharedMutableState to work. See it's API design: https://github.com/Kotlin/kotlinx.coroutines/issues/2034

Pablo Valdes
  • 734
  • 8
  • 19
-1

Plz find my answer as below. I used MutableStateFlow instead of MutableSharedFlow and I am able to see expected result. Plz find below code for Future reference.

ViewModel:

 private val _state = MutableStateFlow<Response<Any>>(Response.Loading)

    val state = _state.asStateFlow()

    fun checkUser(){
        viewModelScope.launch {
            try{
                _state.value = Response.Loading
                val result = myCartAuthenticationRepository.isUserLoggedIn()
                result?.let {
                    val receivedUser = myCartAuthenticationRepository.getCurrentUser()
                    receivedUser.collect{ user ->
                        user?.let {
                            _state.value =      Response.Success(it)
                        }?: run {
                            _state.value =     Response.Login
                        }
                    }
                }?:run{
                    _state.value = Response.Login
                }

            }catch (e:Exception){
                e.printStackTrace()
                _state.value = Response.Error(e.message.toString())
            }
        }
    }

My Composable Function:

fun LaunchApp(
    navController: NavHostController,
    appLauncherViewModel: AppLauncherViewModel = get()
) {
    var showProgress by rememberSaveable { mutableStateOf(false) }


    val context = LocalContext.current
    appLauncherViewModel.checkUser()
    val currentState by appLauncherViewModel.state.collectAsState()
    LaunchedEffect(key1 = context) {
        when (currentState) {
            is Response.Loading -> {
                showProgress = true
            }
            is Response.Error -> {
                val errorMessage = (currentState as Response.Error).errorMessage
                Toast.makeText(context, "Error is $errorMessage", Toast.LENGTH_LONG).show()
                showProgress = true
            }
            is Response.Success -> {
                val user = (currentState as Response.Success).data as User
                navigateToStore(navController, user.userEmail)
                showProgress = false
            }

            is Response.Login -> {
                navigateToLogin(navController)
                showProgress = false
            }
            else -> {

            }
        }
    }



    if (showProgress) {
        ProgressBar()
    }


}

Thanks for responses