如何以正确的方式初始化 Jetpack Compose 视图模型中的多个参数?

问题描述 投票:0回答:1

如果我有多个从存储库获取的参数,这些参数是从数据存储中获取的。我应该如何正确初始化它们?

这是我目前的做法:

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

初始化从

init
块开始。

我希望仅在块完成并加载所有数据后才设置 isDataLoaded 。

android kotlin android-jetpack-compose android-viewmodel
1个回答
0
投票

您不应该在视图模型中收集流。这个想法是只是转换来自较低层的流以将它们暴露给UI。

如果您不首先将数据存储值拆分为多个流,则可以极大地简化视图模型。只需将值捆绑到专用数据类中,如下所示:

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

假设您的数据存储有这些密钥:

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

然后您只需将数据存储流转换为

Flow<User>
,如下所示:

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

当您的存储库将此流程传递到视图模型时,视图模型可以将其转换为 UI 状态:

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

就是这样,您不再需要

init
loadAllData
,也不需要以前单独处理每个用户属性的任何函数。

您只需要一个新的

UserUiState
类型:

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

这就是视图模型现在向 UI 公开的内容。在您的可组合项中,您可以像这样使用 ui 状态:

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

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

这样您就不再需要主动收集流,只需将存储库流转换为

StateFlow<UserUiState>
,最终收集到您的可组合项中。


您还可以考虑将单独的更新函数合并为一个

fun setUser(user: User)
。然后数据存储可以相应地设置首选项。通过这种方式,您可以删除视图模型、存储库和数据存储中的大量样板代码。如果您想更新单个属性,您现在需要传递整个用户对象。这是否适合您可能取决于您的具体用例。

© www.soinside.com 2019 - 2024. All rights reserved.