How to get value of the Flow only once and immediately? - android

I have created my DataStore and I have a boolean value in it:
private val DID_REBOOT = booleanPreferencesKey("did_reboot")
val didRebootFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[DID_REBOOT] ?: false
}
suspend fun setDidReboot(didReboot: Boolean) {
context.dataStore.edit { preferences ->
preferences[DID_REBOOT] = didReboot
}
}
Now, on my BroadcastReceiver I need to check this value. Depending on the the value, I need to perform database operations:
GlobalScope.launch {
if(ds.didRebootFlow.first()) {
Log.d("-----------------", "true")
} else
Log.d("-----------------", "false")
}
In some place in my app, I am setting didRebootFlow to true. But my broadcast is printing false (in the second attempt it works fine).
2021-04-27 12:42:24.347 7469-7469/com.test.datastore D/-----------------: false
I am new to suspended functions and Flow and cannot figure it out how to make my code work.
My code logic is strongly dependant on the value of this field. How to get didRebootFlow once (and immediately) here?
I do not want to use .collect { } because I do not want "observe" didRebootFlow continuously. I would like to get its value only once. (Because observing didRebootFlow non-stop means I will be doing lots of database operations).

Related

Updating MutableStateFlow without emitting to collectors

In an Android project, we are currently trying to switch from LiveData to StateFlow in our viewmodels. But for some rare cases, we need to update our state without notifying the collectors about the change. It might sound weird when we think of the working mechanism of flows, but I want to learn if it's a doable thing or not. Any real solution or workaround would be appreciated.
If you don't need to react to the true state anywhere, but only the publicly emitted state, I would store the true state in a property directly instead of a MutableStateFlow.
private var trueState: MyState = MyState(someDefault)
private val _publicState = MutableStateFlow<MyState>()
val publicstate = _publicState.asStateFlow()
fun updateState(newState: MyState, shouldEmitPublicly: Boolean) {
trueState = newState
if (shouldEmitPublicly) {
_publicState.value = newState
}
}
If you do need to react to it, one alternative to a wrapper class and filtering (#broot's solution) would be to simply keep two separate StateFlows.
Instead of exposing the state flow directly, we can expose another flow that filters the items according to our needs.
For example, we can keep the shouldEmit flag inside emitted items. Or use any other filtering logic:
suspend fun main(): Unit = coroutineScope {
launch {
stateFlow.collect {
println("Collected: $it")
}
}
delay(100)
setState(1)
delay(100)
setState(2)
delay(100)
setState(3, shouldEmit = false)
delay(100)
setState(4)
delay(100)
setState(5)
delay(100)
}
private val _stateFlow = MutableStateFlow(EmittableValue(0))
val stateFlow = _stateFlow.filter { it.shouldEmit }
.map { it.value }
fun setState(value: Int, shouldEmit: Boolean = true) {
_stateFlow.value = EmittableValue(value, shouldEmit)
}
private data class EmittableValue<T>(
val value: T,
val shouldEmit: Boolean = true
)
We can also keep the shouldEmit flag in the object and switch it on/off to temporarily disable emissions.
If you need to expose StateFlow and not just Flow, this should also be possible, but you need to decide if ignored emissions should affect its value or not.

Kotlin Coroutine Flow: Limit the number of collector

Is there a way to limit the number of collector in a function that returns a Flow using flow builder?
I have this public method in a ViewModel
fun fetchAssets(limit: String) {
viewModelScope.launch {
withContext(Dispatchers.IO){
getAssetsUseCase(AppConfigs.ASSET_PARAMS, limit).onEach {
when (it) {
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
This method is called on ViewModel's init block, but can also be called manually on UI.
This flow emits value every 10 seconds.
Repository
override fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
interceptor.baseUrl = AppConfigs.ASSET_BASE_URL
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(service.getAssetItems(query, limit))
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
delay(10_000)
}
}
Unfortunately every time fetch() was invoke from UI, I noticed that it creates another collectors thus can ended up having tons of collector which is really bad and incorrect.
The idea is having a flow that emits value every 10 seconds but can also be invoke manually via UI for immediate data update without having multiple collectors.
You seem to misunderstand what does it mean to collect the flow or you misuse the collect operation. By collecting the flow we mean we observe it for changes. But you try to use collect() to introduce changes to the flow, which can't really work. It just starts another flow in the background.
You should collect the flow only once, so keep it inside init or wherever it is appropriate for your case. Then you need to update the logic of the flow to make it possible to trigger reloading on demand. There are many ways to do it and the solution will differ depending whether you need to reset the timer on manual update or not. For example, we can use the channel to notify the flow about the need to reload:
val reloadChannel = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
...
}
withTimeoutOrNull(10.seconds) { reloadChannel.receive() } // replace `delay()` with this
}
}
fun reload() {
reloadChannel.trySend(Unit)
}
Whenever you need to trigger the manual reload, do not start another flow or invoke another collect() operation, but instead just invoke reload(). Then the flow that is already being collected, will start reloading and will emit state changes.
This solution resets the timer on manual reload, which I believe is better for the user experience.
I ended up moving the timer on ViewModel as I can request on demand fetch while also not having multiple collectors that runs at the same time.
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
Please correct me if this one is a code smell.

StateFlow Observer is being triggered Two Times - Is this a good solution?

Okay so I've been using StateFlow with Room database for a while. Now I have one common case. At the start of my app I have a logic that if ROOM database is empty I should show an EmptyContent(), otherwise I will show the ListContent() from ROOM database.
Now every time I launch the app, I'm always getting that EmptyContent() shown for a HALF a second maybe, and then the ListContent() is displayed. After that when I'm using the app everything works normal. But at that app launch time, while ROOM database is working I guess, that EmptyContent() is shown for just a small amount of period (Because my StateFlow default value is an empty list), and after that the actual LIST from Database is displayed.
Now I have one solution for that, to just use delay() function inside a Coroutine, to wait for example 200MS and then trigger the function for reading the DATABASE, because those 200MS are enough for ROOM database to actually get the value and update my STATE FLOW variable with the actual data instead of using that StateFlow default value for a half second at the beginning.
Is that a good solution, I must ask? Because I'm using coroutine, the thread is not blocked, and I'm just waiting until ROOM database updates my STATE FLOW variable the second time.
#Composable
fun displayContent(
tasks: List<ToDoTask>,
ListContent: #Composable () -> Unit
) {
val scope = rememberCoroutineScope()
var counter by remember { mutableStateOf(0)}
LaunchedEffect(Unit){
scope.launch {
delay(200)
counter = 1
}
}
if(counter == 1){
if (tasks.isNotEmpty()) {
ListContent()
} else {
EmptyContent()
}
}
}
My suggestion would be map your expected states.
For instance:
sealed class RequestState<out T> {
object Idle : RequestState<Nothing>()
object Loading : RequestState<Nothing>()
data class Success<T>(val data: T) : RequestState<T>()
data class Error(
val t: Throwable,
var consumed: Boolean = false
) : RequestState<Nothing>()
}
And your function would be something like:
#Composable
fun YourScreen() {
val requestState = viewModel.screenData.collectAsState()
when (requestState) {
is Idle ->
// This is the default state, do nothing
is Loading ->
// Display some progress indicator
is Success ->
YourListScreen(requestState.data) // Show the list
is Error ->
// Display an error.
}
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
Of course, in your view model you must emit these values properly...
class YourView: ViewModel() {
private val _screenData =
MutableStateFlow<RequestState<List<ToDoTask>>>(RequestState.Idle)
val screenDatae: StateFlow<RequestState<List<ToDoTask>>> = _screenData
fun loadData() {
_screenData.value = Loading
try {
// load the data from database
_screenData.value = Success(yourLoadedData)
} catch (e: Exception) {
_screenData.value = Error(e)
}
}
}

Android: collecting a Kotlin Flow inside another not emitting

I have got the following method:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> = flow {
val jobDomainModelList = mutableListOf<JobDomainModel>()
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.collect { jobEntityList: List<JobEntity> ->
for (jobEntity in jobEntityList) {
categoriesRepository.getCategoryById(jobEntity.categoryId)
.collect { categoryEntity ->
if (categoryEntity.categoryId == jobEntity.categoryId) {
jobDomainModelList.add(jobEntity.toDomainModel(categoryEntity))
}
}
}
emit(jobDomainModelList)
}
}
It searches in a repository calling the search method that returns a Flow<List<JobEntity>>. Then for every JobEntity in the flow, I need to fetch from the DB the category to which that job belongs. Once I have that category and the job, I can convert the job to a domain model object (JobDomainModel) and add it to a list, which will be returned in a flow as the return object of the method.
The problem I'm having is that nothing is ever emitted. I'm not sure if I'm missing something from working with flows in Kotlin, but I don't fetch the category by ID (categoriesRepository.getCategoryById(jobEntity.categoryId)) it then works fine and the list is emitted.
Thanks a lot in advance!
I think the problem is that you're collecting infinite length Flows, so collect never returns. You should use .take(1) to get a finite Flow before collecting it, or use first().
The Flows returned by your DAO are infinite length. The first value is the first query made, but the Flow will continue forever until cancelled. Each item in the Flow is a new query made when the contents of the database change.
Something like this:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> =
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.map { jobEntityList: List<JobEntity> ->
jobEntityList.mapNotNull { jobEntity ->
categoriesRepository.getCategoryById(jobEntity.categoryId)
.first()
.takeIf { it.categoryId == jobEntity.categoryId }
}
}
Alternatively, in your DAO you could make a suspend function version of getCategoryById() that simply returns the list.
Get an idea from the code below if your Kotlin coroutine flow gets lost with a continuation approximate peak alloc exception
fun test(obj1: Object,obj2: Object) = flow {
emit(if (obj1 != null) repository.postObj(obj1).first() else IgnoreObjResponse)
}.map { Pair(it, repository.postObj(obj2).first()) }

Initialize property in Kotlin so that the code is executed only once

I would like to know if there is a more idiomatic way of writing the following in Kotlin:
private var loadingFlag: Boolean = false
override fun onResume() {
super.onResume()
if (!loadingFlag) {
// do something
loadingFlag = true
}
}
so far the closest I could come up with is:
val lazyValue: Unit by lazy {
println("computed!")
Unit
}
fun main() {
println(lazyValue)
println(lazyValue)
}
which executes println("computed!") only once as expected.
Basically when I call the code I want println("computed!") to be called only once and subsequent calls do nothing. Any ideas?
I wouldn't consider it idiomatic to have a property with value Unit , or to call a property for its side effect. Those are both code smells.
You can write a custom getter for a Boolean property so it always returns false after the first time it's retrieved. It can be reset by setting it to true.
var firstTime: Boolean = true
get() = field.also { field = false }
This is just convenience. There's nothing non-idiomatic about your first block of code.

Categories

Resources