I want to store some preferences using DataStore. But the problem is that my application can have multiple users and therefor needs to store these preferences in separate files. I got a working example using only one user but I'm struggling to support multiple users.
Here is an example of my code:
class DataStorageRepository(private val context: Context, private val userRepository: UserRepository) {
private object PreferencesKeys {
val SETTING_ONE = intPreferencesKey("setting_one")
}
// retrieve datastore for currently logged in user.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name)
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { preferences ->
val settingOne = preferences[PreferencesKeys.SETTING_ONE] ?: 0
UserPreferences(settingOne)
}
suspend fun storeSettingOne(settingOne: Int) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SETTING_ONE] = settingOne
}
}
data class UserPreferences(val lastUsedToAccountTab: Int)
}
I'm using Koin and I tried unloading the DataStorageRepository on logout and recreating it on login but the DataStore seems to stay alive until the app is killed and I get the following crash:
java.lang.IllegalStateException: There are multiple DataStores active
for the same file: [...] You should either maintain your DataStore as
a singleton or confirm that there is no two DataStore's active on the
same file (by confirming that the scope is cancelled).
I also tried to use a CoroutineScope and kill that when I log out, but after recreating the scope on login the DataStore doesn't seem to get recreated.
Does DataStore support a way to close the connection or to handle multiple files?
Put this line inside companion object { }
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
My Code
class SettingPrefs(private val context: Context) {
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
private val soundKey = booleanPreferencesKey("sound")
private val vibrateKey = booleanPreferencesKey("vibrate")
}
val getSound: Flow<Boolean>
get() = context.dataStore.data.map {
it[soundKey] ?: true
}
suspend fun setSound(value: Boolean) {
context.dataStore.edit { it[soundKey] = value }
}
val getVibration: Flow<Boolean>
get() = context.dataStore.data.map {
it[vibrateKey] ?: true
}
suspend fun setVibration(value: Boolean) {
context.dataStore.edit { it[vibrateKey] = value }
}
}
You can use different key for different user or manual keep DataStore singleton.
For exception:
java.lang.IllegalStateException: There are multiple DataStores active for the same file: [...] You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).
androidx.datastore:datastore-*:1.0.0-alpha07 is released.
Put this at the top level of your kotlin file so there is only one instance of it.
private val Context.dataStore by preferencesDataStore("settings")
class Xxx{
}
https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0-alpha07.
The Context.createDataStore extension function has been removed and replaced with globalDataStore property delegate. Call globalDataStore once at the top level in your kotlin file. For example:
val Context.myDataStore by dataStore(...)
Put this at the top level of your kotlin file so there is only one instance of it. (I57215, b/173726702)
At the moment I was posting this question I found a solution to this problem. In order to solve my problem I needed to combine my previous two solutions. So on logout I unload the DataStorageRepository and on login I reload it again. I also needed to create a CoroutineScope that I cancel on logout.
My Module
val loggedInModule = module {
single { DataStorageRepository(get(), get()) }
}
I created a scope and passed it to the DataStore
var loggedInScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name, scope = loggedInScope)
On Login
loggedInScope = CoroutineScope(Dispatchers.Default)
loadKoinModules(loggedInModule)
On Logout
loggedInScope.cancel()
unloadKoinModules(loggedInModule)
Just put your declaration datastore out of your DataStorageRepository class
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name="settings")
class DataStorageRepository(context: Context) {
private var appContext = context.applicationContext
val mData: Flow<String?> = appContext.dataStore.data.map { preferences ->
preferences[YOUR_KEY]
}
suspend fun insertData(value: String) {
appContext.dataStore.edit { preferences ->
preferences[YOUR_KEY] = authToken
}
}
companion object {
private val KEY = stringPreferencesKey("data")
}
}
This is what I'm using in my project:
private object UserIdBasedPrefDs {
val lock = Any()
#GuardedBy("lock")
#Volatile
var currentId: String = ""
#GuardedBy("lock")
#Volatile
var INSTANCE: DataStore<Preferences>? = null
}
fun Context.happyStore( // rename what ever you like.
userId: String,
// below 3 optional params are same as int the `preferencesDataStore`.
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
produceMigrations: (Context) -> List<DataMigration<Preferences>> =
{ listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> = UserIdBasedPrefDs.run {
// check if current user id has been changed.
if (userId != currentId && INSTANCE != null) {
synchronized(lock) {
// release previous saved.
INSTANCE = null
// reset new user id.
currentId = userId
}
}
// below is the same logic inside the `preferencesDataStore` delegate.
INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(userId)
}
}
INSTANCE!!
}
}
Hope it can be helpful to you.
Related
I am learning MVVM.
I added an Applicaation class like this codelab code.
Then a problem occurred that the data of the property in the Repository was continuously maintained.
Looking for the cause, the Application class is a Singleton object, so the members are also singletons.
Someone told me to match the lifecycle of the repository with the viewmodel as a solution.
But I don't know how.
Since i'm using MVVM pattern, we have Fragment, ViewModel and Repository.
Speaking of the flow of the app, i create a list on the screen and add an item by pressing a button.
And when the save button is pressed, it is saved to the DB and navigates to another screen.
Repeat this process.
How can I properly reset the List data in the Repository?
Application
class WorkoutApplication : Application() {
val database by lazy { WorkoutDatabase.getDatabase(this) }
val detailRepo: DetailRepository by lazy { DetailRepository(database.workoutDao()) }
}
Repository
class DetailRepository(private val workoutDao : WorkoutDao) {
private var setInfoList = ArrayList<WorkoutSetInfo>()
private lateinit var updatedList : List<WorkoutSetInfo>
fun add() {
setInfoList.let { list ->
val item = WorkoutSetInfo(set = setInfoList.size + 1)
list.add(item)
updatedList = setInfoList.toList()
}
}
fun delete() {
if(setInfoList.size != 0) {
setInfoList.let { list ->
list.removeLast()
updatedList = list.toList()
}
}
return
}
fun save(title: String) {
private val workout = Workout(title = title)
val workoutId = workoutDao.insertWorkout(workout)
val newWorkoutSetInfoList = setInfoList.map { setInfo ->
setInfo.copy(parentWorkoutId = workoutId)
}
workoutDao.insertSetInfoList(newWorkoutSetInfoList)
}
}
I try to access the login state and other user information everywhere in my app.
Here is how I plan on doing it:
Create a UserState data class:
data class UserState(
val username: String = "",
val profileImageUrl: String = "",
var isLoggedIn: Boolean = false,
val isPremiumUser: Boolean = false,
val coins: Int = 0
)
Make it a singleton with hilt and inject it into my AuthRepository:
#Provides
#Singleton
fun provideUserState() = UserState()
#Singleton
#Provides
fun provideAuthRepository(userState: UserState): AuthRepository {
return AuthRepositoryImpl(userState)
}
then when the authentication (firebase + custom backend) is succesfull or other auth functions get called I update the userState with the userData:
class AuthRepositoryImpl #Inject constructor(
private var userState: UserState
) : AuthRepository {
override fun getUserState(): UserState {
return userState
}
override suspend fun authenticateUser(token: String){
val responseUser = authenticateUser(token)
if (responseUser != null) {
userState = UserState(
username = responseUser.username,
profileImageUrl = responseUser.profileImageUrl,
isLoggedIn = true,
isPremiumUser = responseUser.profileImageUrl,
coins = responseUser.coins
)
}
}
}
Now comes the part where I am not sure on how to do it, how do I observe the userState from my ViewModels?
If I call for example the logOut function in my AuthRepository from ViewModel X I want to see/get the change in ViewModel Y.
I call the getUserState method for example in the init block of my MainViewModel:
init {
_mainUserState.value = authRepository.getUserState()
}
So the problem is it only gets updated on the start, how can I observe changes that get made to the UserState Singleton?
I'm not sure if flows are the answer and if yes how exactly to use them int his scenario
You can do something like this with flow in you'r auth repository:
private val _authData = MutableStateFlow<AuthEvent>(AuthEvent.Nothing)
val authData: Flow<AuthEvent> = _authData
then emit like this when login/logout
_authData.emit(AuthEvent.Login)
AuthEvent is just a sealed class
Add the AuthState to AuthEvent.Login class
you can do shared flow too
then in you'r viewModel observe it under viewModelScope
viewModelScope.launch {
authRepository.getAuthFlow().collect() { data ->
when (data) {
is AuthEvent.Logout -> {
sendUiEvent(MainUIEvent.Logout)
_state.value = state.value.copy(isLoggedIn = false, authData = null)
}
is AuthEvent.Login -> {
_state.value = state.value.copy(isLoggedIn = true, authData = data.authData)
sendUiEvent(MainUIEvent.LoggedIn)
}
is AuthEvent.Nothing -> {
}
}
}
}
this appoarch allows you to handle auth status and navigation from you mainViewModel
Im trying to use datastore inside Composable to read user data but cant read the value as string to put inside Text.
That's the datastore
private val Context.userPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(
name = "user"
)
private val USER_FIRST_NAME = stringPreferencesKey("user_first_name")
suspend fun saveUserToPreferencesStore(context: Context) {
context.userPreferencesDataStore.edit { preferences ->
preferences[USER_FIRST_NAME] = "user1"
}
}
fun getUserFromPreferencesStore(context: Context): Flow<String> = context.userPreferencesDataStore.data
.map { preferences ->
preferences[USER_FIRST_NAME] ?: ""
}
and inside Composable:
#Composable
fun myComposable() {
var context = LocalContext.current
LaunchedEffect( true){
saveUserToPreferencesStore(context )
}
Text(getUserFromPreferencesStore(context ))
}
so in your code, getUserFromPreferencesStore() is returning a Flow. so you should collect that as flow, and then compose will auto update once the data is being changed. For example (something similar to this):
val user by getUserFromPreferencesStore(context).collectAsStateWithLifecycleAware(initValue)
I'm migrating from Shared preference to data store using jetpack compose. everything works fine (data is saved and can be retreated successfully). However, whenever a Data is retrieved, the composable keeps on recomposing endlessly. I'm using MVVM architecture and below is how I have implemented data store.
Below is declared in my AppModule.kt
App module in SingletonComponent
#Provides
#Singleton
fun provideUserPreferenceRepository(#ApplicationContext context: Context):
UserPreferencesRepository = UserPreferencesRepositoryImpl(context)
Then here's my ViewModel:
#HiltViewModel
class StoredUserViewModel #Inject constructor(
private val _getUserDataUseCase: GetUserDataUseCase
): ViewModel() {
private val _state = mutableStateOf(UserState())
val state: State<UserState> = _state
fun getUser(){
_getUserDataUseCase().onEach { result ->
val name = result.name
val token = result.api_token
_state.value = UserState(user = UserPreferences(name, agentCode, token, balance))
}.launchIn(viewModelScope)
}}
Finally, Here's my Repository Implementation:
class UserPreferencesRepositoryImpl #Inject constructor(
private val context: Context
): UserPreferencesRepository {
private val Context.dataStore by preferencesDataStore(name = "user_preferences")
}
private object Keys {
val fullName = stringPreferencesKey("full_name")
val api_token = stringPreferencesKey("api_token")
}
private inline val Preferences.fullName get() = this[Keys.fullName] ?: ""
private inline val Preferences.apiToken get() = this[Keys.api_token] ?: ""
override val userPreferences: Flow<UserPreferences> = context.dataStore.data.catch{
// throws an IOException when an error is encountered when reading data
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map { preferences ->
UserPreferences(name = preferences.fullName, api_token = preferences.apiToken)
}.distinctUntilChanged()
I don't know what causes the composable to recompose. Below Is the composable:
#Composable
fun LoginScreen(
navController: NavController,
userViewModel: StoredUserViewModel = hiltViewModel()
) {
Log.v("LOGIN_SCREEN", "CALLED!")
userViewModel.getUser()
}
If anyone can tell me where I've done wrong please enlighten me. I have tried to change the implementation in AppModule for UserPreferencesRepository but no luck.
Below is UseState.kt which is just a data class
data class UserState(
val user: UserPreferences? = null
)
Below is UserPreferences.kt
data class UserPreferences(val name: String, val api_token: String)
I also faced such problem. The solution was became to navigate with LauchedEffect in composable.
before:
if (hasFlight) {
navController.navigate(Screen.StartMovingScreen.route)
}
after:
if (hasFlight) {
LaunchedEffect(Unit) {
navController.navigate(Screen.StartMovingScreen.route)
}
}
This is expected behaviour: you're calling getUser on each recomposition.
#Composable function is a view builder, and should be side-effects free.
Instead you can use special side effect function, like LaunchedEffect, which will launch job only once, until it's removed from view tree or key argument is changed:
LaunchedEffect(Unit) {
userViewModel.getUser()
}
But this also will be re-called in case of configuration change, e.g. screen rotation. To prevent this case, you have two options:
Call getUser inside view model init: in this case it's guarantied that it's called only once.
Create some flag inside view model to prevent redundant request.
More info about Compose side effects in documentation.
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}