Skip to content

Commit

Permalink
LiveDataUtil combineLatest.
Browse files Browse the repository at this point in the history
  • Loading branch information
alan-signal authored and greyson-signal committed May 13, 2020
1 parent 3c5ad51 commit 33e3f78
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 0 deletions.
Expand Up @@ -11,6 +11,7 @@ public class DefaultValueLiveData<T> extends MutableLiveData<T> {
private final T defaultValue;

public DefaultValueLiveData(@NonNull T defaultValue) {
super(defaultValue);
this.defaultValue = defaultValue;
}

Expand Down
@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.util.livedata;

import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;

public final class LiveDataUtil {

private LiveDataUtil() {
}

/**
* Once there is non-null data on both input {@link LiveData}, the {@link Combine} function is run
* and produces a live data of the combined data.
* <p>
* As each live data changes, the combine function is re-run, and a new value is emitted always
* with the latest, non-null values.
*/
public static <A, B, R> LiveData<R> combineLatest(@NonNull LiveData<A> a,
@NonNull LiveData<B> b,
@NonNull Combine<A, B, R> combine) {
return new CombineLiveData<>(a, b, combine);
}

public interface Combine<A, B, R> {
@NonNull R apply(@NonNull A a, @NonNull B b);
}

private static final class CombineLiveData<A, B, R> extends MediatorLiveData<R> {
private A a;
private B b;

CombineLiveData(LiveData<A> liveDataA, LiveData<B> liveDataB, Combine<A, B, R> combine) {
if (liveDataA == liveDataB) {

addSource(liveDataA, (a) -> {
if (a != null) {
this.a = a;
//noinspection unchecked: A is B if live datas are same instance
this.b = (B) a;
setValue(combine.apply(a, b));
}
});

} else {

addSource(liveDataA, (a) -> {
if (a != null) {
this.a = a;
if (b != null) {
setValue(combine.apply(a, b));
}
}
});

addSource(liveDataB, (b) -> {
if (b != null) {
this.b = b;
if (a != null) {
setValue(combine.apply(a, b));
}
}
});
}
}
}
}
@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.util.livedata;

import androidx.annotation.NonNull;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.arch.core.executor.TaskExecutor;

import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

/**
* Copy of androidx.arch.core.executor.testing.InstantTaskExecutorRule.
* <p>
* I didn't want to bring in androidx.arch.core:core-testing at this time.
*/
public final class LiveDataRule extends TestWatcher {
@Override
protected void starting(Description description) {
super.starting(description);

ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(@NonNull Runnable runnable) {
runnable.run();
}

@Override
public void postToMainThread(@NonNull Runnable runnable) {
runnable.run();
}

@Override
public boolean isMainThread() {
return true;
}
});
}

@Override
protected void finished(Description description) {
super.finished(description);
ArchTaskExecutor.getInstance().setDelegate(null);
}
}
@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.util.livedata;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;

import java.util.concurrent.atomic.AtomicReference;

import static org.junit.Assert.assertFalse;

public final class LiveDataTestUtil {

/**
* Observes and then instantly un-observes the supplied live data.
* <p>
* This will therefore only work in conjunction with {@link LiveDataRule}.
*/
public static <T> T getValue(final LiveData<T> liveData) {
AtomicReference<T> data = new AtomicReference<>();
Observer<T> observer = data::set;

liveData.observeForever(observer);
liveData.removeObserver(observer);

return data.get();
}

public static <T> void assertNoValue(final LiveData<T> liveData) {
AtomicReference<Boolean> data = new AtomicReference<>(false);
Observer<T> observer = newValue -> data.set(true);

liveData.observeForever(observer);
liveData.removeObserver(observer);

assertFalse("Expected no value", data.get());
}
}
@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.util.livedata;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;

import static org.junit.Assert.assertEquals;
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.assertNoValue;
import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.getValue;

public final class LiveDataUtilTest {

@Rule
public TestRule rule = new LiveDataRule();

@Test
public void initially_no_value() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

assertNoValue(combined);
}

@Test
public void no_value_after_just_a() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataA.setValue("Hello, ");

assertNoValue(combined);
}

@Test
public void no_value_after_just_b() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataB.setValue("World!");

assertNoValue(combined);
}

@Test
public void combined_value_after_a_and_b() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataA.setValue("Hello, ");
liveDataB.setValue("World!");

assertEquals("Hello, World!", getValue(combined));
}

@Test
public void on_update_a() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataA.setValue("Hello, ");
liveDataB.setValue("World!");

assertEquals("Hello, World!", getValue(combined));

liveDataA.setValue("Welcome, ");
assertEquals("Welcome, World!", getValue(combined));
}

@Test
public void on_update_b() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataA.setValue("Hello, ");
liveDataB.setValue("World!");

assertEquals("Hello, World!", getValue(combined));

liveDataB.setValue("Joe!");
assertEquals("Hello, Joe!", getValue(combined));
}

@Test
public void combined_same_instance() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataA, (a, b) -> a + b);

liveDataA.setValue("Echo! ");

assertEquals("Echo! Echo! ", getValue(combined));
}

@Test
public void on_a_set_before_combine() {
MutableLiveData<String> liveDataA = new MutableLiveData<>();
MutableLiveData<String> liveDataB = new MutableLiveData<>();

liveDataA.setValue("Hello, ");

LiveData<String> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a + b);

liveDataB.setValue("World!");

assertEquals("Hello, World!", getValue(combined));
}

@Test
public void on_default_values() {
MutableLiveData<Integer> liveDataA = new DefaultValueLiveData<>(10);
MutableLiveData<Integer> liveDataB = new DefaultValueLiveData<>(30);

LiveData<Integer> combined = LiveDataUtil.combineLatest(liveDataA, liveDataB, (a, b) -> a * b);

assertEquals(Integer.valueOf(300), getValue(combined));
}
}

0 comments on commit 33e3f78

Please sign in to comment.