diff --git a/app/build.gradle b/app/build.gradle index b4a2af1c8c..009ac1f1e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,10 @@ repositories { maven { url "https://jitpack.io" } } +configurations.all { + exclude group: "androidx.lifecycle", module: "lifecycle-livedata" +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) @@ -178,7 +182,6 @@ dependencies { // https://mvnrepository.com/artifact/androidx.paging/paging-runtime implementation "androidx.paging:paging-runtime:$paging_version" - // https://mvnrepository.com/artifact/androidx.preference/preference implementation "androidx.preference:preference:$preference_version" diff --git a/app/src/main/java/androidx/lifecycle/ComputableLiveData.java b/app/src/main/java/androidx/lifecycle/ComputableLiveData.java new file mode 100644 index 0000000000..837a35dbf9 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/ComputableLiveData.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.arch.core.executor.ArchTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A LiveData class that can be invalidated & computed when there are active observers. + *

+ * It can be invalidated via {@link #invalidate()}, which will result in a call to + * {@link #compute()} if there are active observers (or when they start observing) + *

+ * This is an internal class for now, might be public if we see the necessity. + * + * @param The type of the live data + * @hide internal + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public abstract class ComputableLiveData { + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Executor mExecutor; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final LiveData mLiveData; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final AtomicBoolean mInvalid = new AtomicBoolean(true); + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final AtomicBoolean mComputing = new AtomicBoolean(false); + + /** + * Creates a computable live data that computes values on the arch IO thread executor. + */ + @SuppressWarnings("WeakerAccess") + public ComputableLiveData() { + this(ArchTaskExecutor.getIOThreadExecutor()); + } + + /** + * Creates a computable live data that computes values on the specified executor. + * + * @param executor Executor that is used to compute new LiveData values. + */ + @SuppressWarnings("WeakerAccess") + public ComputableLiveData(@NonNull Executor executor) { + mExecutor = executor; + mLiveData = new LiveData() { + @Override + protected void onActive() { + mExecutor.execute(mRefreshRunnable); + } + }; + } + + /** + * Returns the LiveData managed by this class. + * + * @return A LiveData that is controlled by ComputableLiveData. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public LiveData getLiveData() { + return mLiveData; + } + + @VisibleForTesting + final Runnable mRefreshRunnable = new Runnable() { + @WorkerThread + @Override + public void run() { + boolean computed; + do { + computed = false; + // compute can happen only in 1 thread but no reason to lock others. + if (mComputing.compareAndSet(false, true)) { + // as long as it is invalid, keep computing. + try { + T value = null; + while (mInvalid.compareAndSet(true, false)) { + computed = true; + value = compute(); + } + if (computed) { + mLiveData.postValue(value); + } + } finally { + // release compute lock + mComputing.set(false); + } + } + // check invalid after releasing compute lock to avoid the following scenario. + // Thread A runs compute() + // Thread A checks invalid, it is false + // Main thread sets invalid to true + // Thread B runs, fails to acquire compute lock and skips + // Thread A releases compute lock + // We've left invalid in set state. The check below recovers. + } while (computed && mInvalid.get()); + } + }; + + // invalidation check always happens on the main thread + @VisibleForTesting + final Runnable mInvalidationRunnable = new Runnable() { + @MainThread + @Override + public void run() { + boolean isActive = mLiveData.hasActiveObservers(); + if (mInvalid.compareAndSet(false, true)) { + if (isActive) { + mExecutor.execute(mRefreshRunnable); + } + } + } + }; + + /** + * Invalidates the LiveData. + *

+ * When there are active observers, this will trigger a call to {@link #compute()}. + */ + public void invalidate() { + ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); + } + + // TODO https://issuetracker.google.com/issues/112197238 + @SuppressWarnings({"WeakerAccess", "UnknownNullness"}) + @WorkerThread + protected abstract T compute(); +} diff --git a/app/src/main/java/androidx/lifecycle/LiveData.java b/app/src/main/java/androidx/lifecycle/LiveData.java new file mode 100644 index 0000000000..b03476c8c6 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/LiveData.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +import static androidx.lifecycle.Lifecycle.State.DESTROYED; +import static androidx.lifecycle.Lifecycle.State.STARTED; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.arch.core.internal.SafeIterableMap; + +import java.util.Iterator; +import java.util.Map; + +/** + * LiveData is a data holder class that can be observed within a given lifecycle. + * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and + * this observer will be notified about modifications of the wrapped data only if the paired + * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is + * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via + * {@link #observeForever(Observer)} is considered as always active and thus will be always notified + * about modifications. For those observers, you should manually call + * {@link #removeObserver(Observer)}. + * + *

An observer added with a Lifecycle will be automatically removed if the corresponding + * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for + * activities and fragments where they can safely observe LiveData and not worry about leaks: + * they will be instantly unsubscribed when they are destroyed. + * + *

+ * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods + * to get notified when number of active {@link Observer}s change between 0 and 1. + * This allows LiveData to release any heavy resources when it does not have any Observers that + * are actively observing. + *

+ * This class is designed to hold individual data fields of {@link ViewModel}, + * but can also be used for sharing data between different modules in your application + * in a decoupled fashion. + * + * @param The type of data held by this instance + * @see ViewModel + */ +public abstract class LiveData { + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Object mDataLock = new Object(); + static final int START_VERSION = -1; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static final Object NOT_SET = new Object(); + + private SafeIterableMap, ObserverWrapper> mObservers = + new SafeIterableMap<>(); + + // how many observers are in active state + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mActiveCount = 0; + private volatile Object mData = NOT_SET; + // when setData is called, we set the pending data and actual data swap happens on the main + // thread + @SuppressWarnings("WeakerAccess") /* synthetic access */ + volatile Object mPendingData = NOT_SET; + private int mVersion = START_VERSION; + + private boolean mDispatchingValue; + @SuppressWarnings("FieldCanBeLocal") + private boolean mDispatchInvalidated; + private final Runnable mPostValueRunnable = new Runnable() { + @Override + public void run() { + Object newValue; + synchronized (mDataLock) { + newValue = mPendingData; + mPendingData = NOT_SET; + } + //noinspection unchecked + setValue((T) newValue); + } + }; + + private void considerNotify(ObserverWrapper observer) { + if (!observer.mActive) { + return; + } + // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet. + // + // we still first check observer.active to keep it as the entrance for events. So even if + // the observer moved to an active state, if we've not received that event, we better not + // notify for a more predictable notification order. + if (!observer.shouldBeActive()) { + observer.activeStateChanged(false); + return; + } + if (observer.mLastVersion >= mVersion) { + return; + } + observer.mLastVersion = mVersion; + //noinspection unchecked + observer.mObserver.onChanged((T) mData); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void dispatchingValue(@Nullable ObserverWrapper initiator) { + if (mDispatchingValue) { + mDispatchInvalidated = true; + return; + } + mDispatchingValue = true; + do { + mDispatchInvalidated = false; + if (initiator != null) { + considerNotify(initiator); + initiator = null; + } else { + for (Iterator, ObserverWrapper>> iterator = + mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { + considerNotify(iterator.next().getValue()); + if (mDispatchInvalidated) { + break; + } + } + } + } while (mDispatchInvalidated); + mDispatchingValue = false; + } + + /** + * Adds the given observer to the observers list within the lifespan of the given + * owner. The events are dispatched on the main thread. If LiveData already has data + * set, it will be delivered to the observer. + *

+ * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} + * or {@link Lifecycle.State#RESUMED} state (active). + *

+ * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will + * automatically be removed. + *

+ * When data changes while the {@code owner} is not active, it will not receive any updates. + * If it becomes active again, it will receive the last available data automatically. + *

+ * LiveData keeps a strong reference to the observer and the owner as long as the + * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to + * the observer & the owner. + *

+ * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData + * ignores the call. + *

+ * If the given owner, observer tuple is already in the list, the call is ignored. + * If the observer is already in the list with another owner, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param owner The LifecycleOwner which controls the observer + * @param observer The observer that will receive the events + */ + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { + assertMainThread("observe"); + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + // ignore + return; + } + LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); + ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); + if (existing != null && !existing.isAttachedTo(owner)) { + throw new IllegalArgumentException("Cannot add the same observer" + + " with different lifecycles"); + } + if (existing != null) { + return; + } + owner.getLifecycle().addObserver(wrapper); + } + + /** + * Adds the given observer to the observers list. This call is similar to + * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which + * is always active. This means that the given observer will receive all events and will never + * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop + * observing this LiveData. + * While LiveData has one of such observers, it will be considered + * as active. + *

+ * If the observer was already added with an owner to this LiveData, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param observer The observer that will receive the events + */ + @MainThread + public void observeForever(@NonNull Observer observer) { + assertMainThread("observeForever"); + AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer); + ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); + if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) { + throw new IllegalArgumentException("Cannot add the same observer" + + " with different lifecycles"); + } + if (existing != null) { + return; + } + wrapper.activeStateChanged(true); + } + + /** + * Removes the given observer from the observers list. + * + * @param observer The Observer to receive events. + */ + @MainThread + public void removeObserver(@NonNull final Observer observer) { + assertMainThread("removeObserver"); + ObserverWrapper removed = mObservers.remove(observer); + if (removed == null) { + return; + } + removed.detachObserver(); + removed.activeStateChanged(false); + } + + /** + * Removes all observers that are tied to the given {@link LifecycleOwner}. + * + * @param owner The {@code LifecycleOwner} scope for the observers to be removed. + */ + @SuppressWarnings("WeakerAccess") + @MainThread + public void removeObservers(@NonNull final LifecycleOwner owner) { + assertMainThread("removeObservers"); + for (Map.Entry, ObserverWrapper> entry : mObservers) { + if (entry.getValue().isAttachedTo(owner)) { + removeObserver(entry.getKey()); + } + } + } + + /** + * Posts a task to a main thread to set the given value. So if you have a following code + * executed in the main thread: + *

+     * liveData.postValue("a");
+     * liveData.setValue("b");
+     * 
+ * The value "b" would be set at first and later the main thread would override it with + * the value "a". + *

+ * If you called this method multiple times before a main thread executed a posted task, only + * the last value would be dispatched. + * + * @param value The new value + */ + protected void postValue(T value) { + boolean postTask; + synchronized (mDataLock) { + postTask = mPendingData == NOT_SET; + mPendingData = value; + } + if (!postTask) { + return; + } + ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); + } + + /** + * Sets the value. If there are active observers, the value will be dispatched to them. + *

+ * This method must be called from the main thread. If you need set a value from a background + * thread, you can use {@link #postValue(Object)} + * + * @param value The new value + */ + @MainThread + protected void setValue(T value) { + assertMainThread("setValue"); + mVersion++; + mData = value; + dispatchingValue(null); + } + + /** + * Returns the current value. + * Note that calling this method on a background thread does not guarantee that the latest + * value set will be received. + * + * @return the current value + */ + @Nullable + public T getValue() { + Object data = mData; + if (data != NOT_SET) { + //noinspection unchecked + return (T) data; + } + return null; + } + + int getVersion() { + return mVersion; + } + + /** + * Called when the number of active observers change to 1 from 0. + *

+ * This callback can be used to know that this LiveData is being used thus should be kept + * up to date. + */ + protected void onActive() { + + } + + /** + * Called when the number of active observers change from 1 to 0. + *

+ * This does not mean that there are no observers left, there may still be observers but their + * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED} + * (like an Activity in the back stack). + *

+ * You can check if there are observers via {@link #hasObservers()}. + */ + protected void onInactive() { + + } + + /** + * Returns true if this LiveData has observers. + * + * @return true if this LiveData has observers + */ + @SuppressWarnings("WeakerAccess") + public boolean hasObservers() { + return mObservers.size() > 0; + } + + /** + * Returns true if this LiveData has active observers. + * + * @return true if this LiveData has active observers + */ + @SuppressWarnings("WeakerAccess") + public boolean hasActiveObservers() { + return mActiveCount > 0; + } + + class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver { + @NonNull + final LifecycleOwner mOwner; + + LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) { + super(observer); + mOwner = owner; + } + + @Override + boolean shouldBeActive() { + return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + if (mOwner.getLifecycle().getCurrentState() == DESTROYED) { + removeObserver(mObserver); + return; + } + activeStateChanged(shouldBeActive()); + } + + @Override + boolean isAttachedTo(LifecycleOwner owner) { + return mOwner == owner; + } + + @Override + void detachObserver() { + mOwner.getLifecycle().removeObserver(this); + } + } + + private abstract class ObserverWrapper { + final Observer mObserver; + boolean mActive; + int mLastVersion = START_VERSION; + + ObserverWrapper(Observer observer) { + mObserver = observer; + } + + abstract boolean shouldBeActive(); + + boolean isAttachedTo(LifecycleOwner owner) { + return false; + } + + void detachObserver() { + } + + void activeStateChanged(boolean newActive) { + if (newActive == mActive) { + return; + } + // immediately set active state, so we'd never dispatch anything to inactive + // owner + mActive = newActive; + boolean wasInactive = LiveData.this.mActiveCount == 0; + LiveData.this.mActiveCount += mActive ? 1 : -1; + if (wasInactive && mActive) { + onActive(); + } + if (LiveData.this.mActiveCount == 0 && !mActive) { + onInactive(); + } + if (mActive) { + dispatchingValue(this); + } + } + } + + private class AlwaysActiveObserver extends ObserverWrapper { + + AlwaysActiveObserver(Observer observer) { + super(observer); + } + + @Override + boolean shouldBeActive() { + return true; + } + } + + private static void assertMainThread(String methodName) { + if (!ArchTaskExecutor.getInstance().isMainThread()) { + throw new IllegalStateException("Cannot invoke " + methodName + " on a background" + + " thread"); + } + } +} diff --git a/app/src/main/java/androidx/lifecycle/MediatorLiveData.java b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java new file mode 100644 index 0000000000..58261ddac0 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +import androidx.annotation.CallSuper; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.internal.SafeIterableMap; + +import java.util.Map; + +/** + * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on + * {@code OnChanged} events from them. + *

+ * This class correctly propagates its active/inactive states down to source {@code LiveData} + * objects. + *

+ * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them + * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object: + * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for + * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback + * is called for either of them, we set a new value in {@code liveDataMerger}. + * + *

+ * LiveData liveData1 = ...;
+ * LiveData liveData2 = ...;
+ *
+ * MediatorLiveData liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * 
+ *

+ * Let's consider that we only want 10 values emitted by {@code liveData1}, to be + * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code + * liveData1} and remove it as a source. + *

+ * liveDataMerger.addSource(liveData1, new Observer() {
+ *      private int count = 1;
+ *
+ *      {@literal @}Override public void onChanged(@Nullable Integer s) {
+ *          count++;
+ *          liveDataMerger.setValue(s);
+ *          if (count > 10) {
+ *              liveDataMerger.removeSource(liveData1);
+ *          }
+ *      }
+ * });
+ * 
+ * + * @param The type of data hold by this instance + */ +@SuppressWarnings("WeakerAccess") +public class MediatorLiveData extends MutableLiveData { + private SafeIterableMap, Source> mSources = new SafeIterableMap<>(); + + /** + * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called + * when {@code source} value was changed. + *

+ * {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active. + *

If the given LiveData is already added as a source but with a different Observer, + * {@link IllegalArgumentException} will be thrown. + * + * @param source the {@code LiveData} to listen to + * @param onChanged The observer that will receive the events + * @param The type of data hold by {@code source} LiveData + */ + @MainThread + public void addSource(@NonNull LiveData source, @NonNull Observer onChanged) { + Source e = new Source<>(source, onChanged); + Source existing = mSources.putIfAbsent(source, e); + if (existing != null && existing.mObserver != onChanged) { + throw new IllegalArgumentException( + "This source was already added with the different observer"); + } + if (existing != null) { + return; + } + if (hasActiveObservers()) { + e.plug(); + } + } + + /** + * Stops to listen the given {@code LiveData}. + * + * @param toRemote {@code LiveData} to stop to listen + * @param the type of data hold by {@code source} LiveData + */ + @MainThread + public void removeSource(@NonNull LiveData toRemote) { + Source source = mSources.remove(toRemote); + if (source != null) { + source.unplug(); + } + } + + @CallSuper + @Override + protected void onActive() { + for (Map.Entry, Source> source : mSources) { + source.getValue().plug(); + } + } + + @CallSuper + @Override + protected void onInactive() { + for (Map.Entry, Source> source : mSources) { + source.getValue().unplug(); + } + } + + private static class Source implements Observer { + final LiveData mLiveData; + final Observer mObserver; + int mVersion = START_VERSION; + + Source(LiveData liveData, final Observer observer) { + mLiveData = liveData; + mObserver = observer; + } + + void plug() { + mLiveData.observeForever(this); + } + + void unplug() { + mLiveData.removeObserver(this); + } + + @Override + public void onChanged(@Nullable V v) { + if (mVersion != mLiveData.getVersion()) { + mVersion = mLiveData.getVersion(); + mObserver.onChanged(v); + } + } + } +} diff --git a/app/src/main/java/androidx/lifecycle/MutableLiveData.java b/app/src/main/java/androidx/lifecycle/MutableLiveData.java new file mode 100644 index 0000000000..06014f0ab2 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/MutableLiveData.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +/** + * {@link LiveData} which publicly exposes {@link #setValue(T)} and {@link #postValue(T)} method. + * + * @param The type of data hold by this instance + */ +@SuppressWarnings("WeakerAccess") +public class MutableLiveData extends LiveData { + @Override + public void postValue(T value) { + super.postValue(value); + } + + @Override + public void setValue(T value) { + super.setValue(value); + } +} diff --git a/app/src/main/java/androidx/lifecycle/Observer.java b/app/src/main/java/androidx/lifecycle/Observer.java new file mode 100644 index 0000000000..30afeedf07 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/Observer.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +/** + * A simple callback that can receive from {@link LiveData}. + * + * @param The type of the parameter + * + * @see LiveData LiveData - for a usage description. + */ +public interface Observer { + /** + * Called when the data is changed. + * @param t The new data + */ + void onChanged(T t); +} diff --git a/app/src/main/java/androidx/lifecycle/Transformations.java b/app/src/main/java/androidx/lifecycle/Transformations.java new file mode 100644 index 0000000000..10261e9352 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/Transformations.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2017 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 androidx.lifecycle; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; + +/** + * Transformation methods for {@link LiveData}. + *

+ * These methods permit functional composition and delegation of {@link LiveData} instances. The + * transformations are calculated lazily, and will run only when the returned {@link LiveData} is + * observed. Lifecycle behavior is propagated from the input {@code source} {@link LiveData} to the + * returned one. + */ +@SuppressWarnings("WeakerAccess") +public class Transformations { + + private Transformations() { + } + + /** + * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying + * {@code mapFunction} to each value set on {@code source}. + *

+ * This method is analogous to {@link io.reactivex.Observable#map}. + *

+ * {@code transform} will be executed on the main thread. + *

+ * Here is an example mapping a simple {@code User} struct in a {@code LiveData} to a + * {@code LiveData} containing their full name as a {@code String}. + * + *

+     * LiveData userLiveData = ...;
+     * LiveData userFullNameLiveData =
+     *     Transformations.map(
+     *         userLiveData,
+     *         user -> user.firstName + user.lastName);
+     * });
+     * 
+ * + * @param source the {@code LiveData} to map from + * @param mapFunction a function to apply to each value set on {@code source} in order to set + * it + * on the output {@code LiveData} + * @param the generic type parameter of {@code source} + * @param the generic type parameter of the returned {@code LiveData} + * @return a LiveData mapped from {@code source} to type {@code } by applying + * {@code mapFunction} to each value set. + */ + @MainThread + @NonNull + public static LiveData map( + @NonNull LiveData source, + @NonNull final Function mapFunction) { + final MediatorLiveData result = new MediatorLiveData<>(); + result.addSource(source, new Observer() { + @Override + public void onChanged(@Nullable X x) { + result.setValue(mapFunction.apply(x)); + } + }); + return result; + } + + /** + * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying + * {@code switchMapFunction} to each value set on {@code source}. + *

+ * The returned {@code LiveData} delegates to the most recent {@code LiveData} created by + * calling {@code switchMapFunction} with the most recent value set to {@code source}, without + * changing the reference. In this way, {@code switchMapFunction} can change the 'backing' + * {@code LiveData} transparently to any observer registered to the {@code LiveData} returned + * by {@code switchMap()}. + *

+ * Note that when the backing {@code LiveData} is switched, no further values from the older + * {@code LiveData} will be set to the output {@code LiveData}. In this way, the method is + * analogous to {@link io.reactivex.Observable#switchMap}. + *

+ * {@code switchMapFunction} will be executed on the main thread. + *

+ * Here is an example class that holds a typed-in name of a user + * {@code String} (such as from an {@code EditText}) in a {@link MutableLiveData} and + * returns a {@code LiveData} containing a List of {@code User} objects for users that have + * that name. It populates that {@code LiveData} by requerying a repository-pattern object + * each time the typed name changes. + *

+ * This {@code ViewModel} would permit the observing UI to update "live" as the user ID text + * changes. + * + *

+     * class UserViewModel extends AndroidViewModel {
+     *     MutableLiveData nameQueryLiveData = ...
+     *
+     *     LiveData> getUsersWithNameLiveData() {
+     *         return Transformations.switchMap(
+     *             nameQueryLiveData,
+     *                 name -> myDataSource.getUsersWithNameLiveData(name));
+     *     }
+     *
+     *     void setNameQuery(String name) {
+     *         this.nameQueryLiveData.setValue(name);
+     *     }
+     * }
+     * 
+ * + * @param source the {@code LiveData} to map from + * @param switchMapFunction a function to apply to each value set on {@code source} to create a + * new delegate {@code LiveData} for the returned one + * @param the generic type parameter of {@code source} + * @param the generic type parameter of the returned {@code LiveData} + * @return a LiveData mapped from {@code source} to type {@code } by delegating + * to the LiveData returned by applying {@code switchMapFunction} to each + * value set + */ + @MainThread + @NonNull + public static LiveData switchMap( + @NonNull LiveData source, + @NonNull final Function> switchMapFunction) { + final MediatorLiveData result = new MediatorLiveData<>(); + result.addSource(source, new Observer() { + LiveData mSource; + + @Override + public void onChanged(@Nullable X x) { + LiveData newLiveData = switchMapFunction.apply(x); + if (mSource == newLiveData) { + return; + } + if (mSource != null) { + result.removeSource(mSource); + } + mSource = newLiveData; + if (mSource != null) { + result.addSource(mSource, new Observer() { + @Override + public void onChanged(@Nullable Y y) { + result.setValue(y); + } + }); + } + } + }); + return result; + } + + /** + * Creates a new {@link LiveData} object that does not emit a value until the source LiveData + * value has been changed. The value is considered changed if {@code equals()} yields + * {@code false}. + * + * @param source the input {@link LiveData} + * @param the generic type parameter of {@code source} + * @return a new {@link LiveData} of type {@code X} + */ + @MainThread + @NonNull + public static LiveData distinctUntilChanged(@NonNull LiveData source) { + final MediatorLiveData outputLiveData = new MediatorLiveData<>(); + outputLiveData.addSource(source, new Observer() { + + boolean mFirstTime = true; + + @Override + public void onChanged(X currentValue) { + final X previousValue = outputLiveData.getValue(); + if (mFirstTime + || (previousValue == null && currentValue != null) + || (previousValue != null && !previousValue.equals(currentValue))) { + mFirstTime = false; + outputLiveData.setValue(currentValue); + } + } + }); + return outputLiveData; + } +}