androidkotlinandroid-jetpack-composeandroid-roomkotlin-flow

LaunchedEffect not triggering when adding a new profile in Jetpack Compose


I'm working on a Jetpack Compose app where users can manage login profiles. The profiles are stored in a Room database, and the UI shows the profiles in a dropdown menu. The LaunchedEffect block in my LoginScreen is supposed to react to changes in the profile list (profiles), which is exposed as a StateFlow from the LoginViewModel.

When I edit or delete a profile, the LaunchedEffect is triggered, and the logs confirm that the updated profile list is fetched. However, when I add a new profile, the LaunchedEffect does not trigger.

view model code:

private val _profiles = MutableStateFlow<List<LoginProfile>>(emptyList())
val profiles: StateFlow<List<LoginProfile>> = _profiles

init {
    fetchProfiles()
}

fun fetchProfiles() {
    viewModelScope.launch {
        val currentProfiles = database.loginProfileDao().getAllProfiles()
        _profiles.value = currentProfiles
        Log.d("Profile_Test", "Profiles fetched from DB: $_profiles")
    }
}

fun saveProfileToDatabase(profile: LoginProfile) {
    viewModelScope.launch {
        database.loginProfileDao().insertProfile(profile)
        fetchProfiles()
    }
}

UI Code:

@Composable
fun ProfileDropdown(loginViewModel: LoginViewModel) {
    var expanded by remember { mutableStateOf(false) }
    var selectedItem by remember { mutableStateOf<LoginProfile?>(null) }

    val profiles = loginViewModel.profiles.collectAsState().value

    LaunchedEffect(profiles) {
        Log.d("Profile_Test", "Launched Effect: $profiles")
        if (profiles.isNotEmpty() && selectedItem != profiles.last()) {
            selectedItem = profiles.last()
            loginViewModel.selectedProfile.value = profiles.last()
        } else if (profiles.isEmpty()) {
            selectedItem = null
            loginViewModel.selectedProfile.value = null
        }
    }

    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = { expanded = !expanded }
    ) {
        OutlinedTextField(
            value = selectedItem?.name ?: "Select Profile",
            onValueChange = {},
            readOnly = true,
            trailingIcon = {
                ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
            }
        )
        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            profiles.forEach { profile ->
                DropdownMenuItem(
                    text = { Text(profile.name) },
                    onClick = {
                        selectedItem = profile
                        loginViewModel.selectedProfile.value = profile
                        expanded = false
                    }
                )
            }
        }
    }
}

Why is LaunchedEffect not triggering when a new profile is added, even though _profiles is updated? Is there something I'm missing in how StateFlow or LaunchedEffect works in Compose?

Any help or guidance would be greatly appreciated! Thanks!


Solution

  • You should let the Room database return the profiles in a Flow:

    @Dao
    interface LoginProfileDao {
        @Query(...)
        fun getAllProfiles(): Flow<List<LoginProfile>>
        
        // ...
    }
    

    Then you can simplify the view model accordingly: You do not need _profiles, fetchProfiles and the init block anymore. Just replace profiles with this:

    val profiles: StateFlow<List<LoginProfile>> = database.loginProfileDao().getAllProfiles()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )
    

    It takes the Room flow and converts it to a StateFlow. You don't need to call fetchProfiles() anymore because the flow is automatically updated when anything changes in the database.

    Your composable can stay the same, although it looks suspicious why you use a LaunchedEffect there, that doesn't seem to be necessary and should be removed.