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