summaryrefslogtreecommitdiff
path: root/android/arch/paging/KeyedDataSource.java
blob: 0d4529460ba3422d104c259afbcb82987f0fb80b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.arch.paging;

import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.WorkerThread;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Incremental data loader for paging keyed content, where loaded content uses previously loaded
 * items as input to future loads.
 * <p>
 * Implement a DataSource using KeyedDataSource if you need to use data from item <code>N-1</code>
 * to load item <code>N</code>. This is common, for example, in sorted database queries where
 * attributes of the item such just before the next query define how to execute it.
 * <p>
 * A compute usage pattern with Room SQL queries would look like this (though note, Room plans to
 * provide generation of much of this code in the future):
 * <pre>
 * {@literal @}Dao
 * interface UserDao {
 *     {@literal @}Query("SELECT * from user ORDER BY name DESC LIMIT :limit")
 *     public abstract List&lt;User> userNameInitial(int limit);
 *
 *     {@literal @}Query("SELECT * from user WHERE name &lt; :key ORDER BY name DESC LIMIT :limit")
 *     public abstract List&lt;User> userNameLoadAfter(String key, int limit);
 *
 *     {@literal @}Query("SELECT * from user WHERE name > :key ORDER BY name ASC LIMIT :limit")
 *     public abstract List&lt;User> userNameLoadBefore(String key, int limit);
 * }
 *
 * public class KeyedUserQueryDataSource extends KeyedDataSource&lt;String, User> {
 *     private MyDatabase mDb;
 *     private final UserDao mUserDao;
 *     {@literal @}SuppressWarnings("FieldCanBeLocal")
 *     private final InvalidationTracker.Observer mObserver;
 *
 *     public KeyedUserQueryDataSource(MyDatabase db) {
 *         mDb = db;
 *         mUserDao = db.getUserDao();
 *         mObserver = new InvalidationTracker.Observer("user") {
 *             {@literal @}Override
 *             public void onInvalidated({@literal @}NonNull Set&lt;String> tables) {
 *                 // the user table has been invalidated, invalidate the DataSource
 *                 invalidate();
 *             }
 *         };
 *         db.getInvalidationTracker().addWeakObserver(mObserver);
 *     }
 *
 *     {@literal @}Override
 *     public boolean isInvalid() {
 *         mDb.getInvalidationTracker().refreshVersionsSync();
 *         return super.isInvalid();
 *     }
 *
 *     {@literal @}Override
 *     public String getKey({@literal @}NonNull User item) {
 *         return item.getName();
 *     }
 *
 *     {@literal @}Override
 *     public List&lt;User> loadInitial(int pageSize) {
 *         return mUserDao.userNameInitial(pageSize);
 *     }
 *
 *     {@literal @}Override
 *     public List&lt;User> loadBefore({@literal @}NonNull String userName, int pageSize) {
 *         // Return items adjacent to 'userName' in reverse order
 *         // it's valid to return a different-sized list of items than pageSize, if it's easier
 *         return mUserDao.userNameLoadBefore(userName, pageSize);
 *     }
 *
 *     {@literal @}Override
 *     public List&lt;User> loadAfter({@literal @}Nullable String userName, int pageSize) {
 *         // Return items adjacent to 'userName'
 *         // it's valid to return a different-sized list of items than pageSize, if it's easier
 *         return mUserDao.userNameLoadAfter(userName, pageSize);
 *     }
 * }</pre>
 *
 * @param <Key> Type of data used to query Value types out of the DataSource.
 * @param <Value> Type of items being loaded by the DataSource.
 */
