Jetpack compose 实现单向流 Datastore -> ViewModel -> UI

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

我正在开发我的第一个应用程序。我使用 android jetpack compose 和 Kotlin。 我正在尝试使用数据存储来保留一些用户首选项。 我找到的有关数据存储使用的所有资源都以某种方式使用可观察数据、实时数据等...... 我正在尝试遵循 jetpack compose 教程并使用单向流:数据层 -> viewmodel -> UI。我无法弄清楚如何使用此方案使数据存储区工作。

我的应用程序的灵感来自撰写教程中的 inventory 应用程序。就像那个应用程序一样,我有名为 AppViewModelProvider、AppContainer 等的文件......其代码可以在下面找到。现在,由于这是我的第一个应用程序,我不了解代码的每个细节,我像黑匣子一样使用它们,并且主要通过模仿来工作。

我想要持久化的数据可以在这个屏幕上找到

此屏幕包含 5 个复选框,对应于 5 个布尔值,这些布尔值是我想使用数据存储保留的用户首选项。

为了尝试使数据存储正常工作,我有一个名为“DataStoreManager.kt”的文件,其中包含写入、读取和清除功能。

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import com.example.lfc.ui.settings.SettingsDetails
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException

private const val LFC_DATASTORE = "user_preferences"

class DataStoreManager (
    private val dataStore: DataStore<Preferences>) {
        companion object PreferencesKeys{
        private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = LFC_DATASTORE)
        val FRENCH_BOOL = booleanPreferencesKey("FRENCH_BOOL")
        val ENGLISH_BOOL = booleanPreferencesKey("ENGLISH_BOOL")
        val ARABIC_BOOL = booleanPreferencesKey("ARABIC_BOOL")
        val JAPANESE_BOOL = booleanPreferencesKey("JAPANESE_BOOL")
        val ESPANOL_BOOL = booleanPreferencesKey("ESPANOL_BOOL")
    }
    suspend fun saveToDataStore(settingsDetails: SettingsDetails) {
        dataStore.edit {
            it[FRENCH_BOOL] = settingsDetails.frenchBool
            it[ENGLISH_BOOL] = settingsDetails.englishBool
            it[ARABIC_BOOL] = settingsDetails.arabicBool
            it[JAPANESE_BOOL] = settingsDetails.japaneseBool
            it[ESPANOL_BOOL] = settingsDetails.espanolBool
        }
    }
    fun getFromDataStore(): Flow<SettingsDetails> = dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map {
        SettingsDetails(
            frenchBool  = it[FRENCH_BOOL]?:true,
            englishBool = it[ENGLISH_BOOL]?:true,
            arabicBool = it[ARABIC_BOOL]?:false,
            japaneseBool  = it[JAPANESE_BOOL]?:false,
            espanolBool = it[ESPANOL_BOOL]?:false,
        )
    }

    suspend fun clearDataStore() = dataStore.edit {
        it.clear()
    }
}

现在上面的屏幕与一个名为“SettingsScreen.kt”的文件相关,其中包含 UI,我可以单击和取消单击复选框,并且 UI 工作正常(从图形上讲)。

import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.lfc.LFCTopAppBar
import com.example.lfc.R
import com.example.lfc.data.DataStoreManager
import com.example.lfc.languageList
import com.example.lfc.languageListLabels
import com.example.lfc.ui.AppViewModelProvider
import com.example.lfc.ui.navigation.NavigationDestination
import kotlinx.coroutines.launch

object SettingsDestination : NavigationDestination {
    override val route = "settings"
    override val titleRes = R.string.settings_title
}




