1 /*
2  * Copyright 2020 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.recyclerview.widget;
18 
19 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS;
20 
21 import android.util.Pair;
22 import android.view.ViewGroup;
23 
24 import androidx.recyclerview.widget.RecyclerView.Adapter;
25 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
26 
27 import org.jspecify.annotations.NonNull;
28 
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.List;
32 
33 /**
34  * An {@link Adapter} implementation that presents the contents of multiple adapters in sequence.
35  *
36  * <pre>
37  * MyAdapter adapter1 = ...;
38  * AnotherAdapter adapter2 = ...;
39  * ConcatAdapter concatenated = new ConcatAdapter(adapter1, adapter2);
40  * recyclerView.setAdapter(concatenated);
41  * </pre>
42  * <p>
43  * By default, {@link ConcatAdapter} isolates view types of nested adapters from each other such
44  * that
45  * it will change the view type before reporting it back to the {@link RecyclerView} to avoid any
46  * conflicts between the view types of added adapters. This also means each added adapter will have
47  * its own isolated pool of {@link ViewHolder}s, with no re-use in between added adapters.
48  * <p>
49  * If your {@link Adapter}s share the same view types, and can support sharing {@link ViewHolder}
50  * s between added adapters, provide an instance of {@link Config} where you set
51  * {@link Config#isolateViewTypes} to {@code false}. A common usage pattern for this is to return
52  * the {@code R.layout.<layout_name>} from the {@link Adapter#getItemViewType(int)} method.
53  * <p>
54  * When an added adapter calls one of the {@code notify} methods, {@link ConcatAdapter} properly
55  * offsets values before reporting it back to the {@link RecyclerView}.
56  * If an adapter calls {@link Adapter#notifyDataSetChanged()}, {@link ConcatAdapter} also calls
57  * {@link Adapter#notifyDataSetChanged()} as calling
58  * {@link Adapter#notifyItemRangeChanged(int, int)} will confuse the {@link RecyclerView}.
59  * You are highly encouraged to to use {@link SortedList} or {@link ListAdapter} to avoid
60  * calling {@link Adapter#notifyDataSetChanged()}.
61  * <p>
62  * Whether {@link ConcatAdapter} should support stable ids is defined in the {@link Config}
63  * object. Calling {@link Adapter#setHasStableIds(boolean)} has no effect. See documentation
64  * for {@link Config.StableIdMode} for details on how to configure {@link ConcatAdapter} to use
65  * stable ids. By default, it will not use stable ids and sub adapter stable ids will be ignored.
66  * Similar to the case above, you are highly encouraged to use {@link ListAdapter}, which will
67  * automatically calculate the changes in the data set for you so you won't need stable ids.
68  * <p>
69  * It is common to find the adapter position of a {@link ViewHolder} to handle user action on the
70  * {@link ViewHolder}. For those cases, instead of calling {@link ViewHolder#getAdapterPosition()},
71  * use {@link ViewHolder#getBindingAdapterPosition()}. If your adapters share {@link ViewHolder}s,
72  * you can use the {@link ViewHolder#getBindingAdapter()} method to find the adapter which last
73  * bound that {@link ViewHolder}.
74  */
75 @SuppressWarnings("unchecked")
76 public final class ConcatAdapter extends Adapter<ViewHolder> {
77     static final String TAG = "ConcatAdapter";
78     /**
79      * Bulk of the logic is in the controller to keep this class isolated to the public API.
80      */
81     private final ConcatAdapterController mController;
82 
83     /**
84      * Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given
85      * order.
86      *
87      * @param adapters The list of adapters to add
88      */
89     @SafeVarargs
ConcatAdapter(Adapter<? extends ViewHolder> @onNull .... adapters)90     public ConcatAdapter(Adapter<? extends ViewHolder> @NonNull ... adapters) {
91         this(Config.DEFAULT, adapters);
92     }
93 
94     /**
95      * Creates a ConcatAdapter with the given config and the given adapters in the given order.
96      *
97      * @param config   The configuration for this ConcatAdapter
98      * @param adapters The list of adapters to add
99      * @see Config.Builder
100      */
101     @SafeVarargs
ConcatAdapter( @onNull Config config, Adapter<? extends ViewHolder> @NonNull ... adapters)102     public ConcatAdapter(
103             @NonNull Config config,
104             Adapter<? extends ViewHolder> @NonNull ... adapters) {
105         this(config, Arrays.asList(adapters));
106     }
107 
108     /**
109      * Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given
110      * order.
111      *
112      * @param adapters The list of adapters to add
113      */
ConcatAdapter(@onNull List<? extends Adapter<? extends ViewHolder>> adapters)114     public ConcatAdapter(@NonNull List<? extends Adapter<? extends ViewHolder>> adapters) {
115         this(Config.DEFAULT, adapters);
116     }
117 
118     /**
119      * Creates a ConcatAdapter with the given config and the given adapters in the given order.
120      *
121      * @param config   The configuration for this ConcatAdapter
122      * @param adapters The list of adapters to add
123      * @see Config.Builder
124      */
ConcatAdapter( @onNull Config config, @NonNull List<? extends Adapter<? extends ViewHolder>> adapters)125     public ConcatAdapter(
126             @NonNull Config config,
127             @NonNull List<? extends Adapter<? extends ViewHolder>> adapters) {
128         mController = new ConcatAdapterController(this, config);
129         for (Adapter<? extends ViewHolder> adapter : adapters) {
130             addAdapter(adapter);
131         }
132         // go through super as we override it to be no-op
133         super.setHasStableIds(mController.hasStableIds());
134     }
135 
136     /**
137      * Appends the given adapter to the existing list of adapters and notifies the observers of
138      * this {@link ConcatAdapter}.
139      *
140      * @param adapter The new adapter to add
141      * @return {@code true} if the adapter is successfully added because it did not already exist,
142      * {@code false} otherwise.
143      * @see #addAdapter(int, Adapter)
144      * @see #removeAdapter(Adapter)
145      */
addAdapter(@onNull Adapter<? extends ViewHolder> adapter)146     public boolean addAdapter(@NonNull Adapter<? extends ViewHolder> adapter) {
147         return mController.addAdapter((Adapter<ViewHolder>) adapter);
148     }
149 
150     /**
151      * Adds the given adapter to the given index among other adapters that are already added.
152      *
153      * @param index   The index into which to insert the adapter. ConcatAdapter will throw an
154      *                {@link IndexOutOfBoundsException} if the index is not between 0 and current
155      *                adapter count (inclusive).
156      * @param adapter The new adapter to add to the adapters list.
157      * @return {@code true} if the adapter is successfully added because it did not already exist,
158      * {@code false} otherwise.
159      * @see #addAdapter(Adapter)
160      * @see #removeAdapter(Adapter)
161      */
addAdapter(int index, @NonNull Adapter<? extends ViewHolder> adapter)162     public boolean addAdapter(int index, @NonNull Adapter<? extends ViewHolder> adapter) {
163         return mController.addAdapter(index, (Adapter<ViewHolder>) adapter);
164     }
165 
166     /**
167      * Removes the given adapter from the adapters list if it exists
168      *
169      * @param adapter The adapter to remove
170      * @return {@code true} if the adapter was previously added to this {@code ConcatAdapter} and
171      * now removed or {@code false} if it couldn't be found.
172      */
removeAdapter(@onNull Adapter<? extends ViewHolder> adapter)173     public boolean removeAdapter(@NonNull Adapter<? extends ViewHolder> adapter) {
174         return mController.removeAdapter((Adapter<ViewHolder>) adapter);
175     }
176 
177     @Override
getItemViewType(int position)178     public int getItemViewType(int position) {
179         return mController.getItemViewType(position);
180     }
181 
182     @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)183     public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
184         return mController.onCreateViewHolder(parent, viewType);
185     }
186 
187     @Override
onBindViewHolder(@onNull ViewHolder holder, int position)188     public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
189         mController.onBindViewHolder(holder, position);
190     }
191 
192     /**
193      * Calling this method is an error and will result in an {@link UnsupportedOperationException}.
194      * You should use the {@link Config} object passed into the ConcatAdapter to configure this
195      * behavior.
196      *
197      * @param hasStableIds Whether items in data set have unique identifiers or not.
198      */
199     @Override
setHasStableIds(boolean hasStableIds)200     public void setHasStableIds(boolean hasStableIds) {
201         throw new UnsupportedOperationException(
202                 "Calling setHasStableIds is not allowed on the ConcatAdapter. "
203                         + "Use the Config object passed in the constructor to control this "
204                         + "behavior");
205     }
206 
207     /**
208      * Calling this method is an error and will result in an {@link UnsupportedOperationException}.
209      *
210      * ConcatAdapter infers this value from added {@link Adapter}s.
211      *
212      * @param strategy The saved state restoration strategy for this Adapter such that
213      *                 {@link ConcatAdapter} will allow state restoration only if all added
214      *                 adapters allow it or
215      *                 there are no adapters.
216      */
217     @Override
setStateRestorationPolicy(@onNull StateRestorationPolicy strategy)218     public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) {
219         // do nothing
220         throw new UnsupportedOperationException(
221                 "Calling setStateRestorationPolicy is not allowed on the ConcatAdapter."
222                         + " This value is inferred from added adapters");
223     }
224 
225     @Override
getItemId(int position)226     public long getItemId(int position) {
227         return mController.getItemId(position);
228     }
229 
230     /**
231      * Internal method called by the ConcatAdapterController.
232      */
internalSetStateRestorationPolicy(@onNull StateRestorationPolicy strategy)233     void internalSetStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) {
234         super.setStateRestorationPolicy(strategy);
235     }
236 
237     @Override
getItemCount()238     public int getItemCount() {
239         return mController.getTotalCount();
240     }
241 
242     @Override
onFailedToRecycleView(@onNull ViewHolder holder)243     public boolean onFailedToRecycleView(@NonNull ViewHolder holder) {
244         return mController.onFailedToRecycleView(holder);
245     }
246 
247     @Override
onViewAttachedToWindow(@onNull ViewHolder holder)248     public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
249         mController.onViewAttachedToWindow(holder);
250     }
251 
252     @Override
onViewDetachedFromWindow(@onNull ViewHolder holder)253     public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
254         mController.onViewDetachedFromWindow(holder);
255     }
256 
257     @Override
onViewRecycled(@onNull ViewHolder holder)258     public void onViewRecycled(@NonNull ViewHolder holder) {
259         mController.onViewRecycled(holder);
260     }
261 
262     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)263     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
264         mController.onAttachedToRecyclerView(recyclerView);
265     }
266 
267     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)268     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
269         mController.onDetachedFromRecyclerView(recyclerView);
270     }
271 
272     /**
273      * Returns an unmodifiable copy of the list of adapters in this {@link ConcatAdapter}.
274      * Note that this is a copy hence future changes in the ConcatAdapter are not reflected in
275      * this list.
276      *
277      * @return A copy of the list of adapters in this ConcatAdapter.
278      */
getAdapters()279     public @NonNull List<? extends Adapter<? extends ViewHolder>> getAdapters() {
280         return Collections.unmodifiableList(mController.getCopyOfAdapters());
281     }
282 
283     /**
284      * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}.
285      *
286      * If the given {@link Adapter} is not part of this {@link ConcatAdapter},
287      * {@link RecyclerView#NO_POSITION} is returned.
288      *
289      * @param adapter       The adapter which is a sub adapter of this ConcatAdapter or itself.
290      * @param viewHolder    The view holder whose local position in the given adapter will be
291      *                      returned.
292      * @param localPosition The position of the given {@link ViewHolder} in this {@link Adapter}.
293      * @return The local position of the given {@link ViewHolder} in the given {@link Adapter} or
294      * {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item or the
295      * given {@link Adapter} is not part of this ConcatAdapter.
296      */
297     @Override
findRelativeAdapterPositionIn( @onNull Adapter<? extends ViewHolder> adapter, @NonNull ViewHolder viewHolder, int localPosition)298     public int findRelativeAdapterPositionIn(
299             @NonNull Adapter<? extends ViewHolder> adapter,
300             @NonNull ViewHolder viewHolder,
301             int localPosition) {
302         return mController.getLocalAdapterPosition(adapter, viewHolder, localPosition);
303     }
304 
305 
306     /**
307      * Retrieve the adapter and local position for a given position in this {@code ConcatAdapter}.
308      *
309      * This allows for retrieving wrapped adapter information in situations where you don't have a
310      * {@link ViewHolder}, such as within a
311      * {@link androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup} in which you want to
312      * look up information from the source adapter.
313      *
314      * @param globalPosition The position in this {@code ConcatAdapter}.
315      * @return a Pair with the first element set to the wrapped {@code Adapter} containing that
316      * position and the second element set to the local position in the wrapped adapter
317      * @throws IllegalArgumentException if the specified {@code globalPosition} does not
318      * correspond to a valid element of this adapter.  That is, if {@code globalPosition} is less
319      * than 0 or greater than the total number of items in the {@code ConcatAdapter}
320      */
getWrappedAdapterAndPosition(int globalPosition)321     public @NonNull Pair<Adapter<? extends ViewHolder>, Integer> getWrappedAdapterAndPosition(int
322             globalPosition) {
323         return mController.getWrappedAdapterAndPosition(globalPosition);
324     }
325 
326     /**
327      * The configuration object for a {@link ConcatAdapter}.
328      */
329     public static final class Config {
330         /**
331          * If {@code false}, {@link ConcatAdapter} assumes all assigned adapters share a global
332          * view type pool such that they use the same view types to refer to the same
333          * {@link ViewHolder}s.
334          * <p>
335          * Setting this to {@code false} will allow nested adapters to share {@link ViewHolder}s but
336          * it also means these adapters should not have conflicting view types
337          * ({@link Adapter#getItemViewType(int)}) such that two different adapters return the same
338          * view type for different {@link ViewHolder}s.
339          *
340          * By default, it is set to {@code true} which means {@link ConcatAdapter} will isolate
341          * view types across adapters, preventing them from using the same {@link ViewHolder}s.
342          */
343         public final boolean isolateViewTypes;
344 
345         /**
346          * Defines whether the {@link ConcatAdapter} should support stable ids or not
347          * ({@link Adapter#hasStableIds()}.
348          * <p>
349          * There are 3 possible options:
350          *
351          * {@link StableIdMode#NO_STABLE_IDS}: In this mode, {@link ConcatAdapter} ignores the
352          * stable
353          * ids reported by sub adapters. This is the default mode.
354          *
355          * {@link StableIdMode#ISOLATED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return
356          * {@code true} from {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
357          * {@link Adapter}s to have stable ids. As two different adapters may return same stable ids
358          * because they are unaware of each-other, {@link ConcatAdapter} will isolate each
359          * {@link Adapter}'s id pool from each other such that it will overwrite the reported stable
360          * id before reporting back to the {@link RecyclerView}. In this mode, the value returned
361          * from {@link ViewHolder#getItemId()} might differ from the value returned from
362          * {@link Adapter#getItemId(int)}.
363          *
364          * {@link StableIdMode#SHARED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return
365          * {@code true} from {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
366          * {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS},
367          * {@link ConcatAdapter} will not override the returned item ids. In this mode,
368          * child {@link Adapter}s must be aware of each-other and never return the same id unless
369          * an item is moved between {@link Adapter}s.
370          *
371          * Default value is {@link StableIdMode#NO_STABLE_IDS}.
372          */
373         public final @NonNull StableIdMode stableIdMode;
374 
375 
376         /**
377          * Default configuration for {@link ConcatAdapter} where {@link Config#isolateViewTypes}
378          * is set to {@code true} and {@link Config#stableIdMode} is set to
379          * {@link StableIdMode#NO_STABLE_IDS}.
380          */
381         public static final @NonNull Config DEFAULT = new Config(true, NO_STABLE_IDS);
382 
Config(boolean isolateViewTypes, @NonNull StableIdMode stableIdMode)383         Config(boolean isolateViewTypes, @NonNull StableIdMode stableIdMode) {
384             this.isolateViewTypes = isolateViewTypes;
385             this.stableIdMode = stableIdMode;
386         }
387 
388         /**
389          * Defines how {@link ConcatAdapter} handle stable ids ({@link Adapter#hasStableIds()}).
390          */
391         public enum StableIdMode {
392             /**
393              * In this mode, {@link ConcatAdapter} ignores the stable
394              * ids reported by sub adapters. This is the default mode.
395              * Adding an {@link Adapter} with stable ids will result in a warning as it will be
396              * ignored.
397              */
398             NO_STABLE_IDS,
399             /**
400              * In this mode, {@link ConcatAdapter} will return {@code true} from
401              * {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
402              * {@link Adapter}s to have stable ids. As two different adapters may return
403              * same stable ids because they are unaware of each-other, {@link ConcatAdapter} will
404              * isolate each {@link Adapter}'s id pool from each other such that it will overwrite
405              * the reported stable id before reporting back to the {@link RecyclerView}. In this
406              * mode, the value returned from {@link ViewHolder#getItemId()} might differ from the
407              * value returned from {@link Adapter#getItemId(int)}.
408              *
409              * Adding an adapter without stable ids will result in an
410              * {@link IllegalArgumentException}.
411              */
412             ISOLATED_STABLE_IDS,
413             /**
414              * In this mode, {@link ConcatAdapter} will return {@code true} from
415              * {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
416              * {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS},
417              * {@link ConcatAdapter} will not override the returned item ids. In this mode,
418              * child {@link Adapter}s must be aware of each-other and never return the same id
419              * unless and item is moved between {@link Adapter}s.
420              * Adding an adapter without stable ids will result in an
421              * {@link IllegalArgumentException}.
422              */
423             SHARED_STABLE_IDS
424         }
425 
426         /**
427          * The builder for {@link Config} class.
428          */
429         public static final class Builder {
430             private boolean mIsolateViewTypes = DEFAULT.isolateViewTypes;
431             private StableIdMode mStableIdMode = DEFAULT.stableIdMode;
432 
433             /**
434              * Sets whether {@link ConcatAdapter} should isolate view types of nested adapters from
435              * each other.
436              *
437              * @param isolateViewTypes {@code true} if {@link ConcatAdapter} should override view
438              *                         types of nested adapters to avoid view type
439              *                         conflicts, {@code false} otherwise.
440              *                         Defaults to {@link Config#DEFAULT}'s
441              *                         {@link Config#isolateViewTypes} value ({@code true}).
442              * @return this
443              * @see Config#isolateViewTypes
444              */
setIsolateViewTypes(boolean isolateViewTypes)445             public @NonNull Builder setIsolateViewTypes(boolean isolateViewTypes) {
446                 mIsolateViewTypes = isolateViewTypes;
447                 return this;
448             }
449 
450             /**
451              * Sets how the {@link ConcatAdapter} should handle stable ids
452              * ({@link Adapter#hasStableIds()}). See documentation in {@link Config#stableIdMode}
453              * for details.
454              *
455              * @param stableIdMode The stable id mode for the {@link ConcatAdapter}. Defaults to
456              *                     {@link Config#DEFAULT}'s {@link Config#stableIdMode} value
457              *                     ({@link StableIdMode#NO_STABLE_IDS}).
458              * @return this
459              * @see Config#stableIdMode
460              */
setStableIdMode(@onNull StableIdMode stableIdMode)461             public @NonNull Builder setStableIdMode(@NonNull StableIdMode stableIdMode) {
462                 mStableIdMode = stableIdMode;
463                 return this;
464             }
465 
466             /**
467              * @return A new instance of {@link Config} with the given parameters.
468              */
build()469             public @NonNull Config build() {
470                 return new Config(mIsolateViewTypes, mStableIdMode);
471             }
472         }
473     }
474 }
475