57

I have next use case: User comes to registration form, enters name, email and password and clicks on register button. After that system needs to check if email is taken or not and based on that show error message or create new user...

I am trying to do that using Room, ViewModel and LiveData. This is some project that on which I try to learn these components and I do not have remote api, I will store everything in local database

So I have these classes:

  • RegisterActivity
  • RegisterViewModel
  • User
  • UsersDAO
  • UsersRepository
  • UsersRegistrationService

So the idea that I have is that there will be listener attached to register button which will call RegisterViewModel::register() method.

class RegisterViewModel extends ViewModel {

    //...

    public void register() {
        validationErrorMessage.setValue(null);
        if(!validateInput())
            return;
        registrationService.performRegistration(name.get(), email.get(), password.get());
    }

    //...

}

So that is the basic idea, I also want for performRegistration to return to me newly created user.

The thing that bothers me the most is I do not know how to implement performRegistration function in the service

class UsersRegistrationService {
    private UsersRepository usersRepo;

    //...

    public LiveData<RegistrationResponse<Parent>>  performRegistration(String name, String email, String password) {
         // 1. check if email exists using repository
         // 2. if user exists return RegistrationResponse.error("Email is taken") 
         // 3. if user does not exists create new user and return RegistrationResponse(newUser)
    }
}

As I understand, methods that are in UsersRepository should return LiveData because UsersDAO is returning LiveData

@Dao
abstract class UsersDAO { 
    @Query("SELECT * FROM users WHERE email = :email LIMIT 1")
    abstract LiveData<User> getUserByEmail(String email);
}

class UsersRepository {
    //...
    public LiveData<User> findUserByEmail(String email) {
        return this.usersDAO.getUserByEmail(email);
    }
}

So my problem is how to implement performRegistration() function and how to pass value back to view model and then how to change activity from RegisterActivity to MainActivity...

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
clzola
  • 1,925
  • 3
  • 30
  • 49
  • 1
    So `performRegistration` is basically an insert method? And also, not all Dao methods should return `LiveData` – Suleyman May 30 '18 at 10:39
  • 1
    yes, but it does need to check if that email is taken or not – clzola May 30 '18 at 11:10
  • 1
    So before inserting you want to query the database to check if the email already exists, right? – Suleyman May 30 '18 at 11:50
  • 1
    yes, but DAO.getUserByEmail() returns LiveData – clzola May 30 '18 at 12:15
  • 1
    You should take a look at the guide for the architecture components https://developer.android.com/jetpack/docs/guide . In the UsersRegistrationService class you'll need a MediatorLivedata to which you'll add as source LiveDatas for each state of registering a user. – user May 30 '18 at 12:34
  • But your method `getUserByEmail()` doesn't have to return a result wrapped in `LiveData`, it can simply return `User`. But in that case, you won't be able to attach an observer obviously, and you will have to handle the execution of a query on a separate thread. Because normally `LiveData` handles that for you. Try removing `LiveData` and leaving just `User`, it will tell you that it can't run on UI thread. But look into `MediatorLiveData` as @Luksprog mentioned. – Suleyman May 30 '18 at 12:42
  • @Luksprog Can you please check this code https://paste2.org/AbOh6dV5 if it is okey? Only problem I have is that I get exception that database operations cannot be done in Main thread – clzola May 30 '18 at 17:01
  • @clzola That should work you just need all the code you execute in the register() method on a background thread(with AsyncTask, Thread, ExecutorService) as Room doesn't allow queries on the main thread. – user May 30 '18 at 18:06
  • @Luksprog Thanks, I came up with that code reading a lot over internet, but wasn't sure if that is one of practices how it should be done... – clzola May 30 '18 at 18:26

13 Answers13

118

You can use my helper method:

val profile = MutableLiveData<ProfileData>()

val user = MutableLiveData<CurrentUser>()

val title = profile.combineWith(user) { profile, user ->
    "${profile.job} ${user.name}"
}

fun <T, K, R> LiveData<T>.combineWith(
    liveData: LiveData<K>,
    block: (T?, K?) -> R
): LiveData<R> {
    val result = MediatorLiveData<R>()
    result.addSource(this) {
        result.value = block(this.value, liveData.value)
    }
    result.addSource(liveData) {
        result.value = block(this.value, liveData.value)
    }
    return result
}
Tatsuya Fujisaki
  • 1,434
  • 15
  • 18
