MediatorLiveData vs LiveData - kotlin

I am not able to get the idea of MediatorLiveData while working with LiveData. In documentation it is mentioned that MediatorLiveData is a subclass of LiveData that will observe LiveData and react on onChanged method. My question is what's it's difference from having a function in observe of LiveData and do something with data if the new data is different?
For example, let's say I have query from Room Database that returns LiveData as below
#Query(“SELECT * FROM Users WHERE userid = :id”)
fun getUserById(id: String): LiveData<User>
If I want to use MediatorLiveData, I can have an extension as below
fun <T> LiveData<T>.distinctUntilChanged(): LiveData<T>{
var lastValue: Any? = Any()
return MediatorLiveData<T>().apply {
addSource(this#distinctUntilChanged){
if (it != lastValue){
lastValue = it
postValue(it)
}
}
}
}
And then use it in my Activity as
userDao.getUserById("someId").distinctUntilChanged()
.observe(this, user -> {/*Do something with user*/})
Contrary to above scenario, I can just have a wrapper function inside observe method as below
var lastUser = null
userDao.getUserById("someId")
.observe(this, user -> {
if (lastUser != user){
lastUser = user
/*Do something with user*/
}
})
Can anyone elaborate why I should use MediatorLiveData instead of just having simple check?

Related

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!

How to emit the result of a subscription

I have the following situation:
I am using the RxKotlin extensions for detecting clicks in the buttons of my activity
I am using Room for inserting records in a database
This is the code related in my activity:
button.clicks()
.flatMap {
val list = mutableListOf<Answer>()
val date = Date()
list.add(Answer("some placeholder info"))
list.add(Answer("Another placeholder info"))
Observable.fromArray(list)
}
.map {
upsertStatusQuestionsViewModel.insertMultipleAnswers(it)
}.subscribe {
// it here is an object Maybe<Answer>
}
And this is the code of the ViewModel:
fun insertMultipleAnswers(answers: List<Answer>) = database.answerDao()
.createMultiple(answers.toList())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
I would like to show some information about the answer inserted in the database, for that reason, I need to get the Answer object in my subscription. However, I don't know which operator can I use for achieving that the it object in the subscription is of class Answer, instead of Maybe<Answer>.
Thanks a lot for your help!
If anyone stumbles with this, the solution is to parse the Maybe to an Observable, as Observable implements ObservableSource, so my code is now something like this:
upsert_status_questions_confirm.clicks()
.map {
val list = mutableListOf<Answer>()
list.add(Answer("Some placeholder"))
list.add(Answer("Another placeholder"))
list
}.flatMap {
upsertStatusQuestionsViewModel.insertMultipleAnswers(*it.toTypedArray())
}.subscribe({
// Success...
}, {
// Error...
})
And in the ViewModel:
fun insertMultipleAnswers(vararg answers: Answer) = database.answerDao()
.createMultiple(answers.toList())
.toObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

How To await a function call?