public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {

    @Nullable
    @Override
    List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) {
        return loadAfter(getKey(currentEndItem), pageSize);
    }

    @Nullable
    @Override
    List<Value> loadBeforeImpl(
            int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize) {
        List<Value> list = loadBefore(getKey(currentBeginItem), pageSize);

        if (list != null && list.size() > 1) {
            // TODO: move out of keyed entirely, into the DB DataSource.
            list = new ArrayList<>(list);
            Collections.reverse(list);
        }
        return list;
    }

    @Nullable
    private NullPaddedList<Value> loadInitialInternal(
            @Nullable Key key, int initialLoadSize, boolean enablePlaceholders) {
        List<Value> list;
        if (key == null) {
            // no key, so load initial.
            list = loadInitial(initialLoadSize);
            if (list == null) {
                return null;
            }
        } else {
            List<Value> after = loadAfter(key, initialLoadSize / 2);
            if (after == null) {
                return null;
            }

            Key loadBeforeKey = after.isEmpty() ? key : getKey(after.get(0));
            List<Value> before = loadBefore(loadBeforeKey, initialLoadSize / 2);
            if (before == null) {
                return null;
            }
            if (!after.isEmpty() || !before.isEmpty()) {
                // one of the lists has data
                if (after.isEmpty()) {
                    // retry loading after, since it may be that the key passed points to the end of
                    // the list, so we need to load after the last item in the before list
                    after = loadAfter(getKey(before.get(0)), initialLoadSize / 2);
                    if (after == null) {
                        return null;
                    }
                }
                // assemble full list
                list = new ArrayList<>();
                list.addAll(before);
                // Note - we reverse the list instead of before, in case before is immutable
                Collections.reverse(list);
                list.addAll(after);
            } else {
                // load before(key) and load after(key) failed - try load initial to be *sure* we
                // catch the case where there's only one item, which is loaded by the key case
                list = loadInitial(initialLoadSize);
                if (list == null) {
                    return null;
                }
            }
        }

        if (list.isEmpty()) {
            // wasn't able to load any items, so publish an unpadded empty list.
            return new NullPaddedList<>(0, Collections.<Value>emptyList());
        }

        int itemsBefore = COUNT_UNDEFINED;
        int itemsAfter = COUNT_UNDEFINED;
        if (enablePlaceholders) {
            itemsBefore = countItemsBefore(getKey(list.get(0)));
            itemsAfter = countItemsAfter(getKey(list.get(list.size() - 1)));
            if (isInvalid()) {
                return null;
            }
        }
        if (itemsBefore == COUNT_UNDEFINED || itemsAfter == COUNT_UNDEFINED) {
            return new NullPaddedList<>(0, list, 0);
        } else {
            return new NullPaddedList<>(itemsBefore, list, itemsAfter);
        }
    }

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @WorkerThread
    @Override
    public NullPaddedList<Value> loadInitial(
            @Nullable Key key, int initialLoadSize, boolean enablePlaceholders) {
        if (isInvalid()) {
            return null;
        }
        NullPaddedList<Value> list = loadInitialInternal(key, initialLoadSize, enablePlaceholders);
        if (list == null || isInvalid()) {
            return null;
        }
        return list;
    }

    /**
     * Return a key associated with the given item.
     * <p>
     * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique
     * integer ID, you would return {@code item.getID()} here. This key can then be passed to
     * {@link #loadBefore(Key, int)} or {@link #loadAfter(Key, int)} to load additional items
     * adjacent to the item passed to this function.
     * <p>
     * If your key is more complex, such as when you're sorting by name, then resolving collisions
     * with integer ID, you'll need to return both. In such a case you would use a wrapper class,
     * such as {@code Pair<String, Integer>} or, in Kotlin,
     * {@code data class Key(val name: String, val id: Int)}
     *
     * @param item Item to get the key from.
     * @return Key associated with given item.
     */
    @NonNull
    @AnyThread
    public abstract Key getKey(@NonNull Value item);

    /**
     * Return the number of items that occur before the item uniquely identified by {@code key} in
     * the data set.
     * <p>
     * For example, if you're loading items sorted by ID, then this would return the total number of
     * items with ID less than {@code key}.
     * <p>
     * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsAfter(Key)}, your
     * data source will not present placeholder null items in place of unloaded data.
     *
     * @param key A unique identifier of an item in the data set.
     * @return Number of items in the data set before the item identified by {@code key}, or
     *         {@link #COUNT_UNDEFINED}.
     *
     * @see #countItemsAfter(Key)
     */
    @WorkerThread
    public int countItemsBefore(@NonNull Key key) {
        return COUNT_UNDEFINED;
    }

    /**
     * Return the number of items that occur after the item uniquely identified by {@code key} in
     * the data set.
     * <p>
     * For example, if you're loading items sorted by ID, then this would return the total number of
     * items with ID greater than {@code key}.
     * <p>
     * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsBefore(Key)}, your
     * data source will not present placeholder null items in place of unloaded data.
     *
     * @param key A unique identifier of an item in the data set.
     * @return Number of items in the data set after the item identified by {@code key}, or
     *         {@link #COUNT_UNDEFINED}.
     *
     * @see #countItemsBefore(Key)
     */
    @WorkerThread
    public int countItemsAfter(@NonNull Key key) {
        return COUNT_UNDEFINED;
    }

    @WorkerThread
    @Nullable
    public abstract List<Value> loadInitial(int pageSize);

    /**
     * Load list data after the specified item.
     * <p>
     * It's valid to return a different list size than the page size, if it's easier for this data
     * source. It is generally safer to increase the number loaded than reduce.
     *
     * @param currentEndKey Load items after this key. May be null on initial load, to indicate load
     *                      from beginning.
     * @param pageSize      Suggested number of items to load.
     * @return List of items, starting after the specified item. Null if the data source is
     * no longer valid, and should not be queried again.
     */
    @SuppressWarnings("WeakerAccess")
    @WorkerThread
    @Nullable
    public abstract List<Value> loadAfter(@NonNull Key currentEndKey, int pageSize);

    /**
     * Load data before the currently loaded content, starting at the provided index,
     * in reverse-display order.
     * <p>
     * It's valid to return a different list size than the page size, if it's easier for this data
     * source. It is generally safer to increase the number loaded than reduce.
     * <p class="note"><strong>Note:</strong> Items returned from loadBefore <em>must</em> be in
     * reverse order from how they will be presented in the list. The first item in the return list
     * will be prepended immediately before the current beginning of the list. This is so that the
     * KeyedDataSource may return a different number of items from the requested {@code pageSize} by
     * shortening or lengthening the return list as it desires.
     * <p>
     *
     * @param currentBeginKey Load items before this key.
     * @param pageSize         Suggested number of items to load.
     * @return List of items, in descending order, starting after the specified item. Null if the
     * data source is no longer valid, and should not be queried again.
     */
    @SuppressWarnings("WeakerAccess")
    @WorkerThread
    @Nullable
    public abstract List<Value> loadBefore(@NonNull Key currentBeginKey, int pageSize);

    @Nullable
    @Override
    Key getKey(int position, Value item) {
        if (item == null) {
            return null;
        }
        return getKey(item);
    }
}