Clear values inside of the MutableLiveData response collection - kotlin

I have an app, which uses api calls, to get some data by Retrofit from the server. I've implemented SwiperRefreshLayout to allow user, to perform another call.
Currently, I'm struggling with clearing MutableLiveData collection, which stores response from the server. I'd like to clear that collection every time the OnRefreshListener will be triggered.
I've tried to "fill" the MutableLiveData with null (as it comes by default, right?) but since I've set the observable in OnCreateView to pass the data to the Adapter, after every refresh I got NullPointerException error.
How I may solve it? Should I do something like unobserving, and observing the response collection again, when OnRefreshListener is triggered? Here's some code:
ViewModel
var responseData = MutableLiveData<Model?>()
fun fetchData(baseCurrency: String, selectedCurrencies: String) {
viewModelScope.launch {
val response =
retrofitRepository.fetchHistoricalData(date, selectedCurrencies, baseCurrency)
response.enqueue(object : retrofit2.Callback<Model> {
override fun onResponse(
call: Call<HistoricalRatesModel>,
response: Response<HistoricalRatesModel>
) {
if (response.isSuccessful) {
responseData.value = response.body()
}
}
override fun onFailure(call: Call<HistoricalRatesModel>, t: Throwable) {
Log.i(TAG, "onFailure ERROR\n${t.message}")
}
})
}
}
Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHistoricalRatesBinding.inflate(inflater, container, false)
val view = mBinding.root
mViewModel.responseData.observe(requireActivity(), androidx.lifecycle.Observer {
mAdapter = Adapter()
mAdapter?.setData(it!!.rates)
mBinding.hrv.layoutManager = LinearLayoutManager(this.context)
mBinding.hrv.adapter = mAdapter
})
mBinding.refreshContainer.setOnRefreshListener {
mBinding.refreshContainer.isRefreshing = false
}
return view
}

You're not handling that potential null value in your observer - in fact you're telling the compiler that it will never be null!
mViewModel.responseData.observe(requireActivity(), androidx.lifecycle.Observer {
...
mAdapter?.setData(it!!.rates)
})
How you handle it depends on what you want to do when that null value is pushed to observers. If you want to clear the data in the adapter, you could do:
mAdapter?.setData(it?.rates ?: emptyList<Rate>())
or you could make your Adapter's setData() function accept null (and decide internally how to handle that) and then you can just do:
mAdapter?.setData(it?.rates)
If any of that's confusing, make sure you're familiar with the null safety features in Kotlin, and how thing?.stuff?.value evaluates to null if any of those variables in the chain are null

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

Kotlin SharedFlow ViewModel emits before subscribed

I am trying to use SharedFlow as data provider for a Fragment in MVVM architecture.
In the Fragment class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewModel.init()
}
In the ViewModel class:
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow()
val data: SharedFlow<DataState> = _data
fun init() {
...
//(listen for other data providers that generate data for SharedFlow)
...
viewModelCoroutineScope.launch {
val dataCollection = interactor.getDataCollection()
dataCollection.forEach { data ->
if (data != null) {
_data.emit(DataState(data = data))
}
}
}
}
The problem is that in 50% cases viewmodel.init() starts before subscriber under scope is connected to Flow - which results in some data lost.
Why SharedFlow is used? That is because ViewModel have subscriptions to other data sources which could send a lot of data instances in the irregular way all needed to collect, so StateFlow/LiveData with their "store only last value" is not good for this.
When I've tried to pin viewmodel.init() to subscriber coroutine like this:
val job = viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { value ->
handleData(data)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
job.join()
viewModel.init()
}
the ViewModel emits data, but Fragment is never collects it.
What is right way to guarantee that subscribers is on before call of the ViewModel to start data sending through SharedFlow?
You should give your SharedFlow a replay value of 1 so late subscribers will still get the most recent value. You need this anyway. If the screen rotates, the recreated Fragment will need the latest value to show in the UI.
private val _data: MutableSharedFlow<DataState> = MutableSharedFlow(replay = 1)
But actually, it would be better to use shareIn instead of MutableSharedFlow, because then you can pause collection when there are no active subscribers, so you can avoid unnecessary monitoring of resources when the associated Fragment is off-screen. Like this:
val data: SharedFlow<DataState> = interactor.getDataCollection()
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If getDataCollection() is a suspend function, you could do it like this:
val data: SharedFlow<DataState> = flow {
interactor.getDataCollection().emitAll()
}
.mapNotNull { it?.let(::DataState) }
.shareIn(viewModelScope, SharingStarted.whileSubscribed(5000L), replay = 1)
If it's not a suspend function, why do you have a getter function at all? Kotlin uses properties instead.

How do I use registerForActivityResult with StartIntentSenderForResult contract?

