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 17package androidx.paging; 18 19import androidx.annotation.NonNull; 20import androidx.annotation.Nullable; 21import androidx.arch.core.executor.ArchTaskExecutor; 22import androidx.lifecycle.LiveData; 23import androidx.recyclerview.widget.AdapterListUpdateCallback; 24import androidx.recyclerview.widget.AsyncDifferConfig; 25import androidx.recyclerview.widget.DiffUtil; 26import androidx.recyclerview.widget.ListUpdateCallback; 27import androidx.recyclerview.widget.RecyclerView; 28 29import java.util.concurrent.Executor; 30 31/** 32 * Helper object for mapping a {@link PagedList} into a 33 * {@link androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}. 34 * <p> 35 * For simplicity, the {@link PagedListAdapter} wrapper class can often be used instead of the 36 * differ directly. This diff class is exposed for complex cases, and where overriding an adapter 37 * base class to support paging isn't convenient. 38 * <p> 39 * When consuming a {@link LiveData} of PagedList, you can observe updates and dispatch them 40 * directly to {@link #submitList(PagedList)}. The AsyncPagedListDiffer then can present this 41 * updating data set simply for an adapter. It listens to PagedList loading callbacks, and uses 42 * DiffUtil on a background thread to compute updates as new PagedLists are received. 43 * <p> 44 * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an 45 * adapter to acquire and present data objects. 46 * <p> 47 * A complete usage pattern with Room would look like this: 48 * <pre> 49 * {@literal @}Dao 50 * interface UserDao { 51 * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") 52 * public abstract DataSource.Factory<Integer, User> usersByLastName(); 53 * } 54 * 55 * class MyViewModel extends ViewModel { 56 * public final LiveData<PagedList<User>> usersList; 57 * public MyViewModel(UserDao userDao) { 58 * usersList = new LivePagedListBuilder<>( 59 * userDao.usersByLastName(), /* page size {@literal *}/ 20).build(); 60 * } 61 * } 62 * 63 * class MyActivity extends AppCompatActivity { 64 * {@literal @}Override 65 * public void onCreate(Bundle savedState) { 66 * super.onCreate(savedState); 67 * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class); 68 * RecyclerView recyclerView = findViewById(R.id.user_list); 69 * final UserAdapter adapter = new UserAdapter(); 70 * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList)); 71 * recyclerView.setAdapter(adapter); 72 * } 73 * } 74 * 75 * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { 76 * private final AsyncPagedListDiffer<User> mDiffer 77 * = new AsyncPagedListDiffer(this, DIFF_CALLBACK); 78 * {@literal @}Override 79 * public int getItemCount() { 80 * return mDiffer.getItemCount(); 81 * } 82 * public void submitList(PagedList<User> pagedList) { 83 * mDiffer.submitList(pagedList); 84 * } 85 * {@literal @}Override 86 * public void onBindViewHolder(UserViewHolder holder, int position) { 87 * User user = mDiffer.getItem(position); 88 * if (user != null) { 89 * holder.bindTo(user); 90 * } else { 91 * // Null defines a placeholder item - AsyncPagedListDiffer will automatically 92 * // invalidate this row when the actual object is loaded from the database 93 * holder.clear(); 94 * } 95 * } 96 * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = 97 * new DiffUtil.ItemCallback<User>() { 98 * {@literal @}Override 99 * public boolean areItemsTheSame( 100 * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { 101 * // User properties may have changed if reloaded from the DB, but ID is fixed 102 * return oldUser.getId() == newUser.getId(); 103 * } 104 * {@literal @}Override 105 * public boolean areContentsTheSame( 106 * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { 107 * // NOTE: if you use equals, your object must properly override Object#equals() 108 * // Incorrectly returning false here will result in too many animations. 109 * return oldUser.equals(newUser); 110 * } 111 * } 112 * }</pre> 113 * 114 * @param <T> Type of the PagedLists this differ will receive. 115 */ 116public class AsyncPagedListDiffer<T> { 117 // updateCallback notifications must only be notified *after* new data and item count are stored 118 // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data 119 private final ListUpdateCallback mUpdateCallback; 120 private final AsyncDifferConfig<T> mConfig; 121 122 @SuppressWarnings("RestrictedApi") 123 Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor(); 124 125 // TODO: REAL API 126 interface PagedListListener<T> { 127 void onCurrentListChanged(@Nullable PagedList<T> currentList); 128 } 129 130 @Nullable 131 PagedListListener<T> mListener; 132 133 private boolean mIsContiguous; 134 135 private PagedList<T> mPagedList; 136 private PagedList<T> mSnapshot; 137 138 // Max generation of currently scheduled runnable 139 private int mMaxScheduledGeneration; 140 141 /** 142 * Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter), 143 * new AsyncDifferConfig.Builder<T>(diffCallback).build();} 144 * 145 * @param adapter Adapter that will receive update signals. 146 * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to 147 * compare items in the list. 148 */ 149 @SuppressWarnings("WeakerAccess") 150 public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter, 151 @NonNull DiffUtil.ItemCallback<T> diffCallback) { 152 mUpdateCallback = new AdapterListUpdateCallback(adapter); 153 mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build(); 154 } 155 156 @SuppressWarnings("WeakerAccess") 157 public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback, 158 @NonNull AsyncDifferConfig<T> config) { 159 mUpdateCallback = listUpdateCallback; 160 mConfig = config; 161 } 162 163 private PagedList.Callback mPagedListCallback = new PagedList.Callback() { 164 @Override 165 public void onInserted(int position, int count) { 166 mUpdateCallback.onInserted(position, count); 167 } 168 169 @Override 170 public void onRemoved(int position, int count) { 171 mUpdateCallback.onRemoved(position, count); 172 } 173 174 @Override 175 public void onChanged(int position, int count) { 176 // NOTE: pass a null payload to convey null -> item 177 mUpdateCallback.onChanged(position, count, null); 178 } 179 }; 180 181 /** 182 * Get the item from the current PagedList at the specified index. 183 * <p> 184 * Note that this operates on both loaded items and null padding within the PagedList. 185 * 186 * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}. 187 * @return The item, or null, if a null placeholder is at the specified position. 188 */ 189 @SuppressWarnings("WeakerAccess") 190 @Nullable 191 public T getItem(int index) { 192 if (mPagedList == null) { 193 if (mSnapshot == null) { 194 throw new IndexOutOfBoundsException( 195 "Item count is zero, getItem() call is invalid"); 196 } else { 197 return mSnapshot.get(index); 198 } 199 } 200 201 mPagedList.loadAround(index); 202 return mPagedList.get(index); 203 } 204 205 /** 206 * Get the number of items currently presented by this Differ. This value can be directly 207 * returned to {@link RecyclerView.Adapter#getItemCount()}. 208 * 209 * @return Number of items being presented. 210 */ 211 @SuppressWarnings("WeakerAccess") 212 public int getItemCount() { 213 if (mPagedList != null) { 214 return mPagedList.size(); 215 } 216 217 return mSnapshot == null ? 0 : mSnapshot.size(); 218 } 219 220 /** 221 * Pass a new PagedList to the differ. 222 * <p> 223 * If a PagedList is already present, a diff will be computed asynchronously on a background 224 * thread. When the diff is computed, it will be applied (dispatched to the 225 * {@link ListUpdateCallback}), and the new PagedList will be swapped in as the 226 * {@link #getCurrentList() current list}. 227 * 228 * @param pagedList The new PagedList. 229 */ 230 @SuppressWarnings("ReferenceEquality") 231 public void submitList(final PagedList<T> pagedList) { 232 if (pagedList != null) { 233 if (mPagedList == null && mSnapshot == null) { 234 mIsContiguous = pagedList.isContiguous(); 235 } else { 236 if (pagedList.isContiguous() != mIsContiguous) { 237 throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both" 238 + " contiguous and non-contiguous lists."); 239 } 240 } 241 } 242 243 if (pagedList == mPagedList) { 244 // nothing to do 245 return; 246 } 247 248 // incrementing generation means any currently-running diffs are discarded when they finish 249 final int runGeneration = ++mMaxScheduledGeneration; 250 251 if (pagedList == null) { 252 int removedCount = getItemCount(); 253 if (mPagedList != null) { 254 mPagedList.removeWeakCallback(mPagedListCallback); 255 mPagedList = null; 256 } else if (mSnapshot != null) { 257 mSnapshot = null; 258 } 259 // dispatch update callback after updating mPagedList/mSnapshot 260 mUpdateCallback.onRemoved(0, removedCount); 261 if (mListener != null) { 262 mListener.onCurrentListChanged(null); 263 } 264 return; 265 } 266 267 if (mPagedList == null && mSnapshot == null) { 268 // fast simple first insert 269 mPagedList = pagedList; 270 pagedList.addWeakCallback(null, mPagedListCallback); 271 272 // dispatch update callback after updating mPagedList/mSnapshot 273 mUpdateCallback.onInserted(0, pagedList.size()); 274 275 if (mListener != null) { 276 mListener.onCurrentListChanged(pagedList); 277 } 278 return; 279 } 280 281 if (mPagedList != null) { 282 // first update scheduled on this list, so capture mPages as a snapshot, removing 283 // callbacks so we don't have resolve updates against a moving target 284 mPagedList.removeWeakCallback(mPagedListCallback); 285 mSnapshot = (PagedList<T>) mPagedList.snapshot(); 286 mPagedList = null; 287 } 288 289 if (mSnapshot == null || mPagedList != null) { 290 throw new IllegalStateException("must be in snapshot state to diff"); 291 } 292 293 final PagedList<T> oldSnapshot = mSnapshot; 294 final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot(); 295 mConfig.getBackgroundThreadExecutor().execute(new Runnable() { 296 @Override 297 public void run() { 298 final DiffUtil.DiffResult result; 299 result = PagedStorageDiffHelper.computeDiff( 300 oldSnapshot.mStorage, 301 newSnapshot.mStorage, 302 mConfig.getDiffCallback()); 303 304 mMainThreadExecutor.execute(new Runnable() { 305 @Override 306 public void run() { 307 if (mMaxScheduledGeneration == runGeneration) { 308 latchPagedList(pagedList, newSnapshot, result); 309 } 310 } 311 }); 312 } 313 }); 314 } 315 316 private void latchPagedList( 317 PagedList<T> newList, PagedList<T> diffSnapshot, 318 DiffUtil.DiffResult diffResult) { 319 if (mSnapshot == null || mPagedList != null) { 320 throw new IllegalStateException("must be in snapshot state to apply diff"); 321 } 322 323 PagedList<T> previousSnapshot = mSnapshot; 324 mPagedList = newList; 325 mSnapshot = null; 326 327 // dispatch update callback after updating mPagedList/mSnapshot 328 PagedStorageDiffHelper.dispatchDiff(mUpdateCallback, 329 previousSnapshot.mStorage, newList.mStorage, diffResult); 330 331 newList.addWeakCallback(diffSnapshot, mPagedListCallback); 332 if (mListener != null) { 333 mListener.onCurrentListChanged(mPagedList); 334 } 335 } 336 337 /** 338 * Returns the PagedList currently being displayed by the differ. 339 * <p> 340 * This is not necessarily the most recent list passed to {@link #submitList(PagedList)}, 341 * because a diff is computed asynchronously between the new list and the current list before 342 * updating the currentList value. May be null if no PagedList is being presented. 343 * 344 * @return The list currently being displayed, may be null. 345 */ 346 @SuppressWarnings("WeakerAccess") 347 @Nullable 348 public PagedList<T> getCurrentList() { 349 if (mSnapshot != null) { 350 return mSnapshot; 351 } 352 return mPagedList; 353 } 354} 355