So I have some asynchronous operations happening, I can create some lambada, call a function and pass that value to them. But what i want is not to have the result of the operation as a parameter, I want to return them.
As a example, I have a class A with some listeners, if there is a result all listeners are notified. So basically the asyncFunction should return a result if there is one otherwise be suspended.
object A {
val listeners = mutableListOf<(Int) -> Unit>()
fun onResult(value: Int) {
listeners.forEach { it(value) }
}
}
fun asyncFunction(): Deferred<Int> {
return async {
A.listeners.add({ result ->
})
return result
}
}
What I'm thinking right now (maybe I'm completely on the wrong track), is to have something like a Deferred, to which i can send the result and it returns. Is there something like that? Can I implement a Deffered myself?
class A {
private val awaiter: ??? // can this be a Deferred ?
fun onResult(result: Int) {
awaiter.putResult(result)
}
fun awaitResult(): Int {
return awaiter.await()
}
}
val a = A()
launch {
val result = a.awaitResult()
}
launch {
a.onResult(42)
}
So I do know that with callbacks this can be handled but it would be cleaner and easier to have it that way.
I hope there is a nice and clean solution im just missing.
Your asyncFunction should in fact be a suspendable function:
suspend fun suspendFunction(): Int =
suspendCoroutine { cont -> A.listeners.add { cont.resume(it) } }
Note that it returns the Int result and suspends until it's available.
However, this is just a fix for your immediate problem. It will still malfunction in many ways:
the listener's purpose is served as soon as it gets the first result, but it stays in the listener list forever, resulting in a memory leak
if the result arrived before you called suspendFunction, it will miss it and hang.
You can keep improving it manually (it's a good way to learn) or switch to a solid solution provided by the standard library. The library solution is CompletableDeferred:
object A {
val result = CompletableDeferred<Int>()
fun provideResult(r: Int) {
result.complete(r)
}
}
suspend fun suspendFunction(): Int = A.result.await()

return value only if Observer method called

I'm trying to achieve to return value of method only if Observer method called. But didn't know the right way. I use let but it's required unit and i have to return MutableList<Pair<String,String>>.
That's my method:
override fun getPlaylistsNameAndId(userCategory: String):MutableList<Pair<String,String>> {
val abc = mutableListOf<Pair<String,String>>()
addPlaylistViewModel.getPlaylistsForChips(userCategory).observe(this, Observer { it ->
it.forEach {
abc.add(Pair(it.playlistName,it.playlistId))
}
//i'm called
})
// return if (observer called) else wait for calling.
}
Remember that observe is an async operation. So your method will return immediately after setting up the observer. The observer will only execute later - asynchronously.
I'm doing this in my application: (Pseudo code)
val myData: List<MyData> = arrayListOf()
val myLiveData: LiveData<List<MyData>> by lazy { MyRoomDatabase.getInstance(this).myDataDao().myDataLive }
override fun onCreate() {
super.onCreate()
myLiveData.observe(this, Observer { data ->
myData = data
}
}
Essentially, whenever the live data updates, it'll also update the myData property.
So any time we access the myData property, it should be up to date.

RxJava2 Kotlin SwitchMap from RealmResults to Observable

OK this is my first question in RxJava so please be gentle.
I'm querying Realm for existing users, getting a RealmResults list back as a flowable, then I would like to either create a new user or return the existing user, then convert to JSON.
This is what I have so far. I'm a bit stuck.
fun getUsers(realm: Realm): Flowable<RealmResults<User>> {
return when (realm.isAutoRefresh) {
true -> realm.where<User>().findAllAsync().asFlowable().filter(RealmResults<User>::isLoaded)
false -> Flowable.just(realm.where<User>().findAll())
}
}
fun checkNewUserRequired(realm: Realm, results: RealmResults<User>): Observable<String> {
if (results.isEmpty()) {
//not complete, I will create a new user here
return Observable.just("Dummy")
} else {
val user = realm.where<User>().findFirst()!!
val detachedUser = realm.copyFromRealm(user)
return Observable.just(userToJsonString(realm, detachedUser))
}
}
val getNewUser= getUsers(realm)
.take(1)
.switchMap{ results -> checkNewUserRequired(realm, results) }
.subscribe{
//log result
result : String -> Log.d(TAG, "JSON OUTPUT: $result")
}
The error is on the switchmap. I'm very familiar with the operator in RxJS but I'm struggling with the syntax.
Any help much appreciated.
You are trying to switchMap an Flowable into an Observable, which are actually different types. You need to convert from one type to the other.
The easiest solution in your case, since it looks like you will not have any issues related to Back pressure, is to convert checkNewUserRequired to return a Flowable
Example
fun checkNewUserRequired(realm: Realm, results: RealmResults<User>): Flowable<String> = Flowable.just(
if (results.isEmpty()) "Dummy"
else {
val user = realm.where<User>().findFirst()!!
val detachedUser = realm.copyFromRealm(user)
userToJsonString(realm, detachedUser)
}
)
You can also convert from an existing Observable to Flowable using the function toFlowable, but then you need to specify a BackpressureStrategy.
Example
.switchMap{ results -> checkNewUserRequired(realm, results).toFlowable(BackpressureStrategy.DROP) }