1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.lifecycle;
18 
19 import androidx.annotation.CallSuper;
20 import androidx.annotation.MainThread;
21 import androidx.annotation.NonNull;
22 import androidx.annotation.Nullable;
23 import androidx.arch.core.internal.SafeIterableMap;
24 
25 import java.util.Map;
26 
27 /**
28  * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
29  * {@code OnChanged} events from them.
30  * <p>
31  * This class correctly propagates its active/inactive states down to source {@code LiveData}
32  * objects.
33  * <p>
34  * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
35  * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
36  * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
37  * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
38  * is called for either of them, we set a new value in {@code liveDataMerger}.
39  *
40  * <pre>
41  * LiveData<Integer> liveData1 = ...;
42  * LiveData<Integer> liveData2 = ...;
43  *
44  * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
45  * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
46  * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
47  * </pre>
48  * <p>
49  * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
50  * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
51  * liveData1} and remove it as a source.
52  * <pre>
53  * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
54  *      private int count = 1;
55  *
56  *      {@literal @}Override public void onChanged(@Nullable Integer s) {
57  *          count++;
58  *          liveDataMerger.setValue(s);
59  *          if (count > 10) {
60  *              liveDataMerger.removeSource(liveData1);
61  *          }
62  *      }
63  * });
64  * </pre>
65  *
66  * @param <T> The type of data hold by this instance
67  */
68 @SuppressWarnings("WeakerAccess")
69 public class MediatorLiveData<T> extends MutableLiveData<T> {
70     private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
71 
72     /**
73      * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called
74      * when {@code source} value was changed.
75      * <p>
76      * {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active.
77      * <p> If the given LiveData is already added as a source but with a different Observer,
78      * {@link IllegalArgumentException} will be thrown.
79      *
80      * @param source    the {@code LiveData} to listen to
81      * @param onChanged The observer that will receive the events
82      * @param <S>       The type of data hold by {@code source} LiveData
83      */
84     @MainThread
addSource(@onNull LiveData<S> source, @NonNull Observer<? super S> onChanged)85     public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
86         Source<S> e = new Source<>(source, onChanged);
87         Source<?> existing = mSources.putIfAbsent(source, e);
88         if (existing != null && existing.mObserver != onChanged) {
89             throw new IllegalArgumentException(
90                     "This source was already added with the different observer");
91         }
92         if (existing != null) {
93             return;
94         }
95         if (hasActiveObservers()) {
96             e.plug();
97         }
98     }
99 
100     /**
101      * Stops to listen the given {@code LiveData}.
102      *
103      * @param toRemote {@code LiveData} to stop to listen
104      * @param <S>      the type of data hold by {@code source} LiveData
105      */
106     @MainThread
removeSource(@onNull LiveData<S> toRemote)107     public <S> void removeSource(@NonNull LiveData<S> toRemote) {
108         Source<?> source = mSources.remove(toRemote);
109         if (source != null) {
110             source.unplug();
111         }
112     }
113 
114     @CallSuper
115     @Override
onActive()116     protected void onActive() {
117         for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
118             source.getValue().plug();
119         }
120     }
121 
122     @CallSuper
123     @Override
onInactive()124     protected void onInactive() {
125         for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
126             source.getValue().unplug();
127         }
128     }
129 
130     private static class Source<V> implements Observer<V> {
131         final LiveData<V> mLiveData;
132         final Observer<? super V> mObserver;
133         int mVersion = START_VERSION;
134 
Source(LiveData<V> liveData, final Observer<? super V> observer)135         Source(LiveData<V> liveData, final Observer<? super V> observer) {
136             mLiveData = liveData;
137             mObserver = observer;
138         }
139 
plug()140         void plug() {
141             mLiveData.observeForever(this);
142         }
143 
unplug()144         void unplug() {
145             mLiveData.removeObserver(this);
146         }
147 
148         @Override
onChanged(@ullable V v)149         public void onChanged(@Nullable V v) {
150             if (mVersion != mLiveData.getVersion()) {
151                 mVersion = mLiveData.getVersion();
152                 mObserver.onChanged(v);
153             }
154         }
155     }
156 }
157