@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    navigateBack: () -> Unit,
    onNavigateUp: () -> Unit,
    canNavigateBack: Boolean = true,
    viewModel: SettingsViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
    val dataStoreManager = viewModel.dataStoreManager


    Scaffold(
        topBar = {
            LFCTopAppBar(
                title = stringResource(SettingsDestination.titleRes),
                canNavigateBack = canNavigateBack,
                navigateUp = onNavigateUp
            )
        }
    ) { innerPadding ->
        SettingsBody(
            dataStoreManager = dataStoreManager,
            settingsUiState = viewModel.settingsUiState,
            modifier = Modifier
                .padding(innerPadding)
                .verticalScroll(rememberScrollState())
                .fillMaxWidth()
        )
    }
}


@Composable
fun SettingsBody(
    dataStoreManager: DataStoreManager,
    settingsUiState: SettingsUiState,
    modifier: Modifier = Modifier
) {
    Log.d("scar3", settingsUiState.settingsDetails.toString())


    Column(
        modifier = modifier.padding(dimensionResource(id = R.dimen.padding_medium)),
        verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_large))
    ) {
        for (index in languageList.indices) {LanguageCheckbox( settingsUiState, index, dataStoreManager)}

    }
}

@Composable
private fun LanguageCheckbox( settingsUiState: SettingsUiState, index: Int, dataStoreManager:DataStoreManager) {
    val coroutineScope = rememberCoroutineScope()
    Log.d("scar4", settingsUiState.settingsDetails.toString())
    var checked by remember{
        mutableStateOf(  when(index){
        0-> settingsUiState.settingsDetails.frenchBool
        1-> settingsUiState.settingsDetails.englishBool
        2-> settingsUiState.settingsDetails.arabicBool
        3-> settingsUiState.settingsDetails.japaneseBool
        4-> settingsUiState.settingsDetails.espanolBool
        else -> false
    })}
    Log.d("scar5", checked.toString())

    Column {
        Row(verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()

        ) {
            Checkbox(
                checked = checked,
                onCheckedChange = {
                        isChecked ->
                            checked = isChecked
                            when(index){
                                0-> settingsUiState.settingsDetails.frenchBool=isChecked
                                1-> settingsUiState.settingsDetails.englishBool=isChecked
                                2-> settingsUiState.settingsDetails.arabicBool=isChecked
                                3-> settingsUiState.settingsDetails.japaneseBool=isChecked
                                4-> settingsUiState.settingsDetails.espanolBool=isChecked
                            }
                            coroutineScope.launch {
                                dataStoreManager.saveToDataStore( settingsUiState.settingsDetails)
                        }




                }

            )

            Text(languageListLabels[index])
        }

        }

}

我尝试使用 viewModel 'SettingsViewModel.kt' 来驱动这个 UI,

import android.content.Context
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.dataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.lfc.data.DataStoreManager
import com.example.lfc.languageList
import com.example.lfc.ui.home.HomeListUiState
import com.example.lfc.ui.home.HomeViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

// TODO insert in the right table depending on which collection I come from

class SettingsViewModel( val dataStoreManager: DataStoreManager) : ViewModel() {
// Here I try to initialize the UI state
    var settingsUiState by mutableStateOf( SettingsUiState())
        private set
//    what I am trying to do here is to listen to the flow within the ViewModel, 
//    and update the UIState using this flow, this is probably wrong...
    init {
        viewModelScope.launch {
            dataStoreManager.getFromDataStore().collect{
                settingsUiState.settingsDetails
            }
        }
    }

}



data class SettingsUiState( var settingsDetails: SettingsDetails = SettingsDetails())
data class SettingsDetails(
    var frenchBool: Boolean = true,
    var englishBool: Boolean = true,
    var arabicBool: Boolean= false,
    var japaneseBool: Boolean = false,
    var espanolBool: Boolean = false,
)

无论如何,这不起作用,但我也没想到它,当我退出设置屏幕并返回时,布尔值(复选框)被设置为其初始值。我知道这里可能有很多无意义的代码片段,但我很乐意接受有关如何执行此操作的任何指示,即如何使用 jetpack 实现关系 DATASTORE-> VIEWMODEL -> UI ->VIEMODEL->DATASTORE compose,而不是那些 livedata 。我不理解并且没有在我的应用程序中使用的可观察概念,因为它们不存在于 Jetpack 撰写教程中。

