MutableStateFlow is not emitting values after 1st emit kotlin coroutine

56.5k Views Asked by At

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
10

There are 10 best solutions below

0
On

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.

  1. 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).

  1. 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.
1
On

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()
    }
}
1
On

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.

2
On

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.

5
On

Use a Channel: this does emit after sending the same value twice.

  1. Add this to your ViewModel

    val _intent = Channel<Intent>(Channel.CONFLATED)

  2. Put values using send / trySend

    _intent.send(intentLocal)

  3. observe as flow

    _intent.consumeAsFlow().collect { //do something }

0
On

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
    )
}
1
On

You could make _phoneVerificationFailed nullable and send null between the two calls!

0
On

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
5
On

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)
0
On

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.