Reputation: 3069
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
Upvotes: 56
Views: 64999
Reputation: 7283
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 todistinctUntilChanged
operator. It is used to conflate incoming updates tovalue
inMutableStateFlow
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 {
data object Resend: VerificationError()
override fun equals(other: Any?): Boolean {
return false
}
override fun hashCode(): Int {
return Random.nextInt()
}
}
This problem may occur in different cases. For example, classes with Collection
fields. They'll be considered equal even when the content of the collection is different and therefore won't be re-emitted. Sometimes it's just easier to add a differentiating field like lastUpdated
or createdAt
to know when objects should be treated as equal.
Upvotes: 40
Reputation: 941
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<String>>
. StateFlow calls the equals method for SparseArray. When MutableList<String>
changes, the result of equals does not change at this time (even if the equals
and hashcode
methods of MutableList<String>
change).
Finally, I changed the type to SparseArray<List<String>>
. Although the performance loss caused by adding and deleting data, this also solves the problem fundamentally.
2024 update:
The previous method of overriding equal was not the best method:
A better approach is:
Copy() an Item in a collection to create a copy
Similar to executing map { it.copy } on the original base.
This way you can find that StatedFlow has been updated.
Upvotes: 5
Reputation: 76
if you want to emit same value more than once than use MutableSharedFlow instead of MutableStateFlow.
Like this
private val _categoriesFlow: MutableSharedFlow<List<Category>> = MutableSharedFlow()
val categoriesFlow: MutableSharedFlow<List<Category>> = _categoriesFlow
Upvotes: 3
Reputation: 12847
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
)
}
Upvotes: 2
Reputation: 1367
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)
Upvotes: 34
Reputation: 11018
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 }
Upvotes: 12
Reputation: 194
You could make _phoneVerificationFailed nullable and send null between the two calls!
Upvotes: -1
Reputation: 28773
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).
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.Upvotes: 2
Reputation: 941
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.
Upvotes: 1
Reputation: 139
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.
Upvotes: 13