My understanding of AndroidX DataStore is the operations are supposed to be thread-safe and transactional. But I'm setting a value then immediately reading it, and the value has not been updated. What am I doing wrong? This shouldn't be possible should it?
Here are my "get" and "set" functions.
fun getValue(keyStr: String): String
{
val key = stringPreferencesKey(keyStr)
val value = runBlocking {
context.dataStore.data.map { it[key] ?: "" }
}
return runBlocking { value.first() }
}
fun setValue(keyStr: String, valueStr: String) {
val key = stringPreferencesKey(keyStr)
runBlocking {
context.dataStore.edit { preferences -> preferences[key] = valueStr }
}
}
And here is how they call them in my Application's CREATE method.
setValue("TEST", "testing")
val test = getValue("TEST")
After the "get" call, test=="".
Related
I am trying to poplulate values from a datastore. I only want to recover them from the datastore once, which is why I am canceling the job after 1 second. This prevents it constantly updating.
This does not work. (1)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does not work. (2)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does work! (3)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val job2 = Job()
val job3 = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
val scope2 = CoroutineScope(job2 + Dispatchers.IO)
val scope3 = CoroutineScope(job3 + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope2.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope3.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
job2.cancel()
job3.cancel()
}
Here is the TempDataStore class (4)
class TempDataStore(private val context: Context) {
companion object{
private val Context.dataStore by preferencesDataStore(name = "TempDataStore")
val DISPLAY_TEXT_KEY = stringPreferencesKey("display_text")
val DIE_ONE = stringPreferencesKey("die_one")
val DIE_TWO = stringPreferencesKey("die_two")
}
val getDisplayText: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DISPLAY_TEXT_KEY] ?: "Roll to start!"
}
suspend fun saveDisplayText(text: String) {
context.dataStore.edit { preferences ->
preferences[DISPLAY_TEXT_KEY] = text
}
}
val getDieOne: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_ONE] ?: "1"
}
suspend fun saveDieOne(dieOne: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_ONE] = dieOne.toString()
}
}
val getDieTwo: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_TWO] ?: "2"
}
suspend fun saveDieTwo(dieTwo: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_TWO] = dieTwo.toString()
}
}
suspend fun resetDataStore() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}
It is being called from a composable screen.
LaunchedEffect(true) {
sharedViewModel.setRoles()
sharedViewModel.saveChanges()
sharedViewModel.setupDataStore(context)
}
I was expecting (1) to work. It should run all of them at the same time and return the results accordingly. Instead of populating all of the values, it only populates the first one called. (3), works but I want to understand why it works and not (1) and (2).
Calling collect on a Flow suspends the coroutine until the Flow completes, but a Flow from DataStore never completes because it monitors for changes forever. So your first call to collect prevents the other code in your coroutine from ever being reached.
I'm not exactly why your second and third attempts aren't working, but they are extremely hacky anyway, delaying and cancelling as a means to avoid collecting forever.
Before continuing, I think you should remove the nullability from your Flow types:
val getDieOne: Flow<String?>
should be
val getDieOne: Flow<String>
since you are mapping to a non-nullable value anyway.
I don't know exactly what you're attempting, but I guess it is some initial setup in which you don't need to repeatedly update from the changing values in the Flows, so you only need the first value of each flow. You can use the first value to get that. Since these are pulling from the same data store, there's not really any reason to try to do it in parallel. So your function is pretty simple:
suspend fun setupDataStore(context: Context) {
with(TempDataStore(context)) {
die1.value = getDieOne.first().toInt()
displayText.value = getDisplayText.first()
die2.value = getDieTwo.first().toInt()
}
}
If you want just the first value, why not using the .first() method of Flow? And then you shouldn't need those new scopes!
Try out something like this:
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
die1.value = tempDataStore.getDieOne.first().toInt()
displayText.value = tempDataStore.getDisplayText.first()
die2.value = tempDataStore.getDieTwo.first().toInt()
}
EDIT:
Thanks Tenfour04 for the comment! You're right. I've fixed my code.
I'm trying to insert separators to my list using the paging 3 compose library however, insertSeparators doesn't seem to indicate when we are at the beginning or end. My expectations are that before will be null at the beginning while after will be null at the end of the list. But it's never null thus hard to know when we are at the beginning or end. Here is the code:
private val filterPreferences =
MutableStateFlow(HomePreferences.FilterPreferences())
val games: Flow<PagingData<GameModel>> = filterPreferences.flatMapLatest {
useCase.execute(it)
}.map { pagingData ->
pagingData.map { GameModel.GameItem(it) }
}.map {
it.insertSeparators {before,after->
if (after == null) {
return#insertSeparators null
}
if (before == null) {
Log.i(TAG, "before is null: ") // never reach here
return#insertSeparators GameModel.SeparatorItem("title")
}
if(condition) {
GameModel.SeparatorItem("title")
}
else null
}
}
.cachedIn(viewModelScope)
GamesUseCase
class GamesUseCase #Inject constructor(
private val executionThread: PostExecutionThread,
private val repo: GamesRepo,
) : FlowUseCase<HomePreferences, PagingData<Game>>() {
override val dispatcher: CoroutineDispatcher
get() = executionThread.io
override fun execute(params: HomePreferences?): Flow<PagingData<Game>> {
val preferences = params as HomePreferences.FilterPreferences
preferences.apply {
return repo.fetchGames(query,
parentPlatforms,
platforms,
stores,
developers,
genres,
tags)
}
}
}
FlowUseCase
abstract class FlowUseCase<in Params, out T>() {
abstract val dispatcher: CoroutineDispatcher
abstract fun execute(params: Params? = null): Flow<T>
operator fun invoke(params: Params? = null) = execute(params).flowOn(dispatcher)
}
Here is the dependency :
object Pagination {
object Version {
const val pagingCompose = "1.0.0-alpha14"
}
const val pagingCompose = "androidx.paging:paging-compose:${Version.pagingCompose}"
}
I'm assuming that filterPreferences gives you Flow of some preference and useCase.execute returns Flow<PagingData<Model>>, correct?
I believe that the problem is in usage of flatMapLatest - it mixes page events of multiple useCase.execute calls together.
You should do something like this:
val games: Flow<Flow<PagingData<GameModel>>> = filterPreferences.mapLatest {
useCase.execute(it)
}.mapLatest {
it.map { pagingData -> pagingData.map { GameModel.GameItem(it) } }
}.mapLatest {
it.map { pagingData ->
pagingData.insertSeparators { before, after -> ... }
} // .cachedIn(viewModelScope)
}
This same structure works for us very well. I'm only not sure how cachedIn will work here, we are using a different caching mechanism, but you can try.
I just want to know if it is possible for me to return activePodcastViewData. I get return not allow here anytime I tried to call it on the activePodcastViewData.Without the GlobalScope I do get everything working fine.However I updated my repository by adding suspend method to it.Hence I was getting Suspend function should only be called from a coroutine or another suspend function.
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
GlobalScope.launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
activePodcastViewData
}
}
}
return null
}
class PodcastRepo {
val rssFeedService =RssFeedService.instance
suspend fun getPodcast(url:String):Podcast?{
rssFeedService.getFeed(url)
return Podcast(url,"No name","No Desc","No image")
}
I'm not sure that I understand you correctly but if you want to get activePodcastViewData from coroutine scope you should use some observable data holder. I will show you a simple example with LiveData.
At first, add implementation:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
Now, in your ViewModel we need to create mutableLiveData to hold and emit our future data.
val podcastsLiveData by lazy { MutableLiveData<Podcast>() }
Here your method: (I wouldn't recommend GlobalScope, let's replace it)
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
CoroutineScope(Dispatchers.IO).launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
}
}
}
podcastsLiveData.postValue(activePodcastViewData)
}
As you can see your return null is turned to postValue(). Now you finally can observe this from your Activity:
viewModel.podcastsLiveData.observe(this) {
val podcast = it
//Use your data
}
viewModel.getPodcast()
Now every time you call viewModel.getPodcast() method, code in observe will be invoked.
I hope that I helped some :D
I am trying to use the following code:
suspend fun <T> SavedStateHandle.getStateFlow(
key: String,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
withContext(Dispatchers.Main.immediate) {
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.collect()
}
}
}
I am trying to use it like so:
// in a Jetpack ViewModel
var currentUserId: MutableStateFlow<String?>
private set
init {
runBlocking(viewModelScope.coroutineContext) {
currentUserId = state.getStateFlow("currentUserId", sessionManager.chatUserFlow.value?.uid)
// <--- this line is never reached
}
}
UI thread freezes. I have a feeling it's because of collect() as I'm trying to create an internal subscription managed by the enclosing coroutine context, but I also need to get this StateFlow as a field. There's also the cross-writing of values (if either changes, update the other if it's a new value).
Overall, the issue seems to like on that collect() is suspending, as I never actually reach the line after getStateFlow().
Does anyone know a good way to create an "inner subscription" to a Flow, without ending up freezing the surrounding thread? The runBlocking { is needed so that I can synchronously assign the value to the field in the ViewModel constructor. (Is this even possible within the confines of 'structured concurrency'?)
EDIT:
// For more details, check: https://gist.github.com/marcellogalhardo/2a1ec56b7d00ba9af1ec9fd3583d53dc
fun <T> SavedStateHandle.getStateFlow(
scope: CoroutineScope,
key: String,
initialValue: T
): MutableStateFlow<T> {
val liveData = getLiveData(key, initialValue)
val stateFlow = MutableStateFlow(initialValue)
val observer = Observer<T> { value ->
if (value != stateFlow.value) {
stateFlow.value = value
}
}
liveData.observeForever(observer)
stateFlow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.launchIn(scope)
return stateFlow
}
ORIGINAL:
You can piggyback over the built-in notification system in SavedStateHandle, so that
val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)
...
savedStateHandle.set(Key, "someState")
The mutator happens not through methods of MutableLiveData, but through the SavedStateHandle that will update the LiveData (and therefore the flow) externally.
I am in a similar position, but I do not want to modify the value through the LiveData (as in the accepted solution). I want to use only flow and leave LiveData as an implementation detail of the state handle.
I also did not want to have a var and initialize it in the init block. I changed your code to satisfy both of these constraints and it does not block the UI thread. This would be the syntax:
val currentUserId: MutableStateFlow<String?> = state.getStateFlow("currentUserId", viewModelScope, sessionManager.chatUserFlow.value?.uid)
I provide a scope and use it to launch a coroutine that handles flow's onCompletion and collection. Here is the full code:
fun <T> SavedStateHandle.getStateFlow(
key: String,
scope: CoroutineScope,
initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
if (liveData.value === initialValue) {
liveData.value = initialValue
}
}
val mutableStateFlow = MutableStateFlow(liveData.value)
val observer: Observer<T?> = Observer { value ->
if (value != mutableStateFlow.value) {
mutableStateFlow.value = value
}
}
liveData.observeForever(observer)
scope.launch {
mutableStateFlow.also { flow ->
flow.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}.collect { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}
}
}
mutableStateFlow
}
obj in promoType = [list of string]
its more like 10 firebase queries are running here, looking in 10 particular set of nodes and going down further.
what i'm not sure, whether i require to put on async / await on each of the queries, but all i want is 10 of these queries to run and then result me in whether a couponKey is empty or not. All i want to do is to display whether a coupon entered was correct or not.
further, in changeUserType(couponKey, couponFoundAtKey), some database write operations occur.
fun checkPromo(promoCodeET: String) = async(UI) {
try {
val database = PersistentFirebaseUtil.getDatabase().reference
val job = async(CommonPool) {
for (obj in promoType) {
val query = database.child("promos").child(obj).orderByChild("promoCode").equalTo(promoCodeET)
query.addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
couponKey = dataSnapshot.key.toString()
couponFoundAtKey = dataSnapshot.children.first().key.toString()
if (couponKey.isNotEmpty())
changeUserType(couponKey, couponFoundAtKey)
flag = true
}
}
override fun onCancelled(error: DatabaseError) {
// Failed to read value
}
})
if (flag) break
}
}
job.await()
}
catch (e: Exception) {
}
finally {
if (couponKey.isEmpty()){
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
flag = true
}
}
There are several things I find wrong with your code:
You have an outer async(UI) which doesn't make sense
Your inner async(CommonPool) doesn't make sense either, because your database call is already async
You use the antipattern where you immediately await after async, making it not really "async" (but see above, the whole thing is async with or without this)
Your fetching function has a side-effect of changing the user type
To transfer the results to the caller, you again use side-effects instead of the return value
Your code should be much simpler. You should declare a suspend fun whose return value is the pair (couponKey, coupon):
suspend fun fetchPromo(promoType: String, promoCodeET: String): Pair<String, String>? =
suspendCancellableCoroutine { cont ->
val database = PersistentFirebaseUtil.getDatabase().reference
val query = database.child("promos").child(promoType)
.orderByChild("promoCode").equalTo(promoCodeET)
query.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
cont.resume(
dataSnapshot
.takeIf { it.exists() }
?.let { snapshot ->
snapshot.key.toString()
.takeIf { it.isNotEmpty() }
?.let { key ->
Pair(key, snapshot.children.first().key.toString())
}
}
)
}
override fun onCancelled(error: DatabaseError?) {
if (error != null) {
cont.resumeWithException(MyException(error))
} else {
cont.cancel()
}
}
})
}
To call this function, use a launch(UI) at the call site. Change the user type once you get a non-null value:
launch(UI) {
var found = false
for (type in promoType) {
val (couponKey, coupon) = fetchPromo(type, "promo-code-et") ?: continue
found = true
withContext(CommonPool) {
changeUserType(couponKey, coupon)
}
break
}
if (!found) {
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
}
You say that changeUserType performs some database operations, so I wrapped them in a withContext(CommonPool).
Note also that I extracted the loop over promo types outside the function. This will result in queries being performed sequentially, but you can just write different calling code to achieve parallel lookup:
var numDone = 0
var found = false
promoType.forEach { type ->
launch(UI) {
fetchPromo(type, "promo-code-et")
.also { numDone++ }
?.also { (couponKey, coupon) ->
found = true
launch(CommonPool) {
changeUserType(couponKey, coupon)
}
}
?: if (numDone == promoType.size && !found) {
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
}
}