Marek Kondracki
  • 1,372
  • 2
  • 8
  • 13
36

With the help of MediatorLiveData, you can combine results from multiple sources. Here an example of how would I combine two sources:

class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() {

    private var data1: T? = null
    private var data2: K? = null

    init {
        super.addSource(source1) {
            data1 = it
            value = combine(data1, data2)
        }
        super.addSource(source2) {
            data2 = it
            value = combine(data1, data2)
        }
    }

    override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) {
        throw UnsupportedOperationException()
    }

    override fun <T : Any?> removeSource(toRemove: LiveData<T>) {
        throw UnsupportedOperationException()
    }
}

here is the gist for above, in case it is updated on the future: https://gist.github.com/guness/0a96d80bc1fb969fa70a5448aa34c215

guness
  • 6,336
  • 7
  • 59
  • 88
  • 5
    In order for this to compile (at least under Android Studio 3.4) I had to add "in" before the T type argument in the signature for addSource(). `override fun addSource(source: LiveData, onChanged: Observer) {` – Brian Stewart Apr 21 '19 at 01:44
  • 2
    what if the source count is variable instead of two? – Saman Sattari Jul 22 '19 at 05:33
  • 3
    How can I implement the "combine" part in Java? – adriennoir Sep 16 '19 at 12:58
  • 1
    Why do you need the two overrides for `unsupported operation`? – EpicPandaForce Sep 27 '19 at 11:50
  • 2
    @EpicPandaForce you don't need the overrides, but they are just prevent misuse – Joe Maher Sep 30 '19 at 02:13
  • @adriennoir you can check linked gist. I have updated for java. – guness Oct 07 '20 at 13:26
  • 1
    Why override `addSource` and `removeSource` making this object effectivly `LiveData` but still implement `MediatorLiveData`? That won't prevent misuse but make app fragile – ruX Apr 28 '21 at 14:22
  • maybe simply because `addSource` and `removeSource` are not supported and this is why we have `UnsupportedOperationException` defined. Nonetheless, you can use your own copy that supports those methods. Otherwise, you will find yourself searching for a bug around this in production instead of seeing an instant crash in development time. – guness Apr 28 '21 at 15:55
  • Extending `MediatorLiveData` and then throwing in `addSource` is bad API. You should either use delegation to internal `MediatorLiveData` or make a function instead, like in [Daniel Wilson](https://stackoverflow.com/a/56559611/5394866) anwser. – Peter Aug 17 '22 at 09:55
  • @Peter I have seen examples of this kind back in old times that worked well. This is why I have used the pattern. Honestly I would prefer the answer that has the highest upvotes right now. – guness Aug 17 '22 at 13:36
12

One approach is to use flows for this.

val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()

val titleFlow = profile.asFlow().combine(user.asFlow()){ profile, user ->
    "${profile.job} ${user.name}"
}

And then your Fragment/Activity:

viewLifecycleOwner.lifecycleScope.launch { 
    viewModel.titleFlow.collectLatest { title ->
        Log.d(">>", title)
    }
}

One advantage to this approach is that titleFlow will only emit value when both live datas have emitted at least one value. This interactive diagram will help you understand this https://rxmarbles.com/#combineLatest

Alternative syntax:

val titleFlow = combine(profile.asFlow(), user.asFlow()){ profile, user ->
    "${profile.job} ${user.name}"
}
M-Wajeeh
  • 17,204
  • 10
  • 66
  • 103
  • I have tried this but am getting two emissions of titleFlow. first time with one live data updated and second time with both live data updated. i.e. it is not waiting for both before emitting. In my `ViewModel`: `private val _liveDataA = MutableLiveData(10), private val _liveDataB = MutableLiveData(20)` `val combinedLiveData = combine(_liveDataA.asFlow(), _liveDataB.asFlow()) { a, b ->"$a $b"}`. In my `lifecycleOwner`: `var combinedLiveData = lifecycleOwner.lifecycleScope.launch{viewModel.combinedLiveData.collectLatest { combined -> Log.d(">>", combined)}}` – mars8 Apr 23 '22 at 20:27
  • i have created post https://stackoverflow.com/q/71983906/15597975 – mars8 Apr 23 '22 at 21:58
9

Jose Alcérreca has probably the best answer for this:

fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {

    val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
    val liveData2 = userCheckinsDataSource.getCheckins(newUser)

    val result = MediatorLiveData<UserDataResult>()

    result.addSource(liveData1) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    result.addSource(liveData2) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    return result
}
Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
  • 11
    Please note that this will not combine the results for you. It will only notify you when either sources change – TheRealChx101 Oct 13 '19 at 01:42
  • 3
    @TheRealChx101 the way to combine the data is in the linked article, basically the `combineLatestData` function – Stachu Aug 10 '20 at 09:03
9

without custom class

MediatorLiveData<Pair<Foo?, Bar?>>().apply {
    addSource(fooLiveData) { value = it to value?.second }
    addSource(barLiveData) { value = value?.first to it }
}.observe(this) { pair ->
    // TODO
}
Alessandro Scarozza
  • 4,273
  • 6
  • 31
  • 39
5

You can define a method that would combine multiple LiveDatas using a MediatorLiveData, then expose this combined result as a tuple.

public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> {
    private A a;
    private B b;

    public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) {
        setValue(Pair.create(a, b));

        addSource(ld1, (a) -> { 
             if(a != null) {
                this.a = a;
             } 
             setValue(Pair.create(a, b)); 
        });

        addSource(ld2, (b) -> { 
            if(b != null) {
                this.b = b;
            } 
            setValue(Pair.create(a, b));
        });
    }
}

