1/* 2 * Copyright 2018 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.AnyThread; 20import androidx.annotation.NonNull; 21import androidx.annotation.Nullable; 22import androidx.annotation.WorkerThread; 23import androidx.arch.core.util.Function; 24 25import java.util.ArrayList; 26import java.util.List; 27import java.util.concurrent.CopyOnWriteArrayList; 28import java.util.concurrent.Executor; 29import java.util.concurrent.atomic.AtomicBoolean; 30 31/** 32 * Base class for loading pages of snapshot data into a {@link PagedList}. 33 * <p> 34 * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as 35 * it loads more data, but the data loaded cannot be updated. If the underlying data set is 36 * modified, a new PagedList / DataSource pair must be created to represent the new data. 37 * <h4>Loading Pages</h4> 38 * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter} 39 * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView. 40 * <p> 41 * To control how and when a PagedList queries data from its DataSource, see 42 * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance. 43 * <h4>Updating Paged Data</h4> 44 * A PagedList / DataSource pair are a snapshot of the data set. A new pair of 45 * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or 46 * content update occurs. A DataSource must detect that it cannot continue loading its 47 * snapshot (for instance, when Database query notices a table being invalidated), and call 48 * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from 49 * the new state of the Database query. 50 * <p> 51 * To page in data that doesn't update, you can create a single DataSource, and pass it to a single 52 * PagedList. For example, loading from network when the network's paging API doesn't provide 53 * updates. 54 * <p> 55 * To page in data from a source that does provide updates, you can create a 56 * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the 57 * data set occurs that makes the current snapshot invalid. For example, when paging a query from 58 * the Database, and the table being queried inserts or removes items. You can also use a 59 * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content 60 * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, 61 * you can connect an explicit refresh signal to call {@link #invalidate()} on the current 62 * DataSource. 63 * <p> 64 * If you have more granular update signals, such as a network API signaling an update to a single 65 * item in the list, it's recommended to load data from network into memory. Then present that 66 * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory 67 * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the 68 * snapshot can be created. 69 * <h4>Implementing a DataSource</h4> 70 * To implement, extend one of the subclasses: {@link PageKeyedDataSource}, 71 * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}. 72 * <p> 73 * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For 74 * example a network response that returns some items, and a next/previous page links. 75 * <p> 76 * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item 77 * {@code N}. For example, if requesting the backend for the next comments in the list 78 * requires the ID or timestamp of the most recent loaded comment, or if querying the next users 79 * from a name-sorted database query requires the name and unique ID of the previous. 80 * <p> 81 * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary 82 * positions, and provide a fixed item count. PositionalDataSource supports querying pages at 83 * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that 84 * PositionalDataSource is required to respect page size for efficient tiling. If you want to 85 * override page size (e.g. when network page size constraints are only known at runtime), use one 86 * of the other DataSource classes. 87 * <p> 88 * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not 89 * return {@code null} items in lists that it loads. This is so that users of the PagedList 90 * can differentiate unloaded placeholder items from content that has been paged in. 91 * 92 * @paramInput used to trigger initial load from the DataSource. Often an Integer position. 93 * @param Value type loaded by the DataSource. 94 */ 95@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety 96public abstract class DataSource<Key, Value> { 97 /** 98 * Factory for DataSources. 99 * <p> 100 * Data-loading systems of an application or library can implement this interface to allow 101 * {@code LiveData<PagedList>}s to be created. For example, Room can provide a 102 * DataSource.Factory for a given SQL query: 103 * 104 * <pre> 105 * {@literal @}Dao 106 * interface UserDao { 107 * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") 108 * public abstract DataSource.Factory<Integer, User> usersByLastName(); 109 * } 110 * </pre> 111 * In the above sample, {@code Integer} is used because it is the {@code Key} type of 112 * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to 113 * page a large query with a PositionalDataSource. 114 * 115 * @param Key identifying items in DataSource. 116 * @param Type of items in the list loaded by the DataSources. 117 */ 118 public abstract static class Factory<Key, Value> { 119 /** 120 * Create a DataSource. 121 * <p> 122 * The DataSource should invalidate itself if the snapshot is no longer valid. If a 123 * DataSource becomes invalid, the only way to query more data is to create a new DataSource 124 * from the Factory. 125 * <p> 126 * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource 127 * when the current DataSource is invalidated, and pass the new PagedList through the 128 * {@code LiveData<PagedList>} to observers. 129 * 130 * @return the new DataSource. 131 */ 132 public abstract DataSource<Key, Value> create(); 133 134 /** 135 * Applies the given function to each value emitted by DataSources produced by this Factory. 136 * <p> 137 * Same as {@link #mapByPage(Function)}, but operates on individual items. 138 * 139 * @param function Function that runs on each loaded item, returning items of a potentially 140 * new type. 141 * @param Type of items produced by the new DataSource, from the passed function. 142 * 143 * @return A new DataSource.Factory, which transforms items using the given function. 144 * 145 * @see #mapByPage(Function) 146 * @see DataSource#map(Function) 147 * @see DataSource#mapByPage(Function) 148 */ 149 @NonNull 150 public <ToValue> DataSource.Factory<Key, ToValue> map( 151 @NonNull Function<Value, ToValue> function) { 152 return mapByPage(createListFunction(function)); 153 } 154 155 /** 156 * Applies the given function to each value emitted by DataSources produced by this Factory. 157 * <p> 158 * Same as {@link #map(Function)}, but allows for batch conversions. 159 * 160 * @param function Function that runs on each loaded page, returning items of a potentially 161 * new type. 162 * @param Type of items produced by the new DataSource, from the passed function. 163 * 164 * @return A new DataSource.Factory, which transforms items using the given function. 165 * 166 * @see #map(Function) 167 * @see DataSource#map(Function) 168 * @see DataSource#mapByPage(Function) 169 */ 170 @NonNull 171 public <ToValue> DataSource.Factory<Key, ToValue> mapByPage( 172 @NonNull final Function<List<Value>, List<ToValue>> function) { 173 return new Factory<Key, ToValue>() { 174 @Override 175 public DataSource<Key, ToValue> create() { 176 return Factory.this.create().mapByPage(function); 177 } 178 }; 179 } 180 } 181 182 @NonNull 183 static <X, Y> Function<List<X>, List<Y>> createListFunction( 184 final @NonNull Function<X, Y> innerFunc) { 185 return new Function<List<X>, List<Y>>() { 186 @Override 187 public List<Y> apply(@NonNull List<X> source) { 188 List<Y> out = new ArrayList<>(source.size()); 189 for (int i = 0; i < source.size(); i++) { 190 out.add(innerFunc.apply(source.get(i))); 191 } 192 return out; 193 } 194 }; 195 } 196 197 static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) { 198 List<B> dest = function.apply(source); 199 if (dest.size() != source.size()) { 200 throw new IllegalStateException("Invalid Function " + function 201 + " changed return size. This is not supported."); 202 } 203 return dest; 204 } 205 206 // Since we currently rely on implementation details of two implementations, 207 // prevent external subclassing, except through exposed subclasses 208 DataSource() { 209 } 210 211 /** 212 * Applies the given function to each value emitted by the DataSource. 213 * <p> 214 * Same as {@link #map(Function)}, but allows for batch conversions. 215 * 216 * @param function Function that runs on each loaded page, returning items of a potentially 217 * new type. 218 * @param Type of items produced by the new DataSource, from the passed function. 219 * 220 * @return A new DataSource, which transforms items using the given function. 221 * 222 * @see #map(Function) 223 * @see DataSource.Factory#map(Function) 224 * @see DataSource.Factory#mapByPage(Function) 225 */ 226 @NonNull 227 public abstract <ToValue> DataSource<Key, ToValue> mapByPage( 228 @NonNull Function<List<Value>, List<ToValue>> function); 229 230 /** 231 * Applies the given function to each value emitted by the DataSource. 232 * <p> 233 * Same as {@link #mapByPage(Function)}, but operates on individual items. 234 * 235 * @param function Function that runs on each loaded item, returning items of a potentially 236 * new type. 237 * @param Type of items produced by the new DataSource, from the passed function. 238 * 239 * @return A new DataSource, which transforms items using the given function. 240 * 241 * @see #mapByPage(Function) 242 * @see DataSource.Factory#map(Function) 243 * @see DataSource.Factory#mapByPage(Function) 244 */ 245 @NonNull 246 public abstract <ToValue> DataSource<Key, ToValue> map( 247 @NonNull Function<Value, ToValue> function); 248 249 /** 250 * Returns true if the data source guaranteed to produce a contiguous set of items, 251 * never producing gaps. 252 */ 253 abstract boolean isContiguous(); 254 255 static class LoadCallbackHelper<T> { 256 static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) { 257 if (position < 0) { 258 throw new IllegalArgumentException("Position must be non-negative"); 259 } 260 if (data.size() + position > totalCount) { 261 throw new IllegalArgumentException( 262 "List size + position too large, last item in list beyond totalCount."); 263 } 264 if (data.size() == 0 && totalCount > 0) { 265 throw new IllegalArgumentException( 266 "Initial result cannot be empty if items are present in data set."); 267 } 268 } 269 270 @PageResult.ResultType 271 final int mResultType; 272 private final DataSource mDataSource; 273 private final PageResult.Receiver<T> mReceiver; 274 275 // mSignalLock protects mPostExecutor, and mHasSignalled 276 private final Object mSignalLock = new Object(); 277 private Executor mPostExecutor = null; 278 private boolean mHasSignalled = false; 279 280 LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType, 281 @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) { 282 mDataSource = dataSource; 283 mResultType = resultType; 284 mPostExecutor = mainThreadExecutor; 285 mReceiver = receiver; 286 } 287 288 void setPostExecutor(Executor postExecutor) { 289 synchronized (mSignalLock) { 290 mPostExecutor = postExecutor; 291 } 292 } 293 294 /** 295 * Call before verifying args, or dispatching actul results 296 * 297 * @return true if DataSource was invalid, and invalid result dispatched 298 */ 299 boolean dispatchInvalidResultIfInvalid() { 300 if (mDataSource.isInvalid()) { 301 dispatchResultToReceiver(PageResult.<T>getInvalidResult()); 302 return true; 303 } 304 return false; 305 } 306 307 void dispatchResultToReceiver(final @NonNull PageResult<T> result) { 308 Executor executor; 309 synchronized (mSignalLock) { 310 if (mHasSignalled) { 311 throw new IllegalStateException( 312 "callback.onResult already called, cannot call again."); 313 } 314 mHasSignalled = true; 315 executor = mPostExecutor; 316 } 317 318 if (executor != null) { 319 executor.execute(new Runnable() { 320 @Override 321 public void run() { 322 mReceiver.onPageResult(mResultType, result); 323 } 324 }); 325 } else { 326 mReceiver.onPageResult(mResultType, result); 327 } 328 } 329 } 330 331 /** 332 * Invalidation callback for DataSource. 333 * <p> 334 * Used to signal when a DataSource a data source has become invalid, and that a new data source 335 * is needed to continue loading data. 336 */ 337 public interface InvalidatedCallback { 338 /** 339 * Called when the data backing the list has become invalid. This callback is typically used 340 * to signal that a new data source is needed. 341 * <p> 342 * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid 343 * for the data source to invalidate itself during its load methods, or for an outside 344 * source to invalidate it. 345 */ 346 @AnyThread 347 void onInvalidated(); 348 } 349 350 private AtomicBoolean mInvalid = new AtomicBoolean(false); 351 352 private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks = 353 new CopyOnWriteArrayList<>(); 354 355 /** 356 * Add a callback to invoke when the DataSource is first invalidated. 357 * <p> 358 * Once invalidated, a data source will not become valid again. 359 * <p> 360 * A data source will only invoke its callbacks once - the first time {@link #invalidate()} 361 * is called, on that thread. 362 * 363 * @param onInvalidatedCallback The callback, will be invoked on thread that 364 * {@link #invalidate()} is called on. 365 */ 366 @AnyThread 367 @SuppressWarnings("WeakerAccess") 368 public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { 369 mOnInvalidatedCallbacks.add(onInvalidatedCallback); 370 } 371 372 /** 373 * Remove a previously added invalidate callback. 374 * 375 * @param onInvalidatedCallback The previously added callback. 376 */ 377 @AnyThread 378 @SuppressWarnings("WeakerAccess") 379 public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { 380 mOnInvalidatedCallbacks.remove(onInvalidatedCallback); 381 } 382 383 /** 384 * Signal the data source to stop loading, and notify its callback. 385 * <p> 386 * If invalidate has already been called, this method does nothing. 387 */ 388 @AnyThread 389 public void invalidate() { 390 if (mInvalid.compareAndSet(false, true)) { 391 for (InvalidatedCallback callback : mOnInvalidatedCallbacks) { 392 callback.onInvalidated(); 393 } 394 } 395 } 396 397 /** 398 * Returns true if the data source is invalid, and can no longer be queried for data. 399 * 400 * @return True if the data source is invalid, and can no longer return data. 401 */ 402 @WorkerThread 403 public boolean isInvalid() { 404 return mInvalid.get(); 405 } 406} 407