Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to initialize multiple parameters in a view model for Jetpack Compose the right way?

If I have multiple parameters that I am getting from the repository which is getting it from a datastore. How should I properly initialize them?

This is my current approach:

@HiltViewModel
class UserDataViewModel @Inject constructor(private val userDataRepository: UserDataRepository) : ViewModel() {
    private val _userIdFlow = MutableStateFlow<String>(AppDefaults.DEFAULT_USER_ID)
    val userIdFlow: StateFlow<String> = _userIdFlow.asStateFlow()

    private val _userNameFlow = MutableStateFlow<String>(AppDefaults.DEFAULT_USER_NAME)
    val userNameFlow: StateFlow<String> = _userNameFlow.asStateFlow()

    private val _userEmailFlow = MutableStateFlow<String>(AppDefaults.DEFAULT_USER_EMAIL)
    val userEmailFlow: StateFlow<String> = _userEmailFlow.asStateFlow()

    private val _isPremiumUserFlow = MutableStateFlow<Boolean>(AppDefaults.DEFAULT_IS_PREMIUM_USER)
    val isPremiumUserFlow: StateFlow<Boolean> = _isPremiumUserFlow.asStateFlow()

    private val _isFirstTimeUserFlow = MutableStateFlow<Boolean>(AppDefaults.DEFAULT_IS_FIRST_TIME_USER)
    val isFirstTimeUserFlow: StateFlow<Boolean> = _isFirstTimeUserFlow.asStateFlow()

    private val _isLoggedInFlow = MutableStateFlow<Boolean>(AppDefaults.DEFAULT_IS_LOGGED_IN)
    val isLoggedInFlow: StateFlow<Boolean> = _isLoggedInFlow.asStateFlow()

    val isDataLoaded = MutableStateFlow(false)

    init {
        loadAllData()
    }

    private fun loadAllData() {
        viewModelScope.launch {
            // Launch all load operations in parallel using async
            val userIdJob = async { loadUserId() }
            val userNameJob = async { loadUserName() }
            val userEmailJob = async { loadUserEmail() }
            val isPremiumUserJob = async { loadIsPremiumUser() }
            val isFirstTimeUserJob = async { loadIsFirstTimeUser() }
            val isLoggedInJob = async { loadIsLoggedIn() }

            try {
                Log.d("Init", "Start loading data")
                // Await all jobs to complete
                awaitAll(
                    userIdJob,
                    userNameJob,
                    userEmailJob,
                    isPremiumUserJob,
                    isFirstTimeUserJob,
                    isLoggedInJob,
                )
                Log.d("Init", "Data Loaded")
            } catch (e: Exception) {
                // Handle any errors
                Log.e("UserDataViewModel", "Error loading data", e)
            } finally {
                withContext(Dispatchers.Main) {
                    isDataLoaded.value = true
                }
            }
        }
    }

    private suspend fun loadUserId() {
        userDataRepository.userIdFlow
            .catch { Log.e("UserDataViewModel", "Error loading userId", it) }
            .collect { _userIdFlow.value = it ?: AppDefaults.DEFAULT_USER_ID }
    }

    private suspend fun loadUserName() {
        userDataRepository.userNameFlow
            .catch { Log.e("UserDataViewModel", "Error loading userName", it) }
            .collect { _userNameFlow.value = it ?: AppDefaults.DEFAULT_USER_NAME }
    }

    private suspend fun loadUserEmail() {
        userDataRepository.userEmailFlow
            .catch { Log.e("UserDataViewModel", "Error loading userEmail", it) }
            .collect { _userEmailFlow.value = it ?: AppDefaults.DEFAULT_USER_EMAIL }
    }

    private suspend fun loadIsPremiumUser() {
        userDataRepository.isPremiumUserFlow
            .catch { Log.e("UserDataViewModel", "Error loading isPremiumUser", it) }
            .collect { _isPremiumUserFlow.value = it ?: AppDefaults.DEFAULT_IS_PREMIUM_USER }
    }

    private suspend fun loadIsFirstTimeUser() {
        userDataRepository.isFirstTimeUserFlow
            .catch { Log.e("UserDataViewModel", "Error loading isFirstTimeUser", it) }
            .collect {
                _isFirstTimeUserFlow.value = it ?: AppDefaults.DEFAULT_IS_FIRST_TIME_USER
            }
    }

    private suspend fun loadIsLoggedIn() {
        userDataRepository.isLoggedInFlow
            .catch { Log.e("UserDataViewModel", "Error loading isLoggedIn", it) }
            .collect {
                _isLoggedInFlow.value = it ?: AppDefaults.DEFAULT_IS_LOGGED_IN
            }
    }