If you need more values, then you can create a CombinedLiveData3<A,B,C> and expose a Triple<A,B,C> instead of the Pair, etc. Just like in https://stackoverflow.com/a/54292960/2413303 .

EDIT: hey look, I even made a library for you that does that from 2 arity up to 16: https://github.com/Zhuinden/livedata-combinetuple-kt

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
5

I did an approach based on @guness answer. I found that being limited to two LiveDatas was not good. What if we want to use 3? We need to create different classes for every case. So, I created a class that handles an unlimited amount of LiveDatas.

/**
  * CombinedLiveData is a helper class to combine results from multiple LiveData sources.
  * @param liveDatas Variable number of LiveData arguments.
  * @param combine   Function reference that will be used to combine all LiveData data results.
  * @param R         The type of data returned after combining all LiveData data.
  * Usage:
  * CombinedLiveData<SomeType>(
  *     getLiveData1(),
  *     getLiveData2(),
  *     ... ,
  *     getLiveDataN()
  * ) { datas: List<Any?> ->
  *     // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
  * }
  */
 class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
                           private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() {

      private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null }

      init {
         for(i in liveDatas.indices){
             super.addSource(liveDatas[i]) {
                 datas[i] = it
                 value = combine(datas)
             }
         }
     }
 }
Damia Fuentes
  • 5,308
  • 6
  • 33
  • 65
  • I got `Type Mismatch Error`, `Required: R?` `Found: Unit` at `value = combine(datas)` – imin May 22 '20 at 08:20
  • Thats because your `combine` method is returning `Unit` instead of `R`. Note `return a SomeType value` in the class documentation. – Damia Fuentes May 22 '20 at 16:37
  • TNX. It's useful helper class, but it should be noticed that return type from combine function should be as same as SomeType declare in CombinedLiveData class, generic type. for example if SomeType is Boolean? (nullable value) combine method also must return Boolean? type – Reyhane Farshbaf Jan 04 '21 at 08:54
  • * Be sure to Observe returned mediatorLiveData or else it won't push updates – Nilesh Deokar May 04 '22 at 22:43
3

Many of these answers work, but also it is assumed the LiveData generic types are not-nullable.

But what if one or more of the given input types are nullable types (given the default Kotlin upper bound for generics is Any?, which is nullable)? The result would be even though the LiveData emitter would emit a value (null), the MediatorLiveData will ignore it, thinking it's his own child live data value not being set.

This solution, instead, takes care of it by forcing the upper bound of the types passed to the mediator to be not null. Lazy but needed.

Also, this implementation avoids same-value after the combiner function has been called, which might or might not be what you need, so feel free to remove the equality check there.