其他可能相关的文件,也可能是错误的

AppViewModelProvider.kt

import android.app.Application
import android.content.Context
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.lfc.LFCApplication
import com.example.lfc.data.DataStoreManager
import com.example.lfc.ui.ItemEdit.ItemEditViewModel
import com.example.lfc.ui.ItemEntry.ItemEntryViewModel
import com.example.lfc.ui.TableEntry.TableEntryViewModel
import com.example.lfc.ui.home.HomeViewModel
import com.example.lfc.ui.settings.SettingsViewModel

/**
 * Provides Factory to create instance of ViewModel for the entire LFC app
 */
object AppViewModelProvider {

    val Factory = viewModelFactory {


        // Initializer for HomeViewModel
        initializer {
            HomeViewModel(lfcApplication().container.itemsRepository)
        }
//        // Initializer for ItemEditViewModel
        initializer {
            ItemEditViewModel(
                this.createSavedStateHandle(),
                lfcApplication().container.itemsRepository
            )
        }
        // Initializer for ItemEntryViewModel
        initializer {
            ItemEntryViewModel(lfcApplication().container.itemsRepository)
        }

        // Initializer for ItemEntryViewModel
        initializer {
            TableEntryViewModel()
        }

        // Initializer for ItemEntryViewModel
        initializer {
            SettingsViewModel(lfcApplication().container.dataStoreManager)
        }
    }
}

/**
 * Extension function to queries for [Application] object and returns an instance of
 * [LFCApplication].
 */
fun CreationExtras.lfcApplication(): LFCApplication =
    (this[AndroidViewModelFactory.APPLICATION_KEY] as LFCApplication)

AppContainer.kt

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

private const val LFC_DATASTORE = "user_preferences"


/**
 * App container for Dependency injection.
 */
interface AppContainer {
    val itemsRepository: ItemsRepository
    val dataStoreManager: DataStoreManager

}

/**
 * [AppContainer] implementation that provides instance of [OfflineItemsRepository]
 */
class AppDataContainer(private val context: Context) : AppContainer {

    private val Context.dataStore by preferencesDataStore(
        name = LFC_DATASTORE
    )
    /**
     * Implementation for [ItemsRepository]
     */
    override val itemsRepository: ItemsRepository by lazy {
        OfflineItemsRepository(LFCDatabase.getDatabase(context).itemDao())
    }
    override val dataStoreManager: DataStoreManager by lazy {
        DataStoreManager(this.context.dataStore)
    }
}

LFCApplication.kt

import android.app.Application
import com.example.lfc.data.AppContainer
import com.example.lfc.data.AppDataContainer

class LFCApplication:  Application() {

    /**
     * AppContainer instance used by the rest of classes to obtain dependencies
     */
    lateinit var container: AppContainer

    override fun onCreate() {
        super.onCreate()
        container = AppDataContainer(this)
    }
}

MainActivity.kt

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.lfc.ui.theme.LFCTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LFCTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    LFCApp()
                }
            }
        }
    }
}
kotlin user-interface android-jetpack-compose viewmodel datastore
1个回答
0
投票

首先,在您的 viewModel 中,在当前代码中,您没有更新您的

settingsUiState
您应该执行以下操作:
...collect { data -> settingsUiState.update { it.copy(settingsDetails = data)} } 

第二,为了你单向的理解,我建议你看一下这个视频视频。它实际上是很长的视频,但是您可以从中获得对单向数据流的很好的理解。不过,外卖是

我们通常在 compose 中使用回调和 lambda,我们一般不使用 将 viewModels、存储库或数据存储(在您的情况下)传递到 down 到 我们的可组合树。相反,我们创建 lambda 或将它们视为 可组合项中和可组合树顶部的参数, 我们有 viewmodel 方法来处理 UI 中的每个操作,然后 我们将它们传递给我们在子进程中作为参数创建的 lambda 可组合项。

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