Why will the emitting data in transform disappear in Kotlin? - kotlin

I use transform to emit both Result.Loading and Result.Success(listEntityToModel(it)) in Code A , and use val myResult by mViewMode.listRecord().collectAsState(initial =Result.Error(Exception()) ) to collect data.
I find the Result.Loading disappear when I use Code A.
But if I use Code B, I can Result.Loading when I use collect.
What's wrong with Code A ?
Code A
override fun listRecord(): Flow<Result<List<MRecord>>> {
return mRecordDao.listRecord().transform {
emit(Result.Loading) //It disappear when colloect
emit( Result.Success(listEntityToModel(it)) )
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
val myResult by mViewMode.listRecord().collectAsState(initial =Result.Error(Exception()) )
Code B
override fun listRecord(): Flow<Result<List<MRecord>>> {
return flow {
emit(Result.Loading) //I can get it when collect
val s=mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }
emitAll(s)
}
}
//The same

In Code A, you are transforming the parent flow, so inside the transform function, the first result from the parent flow has already arrived. It will emit "loading" and then emit the result immediately afterwards because the result is already available. The collector will process both one after the other on the same thread, so there will be no opportunity for the UI to show the first value.
In Code B, you emit "loading" before you even begin to collect from the other flow, so it will be a little while before that first result from the other flow arrives. It has to do some database query to get the first result.

Related

LiveData Observer isn't triggered for the second time

I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
val result = MutableLiveData<ApiResponse<DisastersDTO?>>()
apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if(response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
return result
}
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
remoteDataSource.getDisastersByFilter(filter).map {
when (it) {
is ApiResponse.Empty -> Resource.Error("Terjadi error")
is ApiResponse.Error -> Resource.Error(it.errorMessage)
is ApiResponse.Loading -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(
DataMapper.disastersResponseToDisasterDomain(
it.data
)
)
}
}
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
useCase.getDisastersByFilter(filter)
Here's the **MapsFragment**
private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
when (it) {
is Resource.Success -> {
Log.d("MapsFragmentFilter", "${it.data}")
it.data?.let { listDisaster ->
if(listDisaster.isNotEmpty()) {
map.clear()
addGeofence(listDisaster)
listDisaster.map { disaster ->
placeMarker(disaster)
addCircle(disaster)
}
}
}
}
is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()
is Resource.Loading -> {}
}
}
Here's the MainActivity that triggers the function to hit API
private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
when (menu.itemId) {
R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
R.id.about_us -> viewModel.getDisasters()
}
binding.drawerLayoutMain.closeDrawers()
true
}
I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.
That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.
I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.
If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)
The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.
I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
What I believe you are doing wrong is using LiveData in the first place while using a retrofit.
You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.
And while calling this function from ViewModel, wrap it with viewModelScope.launch{}
fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
useCase.getDisastersByFilter(filter).collect{
// do something....
// assign the values to MutableLiveData or MutableStateFlows
}
}
You should either be using RxJava or CallbackFlow.
I prefer Flows, given below is an example of how your code might look if you use callback flow.
suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
callbackFlow {
apiService.getDisastersByFilter(filter)
.enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if (response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
trySend(ApiResponse.Success(it))
// result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
// result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
trySend(ApiResponse.Error("Terjadi kesalahan!"))
// result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
trySend(ApiResponse.Error(t.localizedMessage!!))
// result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
awaitClose()
}

How can I know which the subclass of sealed class will return when I use Compose in Android Studio?

The Result<out R> is a sealed class which hold three subclass Success, Error and Loading.
The fun Greeting is #Composable.
By my design, I define queryList as Result class, and it is assigned as Loading first, then it will be Success or Error.
1: But the following code can't be compiled as the following error information, what's wrong with my Code?
2: Is there a better solution for my design?
Compile error
Property delegate must have a 'getValue(Nothing?, KProperty>)' method. None of the following functions are suitable.*
#Composable
fun Greeting(
name: String,
mViewMode:SoundViewModel= viewModel()
) {
Column() {
//The following code cause error.
val queryList by produceState(initialValue = Result<Flow<List<MRecord>>>.Loading ) {
value = mViewMode.listRecord()
}
when (queryList){
is Loading -> { ...}
is Error -> { ...}
is Success -> {...}
}
}
}
class SoundViewModel #Inject constructor(): ViewModel()
{
fun listRecord(): Result<Flow<List<MRecord>>>{
return aSoundMeter.listRecord()
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
Since queryList is backed by a delegate, it can not be final.
This means in theory, each time you access it, it might hold a different value. The kotlin compiler is very pessimistic about this and assumes that between the time the is Result.Success branch of your when statement is selected and val mydata = queryList.data is executed, the value of queryList might have changed.
To solve this, you can assign the current value of queryList to a final variable and work with that one instead:
when (val currentList = queryList) {
is Result.Error -> {}
is Result.Loading -> {}
is Result.Success -> {
SomeComposable(currentList.data) //currentList is properly smart-cast to Result.Success
}
}

When do I need use emitAll instead of emit in Kotlin?

In Code A, I should use emit( Result.Success(listEntityToModel(it)) ), and in Code B, I should use emitAll(s).
When do I need use emitAll instead of emit in Kotlin?
Code A
override fun listRecord(): Flow<Result<List<MRecord>>> {
return mRecordDao.listRecord().transform {
emit(Result.Loading)
emit( Result.Success(listEntityToModel(it)) )
}
}
Code B
override fun listRecord(): Flow<Result<List<MRecord>>> {
return flow {
emit(Result.Loading)
val s=mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }
emitAll(s)
}
}
transform is called on each element of a parent flow. If you want to re-emit that element, the only thing you can emit is that one element.
Your Code A is kind of pointless, because it emits a Loading state presumably because you want to show in the UI that something is loading, but it immediately emits data afterwards, so there will be no time to show that loading state. And since the lambda in transform is called on each element after it arrives, it is too late for a loading state to be useful.
In Code B, you are building a flow from scratch with the flow builder. emitAll() emits an entire flow instead of doing it one by one. emitAll(s) is the same as s.collect { emit(it) }.

Suspending until StateFlow reaches one of the desired states and returning the result

Consider a sealed class State.
sealed class State {
object Unknown : State()
object Loading : State()
object Success : State()
data class Failure(val exception: Exception)
}
I have a stateflow where consumers can actively listen to the state updates.
val state:State = MutableStateFlow(State.Unknown)
Now, I also want to have a simple suspend method which waits till the state reaches either Success or Failure, so consumers who just need the result once need not be aware of the stateflow.
How to achieve this?
Although you already came up with a working solution, you might want to make use of the built-in Flow.first { ... } operator for simplicity.
suspend fun waitForResult(): State {
val resultStates = setOf(State.Success::class, State.Failure::class)
return state.first { it::class in resultStates }
}
I was able to come up with the following extension function which looks to be working fine.
suspend fun waitForResult(): State {
val resultStates = setOf(State.Success::class, State.Failure::class)
return state.waitForStates(resultStates)
}
suspend fun <T : Any> StateFlow<T>.waitForStates(states: Set<KClass<out T>>): T = coroutineScope {
var currentValue = value
// not needed for correctness, just an optimisation
if (currentValue::class in states) {
return currentValue
}
coroutineScope {
collect {
if (it::class in states) {
currentValue = it
cancel()
}
}
}
return currentValue
}

MutableStateFlow is not emitting values after 1st emit kotlin coroutine

This is my FirebaseOTPVerificationOperation class, where my MutableStateFlow properties are defined, and values are changed,
#ExperimentalCoroutinesApi
class FirebaseOTPVerificationOperation #Inject constructor(
private val activity: Activity,
val logger: Logger
) {
private val _phoneAuthComplete = MutableStateFlow<PhoneAuthCredential?>(null)
val phoneAuthComplete: StateFlow<PhoneAuthCredential?>
get() = _phoneAuthComplete
private val _phoneVerificationFailed = MutableStateFlow<String>("")
val phoneVerificationFailed: StateFlow<String>
get() = _phoneVerificationFailed
private val _phoneCodeSent = MutableStateFlow<Boolean?>(null)
val phoneCodeSent: StateFlow<Boolean?>
get() = _phoneCodeSent
private val _phoneVerificationSuccess = MutableStateFlow<Boolean?>(null)
val phoneVerificationSuccess: StateFlow<Boolean?>
get() = _phoneVerificationSuccess
fun resendPhoneVerificationCode(phoneNumber: String) {
_phoneVerificationFailed.value = "ERROR_RESEND"
}
}
This is my viewmodal, from where i am listening the changes in stateflow properties, as follows,
class OTPVerificationViewModal #AssistedInject constructor(
private val coroutinesDispatcherProvider: AppCoroutineDispatchers,
private val firebasePhoneVerificationListener: FirebaseOTPVerificationOperation,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
#AssistedInject.Factory
interface Factory {
fun create(savedStateHandle: SavedStateHandle): OTPVerificationViewModal
}
val phoneAuthComplete = viewModelScope.launch {
firebasePhoneVerificationListener.phoneAuthComplete.filter {
Log.e("1","filter auth $it")
it.isNotNull()
}.collect {
Log.e("2","complete auth $it")
}
}
val phoneVerificationFailed = viewModelScope.launch {
firebasePhoneVerificationListener.phoneVerificationFailed.filter {
Log.e("3","filter failed $it")
it.isNotEmpty()
}.collect {
Log.e("4","collect failed $it")
}
}
val phoneCodeSent = viewModelScope.launch {
firebasePhoneVerificationListener.phoneCodeSent.filter {
Log.e("5","filter code $it")
it.isNotNull()
}.collect {
Log.e("6","collect code $it")
}
}
val phoneVerificationSuccess = viewModelScope.launch {
firebasePhoneVerificationListener.phoneVerificationSuccess.filter {
Log.e("7","filter success $it")
it.isNotNull()
}.collect {
Log.e("8","collect success $it")
}
}
init {
resendVerificationCode()
secondCall()
}
private fun secondCall() {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
delay(10000)
resendVerificationCode()
}
}
fun resendVerificationCode() {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
firebasePhoneVerificationListener.resendPhoneVerificationCode(
getNumber()
)
}
}
private fun getNumber() =
"+9191111116055"
}
The issue is that
firebasePhoneVerificationListener.phoneVerificationFailed
is fired in viewmodal for first call of,
init {
resendVerificationCode()
}
but for second call of:
init {
secondCall()
}
firebasePhoneVerificationListener.phoneVerificationFailed is not fired in viewmodal, I don't know why it happened, any reason or explanation will be very appericated.
Current Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
Expected Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
filter failed ERROR_RESEND
collect failed ERROR_RESEND
Pankaj's answer is correct, StateFlow won't emit the same value twice. As the documentation suggests:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
Therefore, to resolve this issue you can create a wrapping class and override the equals (and hashCode) method to return false even if the classes are in fact the same:
sealed class VerificationError {
object Resend: VerificationError()
override fun equals(other: Any?): Boolean {
return false
}
override fun hashCode(): Int {
return Random.nextInt()
}
}
StateFlow is SharedFlow:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
Described in more detail in my article: https://veldan1202.medium.com/kotlin-setup-sharedflow-31debf613b91
val shared = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
shared.tryEmit(value)
The value emitted by state flow is conflated and doesn't emit the same consecutive result twice, you can think as if a condition check is validating the old emitted value is not equal to the newly emitted value.
Current Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
(filter failed ERROR_RESEND
collect failed ERROR_RESEND) This being the same old value which was emitted so you will not see them getting emitted.
Use a Channel: this does emit after sending the same value twice.
Add this to your ViewModel
val _intent = Channel<Intent>(Channel.CONFLATED)
Put values using send / trySend
_intent.send(intentLocal)
observe as flow
_intent.consumeAsFlow().collect { //do something }
I think I have some more in-depth understanding of this issue. The first thing to be sure is that for StateFlow, it is not recommended to use variable collection types (such as MutableList, etc.). Because MutableList is not thread safe. If there are multiple references in the core code, it may cause the program to crash.
Before, the method I used was to wrap the class and override the equals method. However, I think this solution is not the safest method. The safest way is for deep copy, Kotlin provides toMutableList() and toList() methods are both deep copy. The emit method judges whether there is a change depends on whether the result of equals() is equal.
The reason I have this problem is that the data type using emit() is: SparseArray<MutableList>. StateFlow calls the equals method for SparseArray. When MutableList changes, the result of equals does not change at this time (even if the equals and hashcode methods of MutableList change).
Finally, I changed the type to SparseArray<List>. Although the performance loss caused by adding and deleting data, this also solves the problem fundamentally.
As mentioned above, LiveData emits data every time, while StateFlow emits only different values. tryEmit() doesn't work. In my case I found two solutions.
If you have String data, you can emit again this way:
private fun emitNewValue() {
subscriber.value += " "
subscriber.value.dropLast(1)
}
For another class you can use this (or create an extension function):
private fun <T> emitNewValue(value: T) {
if (subscriber.value == value) {
subscriber.value = null
}
subscriber.value = value
}
But it's a bad and buggy way (values are emitted twice additionally).
Try to find all subscribers that change their values. It can be not evident. For instance, focus change listener, Switch (checkbox). When you toggle Switch, a text can also change, so you should subscribe to this listener. The same way when you focus other view, an error text can change.
Use wrapper object with any unique id, for example:
class ViewModel {
private val _listFlow = MutableStateFlow(ListData(emptyList()))
val listFlow: StateFlow<ListData> get() = _listFlow
fun update(list:List<String>){
_listFlow.value = ListData(list)
}
data class ListData constructor(
val list: List<String>,
private val id: UUID = UUID.randomUUID(),//added unique id
)
}
I had a similar problem after merging the streams.
The emit() function will not be executed if == is used to determine equality.
The way to solve the problem: You can wrap a layer and rewrite the hashCode() and equals() methods. The equals() method directly returns false.
This solution works in my code. The stream after the combine has also changed.
Pankaj's answer is correct, StateFlow will not emit the same value twice.
Before wrapping, the result of == is still true even if the content is different.
You could make _phoneVerificationFailed nullable and send null between the two calls!