    fun setUserId(userId: String) {
        viewModelScope.launch {
            userDataRepository.setUserId(userId)
        }
    }

    fun setUserName(userName: String) {
        viewModelScope.launch {
            userDataRepository.setUserName(userName)
        }
    }

    fun setUserEmail(userEmail: String) {
        viewModelScope.launch {
            userDataRepository.setUserEmail(userEmail)
        }
    }

    fun setIsPremiumUser(isPremiumUser: Boolean) {
        viewModelScope.launch {
            userDataRepository.setIsPremiumUser(isPremiumUser)
        }
    }

    fun setIsFirstTimeUser(isFirstTimeUser: Boolean) {
        viewModelScope.launch {
            userDataRepository.setIsFirstTimeUser(isFirstTimeUser)
            Log.d("ViewModel", "setIsFirstTimeUser: $isFirstTimeUser")
        }
    }

    fun setIsLoggedIn(isLoggedIn: Boolean) {
        viewModelScope.launch {
            userDataRepository.setIsLoggedIn(isLoggedIn)
        }
    }
}

The initialization starts in the init block.

I am expecting to set isDataLoaded only after the block is completed and all data is loaded.

like image 719
Aditya Anand Avatar asked Sep 21 '25 04:09

Aditya Anand


1 Answers

You shouldn't collect flows in your view model. The idea is to just transform the flows from the lower layers to expose them to the UI.

You can simplify your view model drastically if you do not split the data store values into multiple flows in the first place. Just bundle the values into a dedicated data class, like this:

data class User(
    val id: String,
    val name: String,
    val email: String,
    val isPremium: Boolean,
    val isFirstTime: Boolean,
    val isLoggedIn: Boolean,
)

Assuming you have these keys for your data store:

private val Keys = object {
    val USER_ID = stringPreferencesKey("user_id")
    val USER_NAME = stringPreferencesKey("user_name")
    val USER_EMAIL = stringPreferencesKey("user_email")
    val IS_PREMIUM_USER = booleanPreferencesKey("is_premium_user")
    val IS_FIRST_TIME_USER = booleanPreferencesKey("is_first_time_user")
    val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
}

Then you just need to transform the datastore flow to a Flow<User> like this:

val user: Flow<User> = dataStore.data
    .map { prefs ->
        User(
            id = prefs[Keys.USER_ID] ?: AppDefaults.DEFAULT_USER_ID,
            name = prefs[Keys.USER_NAME] ?: AppDefaults.DEFAULT_USER_NAME,
            email = prefs[Keys.USER_EMAIL] ?: AppDefaults.DEFAULT_USER_EMAIL,
            isPremium = prefs[Keys.IS_PREMIUM_USER] ?: AppDefaults.DEFAULT_IS_PREMIUM_USER,
            isFirstTime = prefs[Keys.IS_FIRST_TIME_USER] ?: AppDefaults.DEFAULT_IS_FIRST_TIME_USER,
            isLoggedIn = prefs[Keys.IS_LOGGED_IN] ?: AppDefaults.DEFAULT_IS_LOGGED_IN,
        )
    }

When your repository passes this flow through to your view model, the view model can then transform that to a UI state:

val userUiState: StateFlow<UserUiState> = userDataRepository.userFlow
    .mapLatest { UserUiState.Success(it) as UserUiState }
    .catch { emit(UserUiState.Error(it.message)) }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = UserUiState.Loading,
    )

That's it, you won't need init or loadAllData anymore, and you don't need any of the functions that previously handled each single user property seperately.

You just need a new UserUiState type:

sealed interface UserUiState {
    data object Loading : UserUiState
    data class Success(val user: User) : UserUiState
    data class Error(val message: String?) : UserUiState
}

This is what the view model now exposes to the UI. In your composable you can consume the ui state like this:

val uiState = viewModel.userUiState.collectAsStateWithLifecycle().value

when (uiState) {
    is UserUiState.Loading -> CircularProgressIndicator()
    is UserUiState.Error -> ErrorDialog(uiState.message)
    is UserUiState.Success -> UserDetails(uiState.user)
}

This way you do not need to actively collect the flows in the view model anymore, you just transform the repository flow into a StateFlow<UserUiState> which is eventually collected in your composable.


You can also consider merging the separate update functions into a single fun setUser(user: User). The data store can then set the preferences accordingly. This way you could remove a lot of boilerplate code in the view model, repository and data store. If you want to update a single property you need to pass the entire user object now. If that is appropriate for you depends on your specific use case.

like image 99
Leviathan Avatar answered Sep 22 '25 18:09

Leviathan