blob: 51fa66920f06df852c7cd9397e53e6fca0f01c61 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.apps.common.util;
import static java.util.Objects.requireNonNull;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import java.util.function.BiFunction;
import java.util.function.Predicate;
/**
* Utility methods for using {@link LiveData}. In general for Boolean operations, {@code null} is
* treated as an "unknown" value, and operations may use short-circuit evaluation to determine the
* result. LiveData may be in an uninitialized state where observers are not called when registered
* (e.g. a {@link MutableLiveData} where {@link MutableLiveData#setValue(Object)} has not yet been
* called). If a Boolean operation receives an uninitialized LiveData as either of its parameters,
* the result will also be in an uninitialized state.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class LiveDataFunctions {
private LiveDataFunctions() {
}
private static volatile LiveData<?> sNullLiveData;
private static volatile LiveData<Boolean> sTrueLiveData;
private static volatile LiveData<Boolean> sFalseLiveData;
/**
* Returns a LiveData that always emits {@code null}. This is different than an uninitialized
* LiveData in that observers will be called (with {@code null}) when registered.
*/
public static <T> LiveData<T> nullLiveData() {
if (sNullLiveData == null) {
sNullLiveData = dataOf(null);
}
// null can fit any generic type
// noinspection unchecked
return (LiveData<T>) sNullLiveData;
}
/** Returns a LiveData that is initialized with {@code value}. */
public static <T> MutableLiveData<T> dataOf(@Nullable T value) {
MutableLiveData<T> data = new MutableLiveData<>();
data.setValue(value);
return data;
}
/**
* Similar to {@link Transformations#map(LiveData, Function)}, but emits {@code null} when
* {@code source} emits {@code null}. The input to {@code func} may be treated as not nullable.
*/
public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source,
@NonNull Function<T, R> func) {
return mapNonNull(source, null, func);
}
/**
* Similar to {@link Transformations#map(LiveData, Function)}, but emits {@code nullValue} when
* {@code source} emits {@code null}. The input to {@code func} may be treated as not nullable.
*/
public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source, @Nullable R nullValue,
@NonNull Function<T, R> func) {
return Transformations.map(source, value -> value == null ? nullValue : func.apply(value));
}
/**
* Similar to {@link Transformations#switchMap(LiveData, Function)}, but emits {@code null} when
* {@code source} emits {@code null}. The input to {@code func} may be treated as not nullable.
*/
public static <T, R> LiveData<R> switchMapNonNull(@NonNull LiveData<T> source,
@NonNull Function<T, LiveData<R>> func) {
return switchMapNonNull(source, null, func);
}
/**
* Similar to {@link Transformations#switchMap(LiveData, Function)}, but emits {@code nullValue}
* when {@code source} emits {@code null}. The input to {@code func} may be treated as not
* nullable.
*/
public static <T, R> LiveData<R> switchMapNonNull(@NonNull LiveData<T> source,
@Nullable R nullValue,
@NonNull Function<T, LiveData<R>> func) {
return Transformations.switchMap(source,
value -> value == null ? nullLiveData() : func.apply(value));
}
/**
* Similar to {@link Transformations#switchMap(LiveData, Function)}, but emits a FutureData,
* which provides a loading field for operations which may take a long time to finish.
*
* This LiveData emits values only when the loading status of the output changes. It will never
* emit {@code null}. If the output is loading, the emitted FutureData will have a null value
* for the data.
*/
public static <T, R> LiveData<FutureData<R>> loadingSwitchMap(LiveData<T> trigger,
@NonNull Function<T, LiveData<R>> func) {
LiveData<R> output = Transformations.switchMap(trigger, func);
return new MediatorLiveData<FutureData<R>>() {
{
addSource(trigger, data -> setValue(new FutureData<>(true, null)));
addSource(output, data ->
setValue(new FutureData<>(false, output.getValue())));
}
};
}
/**
* Returns a LiveData backed by {@code value} if and only if predicate emits {@code true}. Emits
* {@code null} otherwise.
* <p>
* This is equivalent to {@code iff(predicate, Boolean::booleanValue, value)}
*
* @see #iff(LiveData, Predicate, LiveData)
*/
public static <T> LiveData<T> iff(
@NonNull LiveData<Boolean> predicate, @NonNull LiveData<T> value) {
return iff(predicate, Boolean::booleanValue, value);
}
/**
* Returns a LiveData backed by {@code value} if and only if the trigger emits a value that
* causes {@code predicate} to return {@code true}. Emits {@code null} otherwise.
*/
public static <P, T> LiveData<T> iff(
@NonNull LiveData<P> trigger,
@NonNull Predicate<? super P> predicate,
@NonNull LiveData<T> value) {
return new BinaryOperation<>(
trigger, value, (p, v) -> p == null || !predicate.test(p) ? null : v);
}
/**
* Returns a LiveData that emits a Pair containing the values of the two parameter LiveDatas. If
* either parameter is uninitialized, the resulting LiveData is also uninitialized.
* <p>
* This is equivalent to calling {@code combine(tData, uData, Pair::new)}.
*
* @see #combine(LiveData, LiveData, BiFunction)
*/
public static <T, U> LiveData<Pair<T, U>> pair(
@NonNull LiveData<T> tData, @NonNull LiveData<U> uData) {
return combine(tData, uData, Pair::new);
}
/**
* Returns a LiveData that emits the result of {@code function} on the values of the two
* parameter LiveDatas. If either parameter is uninitialized, the resulting LiveData is also
* uninitialized.
*/
public static <T, U, R> LiveData<R> combine(
@NonNull LiveData<T> tData,
@NonNull LiveData<U> uData,
@NonNull BiFunction<T, U, R> function) {
return new BinaryOperation<>(tData, uData, function);
}
private static class BinaryOperation<T, U, R> extends MediatorLiveData<R> {
@NonNull
private final BiFunction<T, U, R> mFunction;
private boolean mTSet;
private boolean mUSet;
private boolean mValueSet;
@Nullable
private T mTValue;
@Nullable
private U mUValue;
BinaryOperation(
@NonNull LiveData<T> tLiveData,
@NonNull LiveData<U> uLiveData,
@NonNull BiFunction<T, U, R> function) {
this(tLiveData, uLiveData, true, true, function);
}
BinaryOperation(
@NonNull LiveData<T> tLiveData,
@NonNull LiveData<U> uLiveData,
boolean requireTSet,
boolean requireUSet,
@NonNull BiFunction<T, U, R> function) {
this.mFunction = function;
if (!requireTSet) {
mTSet = true;
}
if (!requireUSet) {
mUSet = true;
}
if (tLiveData == uLiveData) {
// Only add the source once and only update once when it changes.
addSource(
tLiveData,
value -> {
mTSet = true;
mUSet = true;
mTValue = value;
// if both references point to the same LiveData, then T and U are
// compatible types.
// noinspection unchecked
mUValue = (U) value;
update();
});
} else {
addSource(requireNonNull(tLiveData), this::updateT);
addSource(requireNonNull(uLiveData), this::updateU);
}
}
private void updateT(@Nullable T tValue) {
mTSet = true;
this.mTValue = tValue;
update();
}
private void updateU(@Nullable U uValue) {
mUSet = true;
this.mUValue = uValue;
update();
}
private void update() {
if (mTSet && mUSet) {
R result = mFunction.apply(mTValue, mUValue);
// Don't setValue if it's the same as the old value unless we haven't set a value
// before.
if (!mValueSet || result != getValue()) {
mValueSet = true;
setValue(result);
}
}
}
}
}