I am writing a Kotlin app and using Firebase for authentication.
As onActivityResult is now depraceted, I am trying to migrate my app to use registerForActivityResult. I have a link to Google account feature, that starts with the Google sign-in flow, as shown here. My code:
private fun initGoogleSignInClient() =
activity?.let {
// Configure Google Sign In
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
// Build a GoogleSignInClient with the options specified by gso.
viewModel.googleSignInClient = GoogleSignIn.getClient(it, gso)
}
private fun showLinkWithGoogle() =
startActivityForResult(viewModel.googleSignInClient.signInIntent, RC_LINK_GOOGLE)
Where initGoogleSignInClient is called in the fragment's onCreateView, and showLinkWithGoogle is called when the user taps the button on the screen. This workes perfectly.
I looked for an example using registerForActivityResult, and the best one I found was at the bottom of this page. I added this code:
private val linkWithGoogle =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
viewModel.handleGoogleResult(it.data)
}
private fun showLinkWithGoogle() =
linkWithGoogle.launch(IntentSenderRequest.Builder(viewModel.googleSignInClient.signInIntent))
But realized that IntentSenderRequest.Builder needs an IntentSender and not an Intent. I haven't found any example of how to build an IntentSender from an Intent, nor a way to get one from my GoogleSignInClient.
Could anyone please provide a full example of using registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult())?
Thank you very much!
For this use-case, you don't need an ActivityResultContracts of type StartIntentSenderForResult but one of type StartActivityForResult. Here is an example (since you did not provide your full implementation):
Fragment
private val googleRegisterResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
result.checkResultAndExecute {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
val account = task.getResult(ApiException::class.java)
loginViewModel.onEvent(LoginRegistrationEvent.SignInWithGoogle(account))
}.onFailure { e -> toast("Error: ${e.message}") }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myGoogleSignInButton.setOnClickListener {
googleRegisterResult.launch(viewModel.googleSignInClient.signInIntent)
}
}
Then, in your viewmodel, you can handle the login as you would usually do, the only difference is, that you no longer need an RC_SIGN_IN
ViewModel Example
class YourViewModel : ViewModel() {
fun onEvent(event: LoginRegistrationEvent) {
when(event) {
is LoginRegistrationEvent.SignInWithGoogle -> {
viewModelScope.launch {
val credential = GoogleAuthProvider.getCredential(event.account.idToken)
Firebase.auth.signInWithCredential(credential).await()
}
}
}
}
}
To make my life easier, I created an extension function, that checks, if the login was successful and then executes a block of code (in this case, getting the account), while caching any exceptions. Futhermore, inside your block, you have access to an instance of ActivityResult as this:
inline fun ActivityResult.checkResultAndExecute(block: ActivityResult.() -> Unit) =
if (resultCode == Activity.RESULT_OK) runCatching(block)
else Result.failure(Exception("Something went wrong"))

Why can't I use this as paramter of observe event when I use LiveData with Kotlin?

I use LiveData in my layout file, and add observe event for some LiveData variable , you can see Code C.
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
2: I think mHomeViewModel.listVoiceBySort.observe(this) {... } in Code B can work well, but in fact, it failed, why?
Code A
binding.lifecycleOwner = this.viewLifecycleOwner //It can work well
binding.lifecycleOwner = this //It can work well
Code B
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) { //It can work well
mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
Code C
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
//binding.lifecycleOwner = this //It can work well
binding.aHomeViewModel = mHomeViewModel
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) {
//mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
myAdapter.submitList(it)
}
...
return binding.root
}
private fun showActionMenu() {
val view = mActivity.findViewById<View>(R.id.menuMoreAction) ?: return
PopupMenu(mContext, view).run {
menuInflater.inflate(R.menu.menu_option_action, menu)
for (item in menu){
if (item.itemId == R.id.menuMoreActionShowCheckBox){
mHomeViewModel.displayCheckBox.observe(this#FragmentHome){
//mHomeViewModel.displayCheckBox.observe(this#FragmentHome.viewLifecycleOwner){ //It can work well
if (it){
item.title =mContext.getString(R.string.menuMoreActionHideCheckBox)
}else{
item.title =mContext.getString(R.string.menuMoreActionShowCheckBox)
}
}
}
}
...
}
}
...
}
viewLifecycleOwner is available only after view of the Fragment is being inflated. Referencing the Fragment this for lifecycleOwner will use the Fragment lifecycle.
You can use this (referencing the Fragment) before view of the fragment is inflated, in onAttach() and onCreateView(). While viewLifecycle can be used in and after any point after view is completely created, after onViewCreated().
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
Because they are both LifecycleOwner. Although you generally want to use viewLifecycleOwner from onViewCreated, otherwise you can get bugs on fragments that are in "limbo state" (replace().addToBackStack()'d).
2.) private fun showActionMenu() { ... observe(
You generally shouldn't "start observing" things that are triggered by side-effects, otherwise you will end up with observers that don't unregister even after your PopupMenu is dismissed, for example.

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!