fun <T1 : Any, T2 : Any, R> combineLatest(
    liveData1: LiveData<T1>,
    liveData2: LiveData<T2>,
    combiner: (T1, T2) -> R,
): LiveData<R> = MediatorLiveData<R>().apply {
    var first: T1? = null
    var second: T2? = null

    fun updateValueIfNeeded() {
        value = combiner(
            first ?: return,
            second ?: return,
        )?.takeIf { it != value } ?: return
    }

    addSource(liveData1) {
        first = it
        updateValueIfNeeded()
    }
    addSource(liveData2) {
        second = it
        updateValueIfNeeded()
    }
}
Alex Facciorusso
  • 2,290
  • 3
  • 21
  • 34
2
LiveData liveData1 = ...;
 LiveData liveData2 = ...;

 MediatorLiveData liveDataMerger = new MediatorLiveData<>();
 liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
 liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
d-feverx
  • 1,424
  • 3
  • 16
  • 31
2

if you want both value not null

fun <T, V, R> LiveData<T>.combineWithNotNull(
        liveData: LiveData<V>,
        block: (T, V) -> R
): LiveData<R> {
    val result = MediatorLiveData<R>()
    result.addSource(this) {
        this.value?.let { first ->
            liveData.value?.let { second ->
                result.value = block(first, second)
            }
        }
    }
    result.addSource(liveData) {
        this.value?.let { first ->
            liveData.value?.let { second ->
                result.value = block(first, second)
            }
        }
    }

    return result
}
Evgenii Doikov
  • 372
  • 3
  • 10
1

If you want to create a field and setup at construction time (use also):

val liveData1 = MutableLiveData(false)
val liveData2 = MutableLiveData(false)

// Return true if liveData1 && liveData2 are true
val liveDataCombined = MediatorLiveData<Boolean>().also {
    // Initial value
    it.value = false
    // Observing changes
    it.addSource(liveData1) { newValue ->
        it.value = newValue && liveData2.value!!
    }
    it.addSource(selectedAddOn) { newValue ->
        it.value = liveData1.value!! && newValue
    }
}
Blundell
  • 75,855
  • 30
  • 208
  • 233
1

Solved with LiveData extensions

fun <T, R> LiveData<T>.map(action: (t: T) -> R): LiveData<R> =
    Transformations.map(this, action)

fun <T1, T2, R> LiveData<T1>.combine(
    liveData: LiveData<T2>,
    action: (t1: T1?, t2: T2?) -> R
): LiveData<R> =
    MediatorLiveData<Pair<T1?, T2?>>().also { med ->
        med.addSource(this) { med.value = it to med.value?.second }
        med.addSource(liveData) { med.value = med.value?.first to it }
    }.map { action(it.first, it.second) }
luigi23
  • 305
  • 2
  • 9
0

Java version, if anyone else is stuck working on some old project

var fullNameLiveData = LiveDataCombiner.combine(
    nameLiveData,
    surnameLiveData,
    (name, surname) -> name + surname
)
public class LiveDataCombiner<First, Second, Combined> {

    private First first;
    private Second second;
    private final MediatorLiveData<Combined> combined = new MediatorLiveData<>();
    private final BiFunction<First, Second, Combined> combine;

    public LiveData<Combined> getCombined() {
        return combined;
    }

    public static <First, Second, Combined>LiveDataCombiner<First, Second, Combined> combine(
            LiveData<First> firstData,
            LiveData<Second> secondData,
            BiFunction<First, Second, Combined> combine
    ) {
        return new LiveDataCombiner<>(firstData, secondData, combine);
    }

    private LiveDataCombiner(
            LiveData<First> firstData,
            LiveData<Second> secondData,
            BiFunction<First, Second, Combined> combine
    ) {
        this.combine = combine;
        addSource(firstData, value -> first = value);
        addSource(secondData, value -> second = value);
    }

    private <T> void addSource(LiveData<T> source, Consumer<T> setValue) {
        combined.addSource(source, second -> {
            setValue.accept(second);
            emit(combine());
        });
    }

    private Combined combine() {
        return combine.apply(first, second);
    }

    private void emit(Combined value) {
        if (combined.getValue() != value)
            combined.setValue(value);
    }
}
Peter
  • 340
  • 4
  • 13