Proper instance creation of Android's Jetpack DataStore (alpha07 version) - android

So with the new alpha07 version, Android ditched the private val dataStore = context.createDataStore(name = "settings_pref"), however the new way they use datastore doesn't work for me.
Since upgrading from "androidx.datastore:datastore-core:1.0.0-alpha06" to alpha07, I can't seem to make my datastore syntax work without getting red-colored code (the error comes when i add context.dataStore.edit). Also downgrading back to alpha06, code that previously worked is now not working anymore (with createDataStore).
What I'm using is their example on the main page but going anywhere else they still haven't updated their examples besides this one.
#Singleton
class PreferencesManager #Inject constructor(#ApplicationContext context: Context) {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
}
If someone knows the problem (or my error), I would appreciate it.

This was throwing me too, but I figured it out (aka, guessed until it worked):
// Note: This is at the top level of the file, outside of any classes.
private val Context.dataStore by preferencesDataStore("user_preferences")
class UserPreferencesManager(context: Context) {
private val dataStore = context.dataStore
// ...
}
This is for a DataStore<Preferences>, but if you need a custom serializer, you can do the following (same parameters as the old method):
// Still top level!
private val Context.dataStore by dataStore(
fileName = "user_preferences",
serializer = MyCustomSerializer,
)

I had the same problem and found a mistake: Context should be as a member of class to use it in any method:
private val Context.dataStore by preferencesDataStore("preferences")
class Preferenses(val context: Context) { get, set, etc}

Related

Is it possible to get a context in a CompositionLocalProvider?

I've got some configuration in a json file stored at the asset folder of the app.
I need this configuration during my complete app so I thought a CompositionLocalProvider may be a good choice.
But now I realize I need the context for parsing that json file and that doesn't seem to be possible.
Might there be another way to achieve the goal I'm looking for?
This is my implementation so far:
val LocalAppConfiguration = compositionLocalOf {
Configuration.init(LocalContext.current) // <-- not possible
}
Where my Configuration is like:
object Configuration {
lateinit var branding: Branding
fun init(context: Context) {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
}
}
I would be very grateful if someone could help me further
compositionLocalOf is not a Composable function. So, LocalContext.current can not be used.
I believe you can achieve the similar goal if you move the initialization of branding outside of the default factory. You'd then do the initialization inside your actual composable where you have access to Context.
Here's a sample code to explain what I'm talking about.
val LocalAppConfiguration = compositionLocalOf {
Configuration
}
#Composable
fun RootApp(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
val brandedConfiguration = Configuration.init(LocalContext.current)
MaterialTheme {
CompositionLocalProvider(LocalAppConfiguration provides brandedConfiguration) {
//your app screen composable here.
}
}
}
Note that you will also have to modify your init method slightly.
object Configuration {
lateinit var branding: Branding
fun init(context: Context) : Configuration {
val gson = GsonBuilder().create()
branding = gson.fromJson(
InputStreamReader(context.assets.open("branding.json")),
Branding::class.java
)
return this
}
}

Kotlin Flow in ViewModel doesn't emit data from Room table sometimes

I'm trying to combine three different flows in my ViewModel to make a list of items that will then be displayed on a RecyclerView in a fragment. I found out that when navigating to the screen, when there is no data in the table yet, the flow for testData1 doesn't emit the data in the table. Happens probably 1/5 of the time. I assume it's a timing issue because it only happens so often, but I don't quite understand why it happens. Also, this only happens when I'm combining flows so maybe I can only have so many flows in one ViewModel?
I added some code to check to see if the data was in the table during setListData() and it's definitely there. I can also see the emit happening but, there is no data coming from room. Any guidance would be greatly appreciated!
Versions I'm using:
Kotlin: 1.4.20-RC
Room: 2.3.0-alpha03
Here is my ViewModel
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.flowOn(Dispatchers.Default)
.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> {
// package the three data elements up to one list of rows
...
}
}
And here is my Repository/DAO layer (repeats for each type of data)
#Query("SELECT * FROM demo_data_1_table")
abstract fun getData1AsFlow() : Flow<List<DemoData1>>
I was able to get around this issue by removing flowOn in the combine function. After removing that call, I no longer had the issue.
I still wanted to run the setListData function on the default dispatcher, so I just changed the context in the setListData instead.
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> = withContext(Dispatchers.Default) {
// package the three data elements up to one list of rows
...
}
}

Android DataStore: Calling Context.createDataStore from Java

I'm trying to implement the new typed DataStore API in Java and I'm having some issues. All the documentation seems to be in Kotlin only and trying to create a new data store is not as straight forward from the Java side it seems.
Calling DataStoreFactoryKt.createDataStore() from Java requires me to provide all the arguments including the ones with default values in the Kotlin implementation. There doesnt seem to be any #JvmOverloads annotation for that function, resulting in my predicament.
fun <T> Context.createDataStore(
fileName: String,
serializer: Serializer<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<T> =
DataStoreFactory.create(
produceFile = { File(this.filesDir, "datastore/$fileName") },
serializer = serializer,
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
)
What's the better way around this, if there is any? Or is the Data Store api simple designed to be used with Kotlin only? I have no idea how I would go about providing a CoroutineScope argument from Java.
After updating dataStore dependency to '1.0.0-alpha08' as below.
// DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
You can have preferences implementation as follow:
private val Context.dataStore by preferencesDataStore("app_preferences")
After that if you like create some preference key:
private object Keys {
val HIDE_VISITED = booleanPreferencesKey("hide_visited")
}
other options can be stringPreferencesKey, intPreferencesKey, etc.
Saving value example:
context.dataStore.edit { prefs -> prefs[Keys.HIDE_VISITED] = hideVisited }
Reading saved value example:
val hideVisited = preferences[Keys.HIDE_VISITED] ?: false
You need to add to your Grade build file the dependency for DataStore preferences:
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha04"
and not the one for Types, that way you will be able to resolve the androidx.datastore.preferences.Context.createDataStore method that you are expecting:
public fun Context.createDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
migrations: List<DataMigration<Preferences>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
) {
File(this.filesDir, "datastore/$name.preferences_pb")
}
If you need to use proto dataStore from version 1.0.0-beta01, you can:
implementation("androidx.datastore:datastore-core:1.0.0-beta01")
initialize with Data Store Factory
val data: DataStore<SomeMessage> = DataStoreFactory.create(
serializer = SessionSerializer, // your Serializer
corruptionHandler = null,
migrations = emptyList(),
scope = CoroutineScope(Dispatchers.IO + Job())
And continue as before
data.updateData {
it.toBuilder().setAddress("address").build()
}
data.collect { ChargingSession ->
ChargingSession.address
}
This is only valid for dependency version 1.0.0-alpha08 and above
#Ercan approach is correct but requires context every time we need access to the dataStore.
Below is a better approach for the same.
private val Context._dataStore: DataStore<Preferences> by preferencesDataStore(APP_PREFERENCES)
private val dataStore : DataStore<Preferences> = context._dataStore
companion object {
const val APP_PREFERENCES = "app_preferences"
}
reference: https://issuetracker.google.com/issues/173726702

SavedStateProvider with the given key is already registered

I'm currently trying the new Jetpack ViewModel with savedState.
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc01'
I'm using 1 Activity and trying to share 1 ViewModel with 2 Fragments but when I try to start the second fragment I get the error from the title.
This is how I'm calling the ViewModel with the savedInstance:
val repository = (activity?.application as App).getRepository()
viewModel = activity?.run {
ViewModelFactory(repository, this, savedInstanceState).create(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
And this is my log:
java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
at androidx.savedstate.SavedStateRegistry.registerSavedStateProvider(SavedStateRegistry.java:111)
at androidx.lifecycle.SavedStateHandleController.attachToLifecycle(SavedStateHandleController.java:50)
at androidx.lifecycle.SavedStateHandleController.create(SavedStateHandleController.java:70)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:67)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:84)
at com.xxx.yyy.presentation.details.DetailsFragment.onCreate(DetailsFragment.kt:29)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2586)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:838)
at androidx.fragment.app.FragmentTransition.addToFirstInLastOut(FragmentTransition.java:1197)
at androidx.fragment.app.FragmentTransition.calculateFragments(FragmentTransition.java:1080)
at androidx.fragment.app.FragmentTransition.startTransitions(FragmentTransition.java:119)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1866)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1824)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
Looks like it's trying to use a SavedState which was already registered and hence the error? I thought that was the whole point of the library. Can anyone help or point on how to use this passing arguments to the ViewModel and using the savedStateHandle?
You should never be calling create yourself - by doing so, you're not actually using the retained ViewModel that is already created, causing the AbstractSavedStateViewModelFactory to attempt to register the same key more than once.
Instead, you should be passing your ViewModelFactory to a ViewModelProvider instance to retrieve the already existing ViewModel or creating it only if necessary:
val repository = (activity?.application as App).getRepository()
viewModel = activity?.run {
val factory = ViewModelFactory(repository, this, savedInstanceState)
ViewModelProvider(this, factory).get(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
If you depend on fragment-ktx of version 1.1.0 or higher, you can instead use the by activityViewModels Kotlin property delegate, which lazily uses ViewModelProvider under the hood:
val viewModel: MainViewModel by activityViewModels {
val repository = (activity?.application as App).getRepository()
ViewModelFactory(repository, requireActivity(), savedInstanceState)
}
When I ran into the given key is already registered error, I did go through this, this and this. But I haven't found anything helpful. In my case, the issue was with Moshi Json Generators.
File1: VictimViewModel.kt
class VictimViewModel constructor(
private val searchManager: SearchManager,
) : ViewModel() {
}
File 2: SearchManager.kt
class SearchManager internal constructor(
private val SearchApi: SearchApi
) {
fun searchString(token: String): Either<A, B> {
return searchApi
.searchString(token)
.process(
this::parseResponse
)
}
}
File 3: SearchAPI.kt
internal interface SearchApi {
#GET("$BASE_PATH/search_string")
fun searchString(#Query("search_string") token: String): NetworkEither<SearchResponse>
}
File 4: SearchResponse.kt (This file has the root cause)
data class SearchResponse(
#Json(name = "suggestions")
val suggestions: List<String>
)
What is wrong with the above code?
I forgot to add #JsonClass(generateAdapter = true) annotate to SearchResponse data class. Which has broken the injection mechanism and started throwing key is already registered error while initiating the ViewModel. Error messaging and the actual issue are totally irrelevant. So, it took a while for me to understand the problem.
Solution:
Update the File 4: SearchResponse.kt as below
#JsonClass(generateAdapter = true)
data class SearchResponse(
#Json(name = "suggestions")
val suggestions: List<String>
)

Unable to access value from `Transformations.map` in unit test

To give some background to this question, I have a ViewModel that waits for some data, posts it to a MutableLiveData, and then exposes all the values through some properties. Here's a short gist of what that looks like:
class QuestionViewModel {
private val state = MutableLiveData<QuestionState>()
private val currentQuestion: Question?
get() = (state.value as? QuestionState.Loaded)?.question
val questionTitle: String
get() = currentQuestion?.title.orEmpty()
...
}
Then, in my test, I mock the data and just run an assertEquals check:
assertEquals("TestTitle", viewModel.questionTitle)
All of this works fine so far, but I actually want my fragment to observe for when the current question changes. So, I tried changing it around to use Transformations.map:
class QuestionViewModel {
private val state = MutableLiveData<QuestionState>()
private val currentQuestion: LiveData<Question> = Transformations.map(state) {
(it as? QuestionState.Loaded)?.question
}
val questionTitle: String
get() = currentQuestion.value?.title.orEmpty()
...
}
Suddenly, all of my assertions in the test class have failed. I made currentQuestion public and verified that it's value is null in my unit test. I've determined this is the issue because:
I can mock the data and still get the right value from my state LiveData
I can run my app and see the expected data on the screen, so this issue is specific to my unit test.
I have already added the InstantTaskExecutorRule to my unit test, but maybe that doesn't handle the Transformations methods?
I recently had the same problem, I've solved it by adding a mocked observer to the LiveData:
#Mock
private lateinit var observer: Observer<Question>
init {
initMocks(this)
}
fun `test using mocked observer`() {
viewModel.currentQuestion.observeForever(observer)
// ***************** Access currentQuestion.value here *****************
viewModel.questionTitle.removeObserver(observer)
}
fun `test using empty observer`() {
viewModel.currentQuestion.observeForever {}
// ***************** Access currentQuestion.value here *****************
}
Not sure how it works exactly or the consequences of not removing the empty observer the after test.
Also, make sure to import the right Observer class. If you're using AndroidX:
import androidx.lifecycle.Observer
Luciano is correct, it's because the LiveData is not being observed. Here is a Kotlin utility class to help with this.
class LiveDataObserver<T>(private val liveData: LiveData<T>): Closeable {
private val observer: Observer<T> = mock()
init {
liveData.observeForever(observer)
}
override fun close() {
liveData.removeObserver(observer)
}
}
// to use:
LiveDataObserver(unit.someLiveData).use {
assertFalse(unit.someLiveData.value!!)
}
Looks like you're missing the .value on the it variable.
private val currentQuestion: LiveData<Question> = Transformations.map(state) {
(it.value as? QuestionState.Loaded)?.question
}

Categories

Resources