Add PagedList.BoundaryCallback for network use case

PagedList.BoundaryCallback
- allows network code to listen for loading-relevant events, like the user has
        scrolled near the beginning or end of PagedList data.
- allows network-only usecase to be built on top of Memory-based PagedList DataSource

Deprecates LivePagedListProvider in favor of new LivePagedListBuilder and DataSource.Factory:
- Splits concerns of DataSource construction (and providing access to data) from
        creating LiveData<PagedList>
- Allows for growth of construction parameters (including new PagedList.BoundaryCallback)
- Simplifies role of library (like Room) providing data - just implement DataSource.Factory

Bug: 68316389
Test: tests in paging-common, paging-runtime, room-integration-tests-testapp

Change-Id: Idb90d8462b286bbd794c61aa7b148cd813715cfb
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
index 2a5cd42..cdff391 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
@@ -47,7 +47,9 @@
             });
         }
 
-        @MainThread
+        // Creation thread for initial synchronous load, otherwise main thread
+        // Safe to access main thread only state - no other thread has reference during construction
+        @AnyThread
         @Override
         public void onPageResult(@NonNull PageResult<K, V> pageResult) {
             if (pageResult.page == null) {
@@ -70,6 +72,17 @@
             } else if (pageResult.type == PageResult.PREPEND) {
                 mKeyedStorage.prependPage(page, ContiguousPagedList.this);
             }
+
+            if (mBoundaryCallback != null) {
+                boolean deferEmpty = mStorage.size() == 0;
+                boolean deferBegin = !deferEmpty
+                        && pageResult.type == PageResult.PREPEND
+                        && pageResult.page.items.size() == 0;
+                boolean deferEnd = !deferEmpty
+                        && pageResult.type == PageResult.APPEND
+                        && pageResult.page.items.size() == 0;
+                deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
+            }
         }
     };
 
@@ -77,9 +90,11 @@
             @NonNull ContiguousDataSource<K, V> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
+            @Nullable BoundaryCallback<V> boundaryCallback,
             @NonNull Config config,
             final @Nullable K key) {
-        super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor, config);
+        super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor,
+                boundaryCallback, config);
         mDataSource = dataSource;
 
         // blocking init just triggers the initial load on the construction thread -
@@ -168,7 +183,7 @@
         final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
 
         // safe to access first item here - mStorage can't be empty if we're prepending
-        final V item = mStorage.getFirstContiguousItem();
+        final V item = mStorage.getFirstLoadedItem();
         mBackgroundThreadExecutor.execute(new Runnable() {
             @Override
             public void run() {
@@ -191,7 +206,7 @@
                 + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
 
         // safe to access first item here - mStorage can't be empty if we're appending
-        final V item = mStorage.getLastContiguousItem();
+        final V item = mStorage.getLastLoadedItem();
         mBackgroundThreadExecutor.execute(new Runnable() {
             @Override
             public void run() {
@@ -234,6 +249,8 @@
         // finally dispatch callbacks, after prepend may have already been scheduled
         notifyChanged(leadingNulls, changedCount);
         notifyInserted(0, addedCount);
+
+        offsetBoundaryAccessIndices(addedCount);
     }
 
     @MainThread
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
index 524e570..ff44521 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -48,6 +48,10 @@
 @SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
 public abstract class DataSource<Key, Value> {
 
+    public interface Factory<Key, Value> {
+        DataSource<Key, Value> create();
+    }
+
     // Since we currently rely on implementation details of two implementations,
     // prevent external subclassing, except through exposed subclasses
     DataSource() {
diff --git a/paging/common/src/main/java/android/arch/paging/ListDataSource.java b/paging/common/src/main/java/android/arch/paging/ListDataSource.java
new file mode 100644
index 0000000..d3a171e
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/ListDataSource.java
@@ -0,0 +1,39 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+public class ListDataSource<T> extends TiledDataSource<T> {
+    private final List<T> mList;
+
+    public ListDataSource(List<T> list) {
+        mList = new ArrayList<>(list);
+    }
+
+    @Override
+    public int countItems() {
+        return mList.size();
+    }
+
+    @Override
+    public List<T> loadRange(int startPosition, int count) {
+        int endExclusive = Math.min(mList.size(), startPosition + count);
+        return mList.subList(startPosition, endExclusive);
+    }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
index 51f524a..f18e108 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -16,8 +16,10 @@
 
 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.lang.ref.WeakReference;
@@ -97,6 +99,8 @@
     final Executor mMainThreadExecutor;
     @NonNull
     final Executor mBackgroundThreadExecutor;
+    @Nullable
+    final BoundaryCallback<T> mBoundaryCallback;
     @NonNull
     final Config mConfig;
     @NonNull
@@ -105,6 +109,16 @@
     int mLastLoad = 0;
     T mLastItem = null;
 
+    // if set to true, mBoundaryCallback is non-null, and should
+    // be dispatched when nearby load has occurred
+    private boolean mBoundaryCallbackBeginDeferred = false;
+    private boolean mBoundaryCallbackEndDeferred = false;
+
+    // lowest and highest index accessed by loadAround. Used to
+    // decide when mBoundaryCallback should be dispatched
+    private int mLowestIndexAccessed = Integer.MAX_VALUE;
+    private int mHighestIndexAccessed = Integer.MIN_VALUE;
+
     private final AtomicBoolean mDetached = new AtomicBoolean(false);
 
     protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
@@ -112,10 +126,12 @@
     PagedList(@NonNull PagedStorage<?, T> storage,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
+            @Nullable BoundaryCallback<T> boundaryCallback,
             @NonNull Config config) {
         mStorage = storage;
         mMainThreadExecutor = mainThreadExecutor;
         mBackgroundThreadExecutor = backgroundThreadExecutor;
+        mBoundaryCallback = boundaryCallback;
         mConfig = config;
     }
 
@@ -129,6 +145,7 @@
      *                           Generally, this is the UI/main thread.
      * @param backgroundThreadExecutor Data loading will be done via this executor - should be a
      *                                 background thread.
+     * @param boundaryCallback Optional boundary callback to attach to the list.
      * @param config PagedList Config, which defines how the PagedList will load data.
      * @param <K> Key type that indicates to the DataSource what data to load.
      * @param <T> Type of items to be held and loaded by the PagedList.
@@ -139,6 +156,7 @@
     private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
+            @Nullable BoundaryCallback<T> boundaryCallback,
             @NonNull Config config,
             @Nullable K key) {
         if (dataSource.isContiguous() || !config.enablePlaceholders) {
@@ -150,12 +168,14 @@
             return new ContiguousPagedList<>(contigDataSource,
                     mainThreadExecutor,
                     backgroundThreadExecutor,
+                    boundaryCallback,
                     config,
                     key);
         } else {
             return new TiledPagedList<>((TiledDataSource<T>) dataSource,
                     mainThreadExecutor,
                     backgroundThreadExecutor,
+                    boundaryCallback,
                     config,
                     (key != null) ? (Integer) key : 0);
         }
@@ -186,6 +206,7 @@
         private DataSource<Key, Value> mDataSource;
         private Executor mMainThreadExecutor;
         private Executor mBackgroundThreadExecutor;
+        private BoundaryCallback mBoundaryCallback;
         private Config mConfig;
         private Key mInitialKey;
 
@@ -229,6 +250,14 @@
             return this;
         }
 
+        @NonNull
+        public Builder<Key, Value> setBoundaryCallback(
+                @Nullable BoundaryCallback boundaryCallback) {
+            mBoundaryCallback = boundaryCallback;
+            return this;
+        }
+
+
         /**
          * The Config defining how the PagedList should load from the DataSource.
          *
@@ -284,10 +313,12 @@
                 throw new IllegalArgumentException("Config required");
             }
 
+            //noinspection unchecked
             return PagedList.create(
                     mDataSource,
                     mMainThreadExecutor,
                     mBackgroundThreadExecutor,
+                    mBoundaryCallback,
                     mConfig,
                     mInitialKey);
         }
@@ -312,7 +343,6 @@
         return item;
     }
 
-
     /**
      * Load adjacent items to passed index.
      *
@@ -321,8 +351,122 @@
     public void loadAround(int index) {
         mLastLoad = index + getPositionOffset();
         loadAroundInternal(index);
+
+        mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
+        mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
+
+        /*
+         * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
+         * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
+         * and accesses happen near the boundaries.
+         *
+         * Note: we post here, since RecyclerView may want to add items in response, and this
+         * call occurs in PagedListAdapter bind.
+         */
+        tryDispatchBoundaryCallbacks(true);
     }
 
+    // Creation thread for initial synchronous load, otherwise main thread
+    // Safe to access main thread only state - no other thread has reference during construction
+    @AnyThread
+    void deferBoundaryCallbacks(final boolean deferEmpty,
+            final boolean deferBegin, final boolean deferEnd) {
+        if (mBoundaryCallback == null) {
+            throw new IllegalStateException("Computing boundary");
+        }
+
+        /*
+         * If lowest/highest haven't been initialized, set them to storage size,
+         * since placeholders must already be computed by this point.
+         *
+         * This is just a minor optimization so that BoundaryCallback callbacks are sent immediately
+         * if the initial load size is smaller than the prefetch window (see
+         * TiledPagedListTest#boundaryCallback_immediate())
+         */
+        if (mLowestIndexAccessed == Integer.MAX_VALUE) {
+            mLowestIndexAccessed = mStorage.size();
+        }
+        if (mHighestIndexAccessed == Integer.MIN_VALUE) {
+            mHighestIndexAccessed = 0;
+        }
+
+        if (deferEmpty || deferBegin || deferEnd) {
+            // Post to the main thread, since we may be on creation thread currently
+            mMainThreadExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    // on is dispatched immediately, since items won't be accessed
+                    //noinspection ConstantConditions
+                    if (deferEmpty) {
+                        mBoundaryCallback.onZeroItemsLoaded();
+                    }
+
+                    // for other callbacks, mark deferred, and only dispatch if loadAround
+                    // has been called near to the position
+                    if (deferBegin) {
+                        mBoundaryCallbackBeginDeferred = true;
+                    }
+                    if (deferEnd) {
+                        mBoundaryCallbackEndDeferred = true;
+                    }
+                    tryDispatchBoundaryCallbacks(false);
+                }
+            });
+        }
+    }
+
+    /**
+     * Call this when mLowest/HighestIndexAccessed are changed, or
+     * mBoundaryCallbackBegin/EndDeferred is set.
+     */
+    private void tryDispatchBoundaryCallbacks(boolean post) {
+        final boolean dispatchBegin = mBoundaryCallbackBeginDeferred
+                && mLowestIndexAccessed <= mConfig.prefetchDistance;
+        final boolean dispatchEnd = mBoundaryCallbackEndDeferred
+                && mHighestIndexAccessed >= size() - mConfig.prefetchDistance;
+
+        if (!dispatchBegin && !dispatchEnd) {
+            return;
+        }
+
+        if (dispatchBegin) {
+            mBoundaryCallbackBeginDeferred = false;
+        }
+        if (dispatchEnd) {
+            mBoundaryCallbackEndDeferred = false;
+        }
+        if (post) {
+            mMainThreadExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
+                }
+            });
+        } else {
+            dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
+        }
+    }
+
+    private void dispatchBoundaryCallbacks(boolean begin, boolean end) {
+        // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
+        if (begin) {
+            //noinspection ConstantConditions
+            mBoundaryCallback.onItemAtFrontLoaded(
+                    snapshot(), mStorage.getFirstLoadedItem(), mStorage.size());
+        }
+        if (end) {
+            //noinspection ConstantConditions
+            mBoundaryCallback.onItemAtEndLoaded(
+                    snapshot(), mStorage.getLastLoadedItem(), mStorage.size());
+        }
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    void offsetBoundaryAccessIndices(int offset) {
+        mLowestIndexAccessed += offset;
+        mHighestIndexAccessed += offset;
+    }
 
     /**
      * Returns size of the list, including any not-yet-loaded null padding.
@@ -351,6 +495,7 @@
      *
      * @return Immutable snapshot of PagedList data.
      */
+    @SuppressWarnings("WeakerAccess")
     @NonNull
     public List<T> snapshot() {
         if (isImmutable()) {
@@ -726,4 +871,15 @@
             }
         }
     }
+
+    /**
+     * WIP API for load-more-into-local-storage callbacks
+     */
+    public abstract static class BoundaryCallback<T> {
+        public abstract void onZeroItemsLoaded();
+        public abstract void onItemAtFrontLoaded(@NonNull List<T> pagedListSnapshot,
+                @NonNull T itemAtFront, int pagedListSize);
+        public abstract void onItemAtEndLoaded(@NonNull List<T> pagedListSnapshot,
+                @NonNull T itemAtEnd, int pagedListSize);
+    }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/PagedStorage.java b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
index 7f91290..b857462 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedStorage.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
@@ -230,13 +230,13 @@
 
     // ---------------- Contiguous API -------------------
 
-    V getFirstContiguousItem() {
+    V getFirstLoadedItem() {
         // safe to access first page's first item here:
         // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
         return mPages.get(0).items.get(0);
     }
 
-    V getLastContiguousItem() {
+    V getLastLoadedItem() {
         // safe to access last page's last item here:
         // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
         Page<K, V> page = mPages.get(mPages.size() - 1);
diff --git a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
index 7e965a0..6a8a748 100644
--- a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
@@ -27,6 +27,7 @@
         super(pagedList.mStorage.snapshot(),
                 pagedList.mMainThreadExecutor,
                 pagedList.mBackgroundThreadExecutor,
+                null,
                 pagedList.mConfig);
         mContiguous = pagedList.isContiguous();
         mLastKey = pagedList.getLastKey();
diff --git a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
index 61dead3..0ea9428 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
@@ -87,6 +87,8 @@
  */
 public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> {
 
+    private int mItemCount;
+
     /**
      * Number of items that this DataSource can provide in total.
      *
@@ -123,6 +125,7 @@
      */
     void loadRangeInitial(int startPosition, int count, int pageSize, int itemCount,
             PageResult.Receiver<Integer, Type> receiver) {
+        mItemCount = itemCount;
 
         if (itemCount == 0) {
             // no data to load, just immediately return empty
@@ -132,7 +135,6 @@
             return;
         }
 
-
         List<Type> list = loadRangeWrapper(startPosition, count);
 
         count = Math.min(count, itemCount - startPosition);
@@ -167,9 +169,15 @@
     void loadRange(int startPosition, int count, PageResult.Receiver<Integer, Type> receiver) {
         List<Type> list = loadRangeWrapper(startPosition, count);
 
-        Page<Integer, Type> page = list != null ? new Page<Integer, Type>(list) : null;
+        Page<Integer, Type> page = null;
+        int trailingNulls = mItemCount - startPosition;
+
+        if (list != null) {
+            page = new Page<Integer, Type>(list);
+            trailingNulls -= list.size();
+        }
         receiver.postOnPageResult(new PageResult<>(
-                PageResult.TILE, page, startPosition, 0, 0));
+                PageResult.TILE, page, startPosition, trailingNulls, 0));
     }
 
     private List<Type> loadRangeWrapper(int startPosition, int count) {
diff --git a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
index 934a0dd..76bb682 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
@@ -17,7 +17,6 @@
 package android.arch.paging;
 
 import android.support.annotation.AnyThread;
-import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
@@ -46,7 +45,9 @@
             });
         }
 
-        @MainThread
+        // Creation thread for initial synchronous load, otherwise main thread
+        // Safe to access main thread only state - no other thread has reference during construction
+        @AnyThread
         @Override
         public void onPageResult(@NonNull PageResult<Integer, T> pageResult) {
             if (pageResult.page == null) {
@@ -67,6 +68,13 @@
                 mKeyedStorage.insertPage(pageResult.leadingNulls, pageResult.page,
                         TiledPagedList.this);
             }
+
+            if (mBoundaryCallback != null) {
+                boolean deferEmpty = mStorage.size() == 0;
+                boolean deferBegin = !deferEmpty && pageResult.leadingNulls == 0;
+                boolean deferEnd = !deferEmpty && pageResult.trailingNulls == 0;
+                deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
+            }
         }
     };
 
@@ -74,15 +82,17 @@
     TiledPagedList(@NonNull TiledDataSource<T> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
+            @Nullable BoundaryCallback<T> boundaryCallback,
             @NonNull Config config,
             int position) {
-        super(new PagedStorage<Integer, T>(),
-                mainThreadExecutor, backgroundThreadExecutor, config);
+        super(new PagedStorage<Integer, T>(), mainThreadExecutor, backgroundThreadExecutor,
+                boundaryCallback, config);
         mDataSource = dataSource;
 
         final int pageSize = mConfig.pageSize;
 
         final int itemCount = mDataSource.countItems();
+
         final int firstLoadSize = Math.min(itemCount,
                 (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize);
         final int firstLoadPosition = computeFirstLoadPosition(
diff --git a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
index 1777cf2..03d553a 100644
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
@@ -22,6 +22,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
@@ -41,17 +43,18 @@
         }
     }
 
-    private inner class TestSource : PositionalDataSource<Item>() {
+    private inner class TestSource(val listData: List<Item> = ITEMS)
+            : PositionalDataSource<Item>() {
         override fun countItems(): Int {
             return if (mCounted) {
-                ITEMS.size
+                listData.size
             } else {
                 DataSource.COUNT_UNDEFINED
             }
         }
 
         private fun getClampedRange(startInc: Int, endExc: Int, reverse: Boolean): List<Item> {
-            val list = ITEMS.subList(Math.max(0, startInc), Math.min(ITEMS.size, endExc))
+            val list = listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
             if (reverse) {
                 Collections.reverse(list)
             }
@@ -140,21 +143,20 @@
         verifyRange(90, 10, TestSource().loadInitial(95, 10, true)!!)
     }
 
-
     private fun createCountedPagedList(
-            config: PagedList.Config, initialPosition: Int): ContiguousPagedList<Int, Item> {
+            initialPosition: Int,
+            pageSize: Int = 20,
+            initLoadSize: Int = 40,
+            prefetchDistance: Int = 20,
+            listData: List<Item> = ITEMS,
+            boundaryCallback: PagedList.BoundaryCallback<Item>? = null)
+            : ContiguousPagedList<Int, Item> {
         return ContiguousPagedList(
-                TestSource(), mMainThread, mBackgroundThread,
-                config,
-                initialPosition)
-    }
-
-    private fun createCountedPagedList(initialPosition: Int): ContiguousPagedList<Int, Item> {
-        return createCountedPagedList(
+                TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
                 PagedList.Config.Builder()
-                        .setInitialLoadSizeHint(40)
-                        .setPageSize(20)
-                        .setPrefetchDistance(20)
+                        .setInitialLoadSizeHint(initLoadSize)
+                        .setPageSize(pageSize)
+                        .setPrefetchDistance(prefetchDistance)
                         .build(),
                 initialPosition)
     }
@@ -240,13 +242,8 @@
 
     @Test
     fun distantPrefetch() {
-        val pagedList = createCountedPagedList(
-                PagedList.Config.Builder()
-                        .setInitialLoadSizeHint(10)
-                        .setPageSize(10)
-                        .setPrefetchDistance(30)
-                        .build(),
-                0)
+        val pagedList = createCountedPagedList(0,
+                initLoadSize = 10, pageSize = 10, prefetchDistance = 30)
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(null, callback)
         verifyRange(0, 10, pagedList)
@@ -303,7 +300,6 @@
         val snapshot = pagedList.snapshot() as PagedList<Item>
         verifyRange(40, 60, snapshot)
 
-
         pagedList.loadAround(if (mCounted) 45 else 5)
         drain()
         verifyRange(20, 80, pagedList)
@@ -315,6 +311,71 @@
         verifyNoMoreInteractions(callback)
     }
 
+    @Test
+    fun boundaryCallback_empty() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createCountedPagedList(0,
+                listData = ArrayList(), boundaryCallback = boundaryCallback)
+        assertEquals(0, pagedList.size)
+
+        // nothing yet
+        verifyNoMoreInteractions(boundaryCallback)
+
+        // onZeroItemsLoaded posted, since creation often happens on BG thread
+        drain()
+        verify(boundaryCallback).onZeroItemsLoaded()
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
+    @Test
+    fun boundaryCallback_delayed() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createCountedPagedList(90,
+                initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback)
+        verifyRange(80, 20, pagedList)
+
+
+        // nothing yet
+        verifyZeroInteractions(boundaryCallback)
+        drain()
+        verifyZeroInteractions(boundaryCallback)
+
+        // loading around last item causes onItemAtEndLoaded
+        pagedList.loadAround(if (mCounted) 99 else 19)
+        drain()
+        verifyRange(80, 20, pagedList)
+        verify(boundaryCallback).onItemAtEndLoaded(
+                any(), eq(ITEMS.last()), eq(if (mCounted) 100 else 20))
+        verifyNoMoreInteractions(boundaryCallback)
+
+
+        // prepending doesn't trigger callback...
+        pagedList.loadAround(if (mCounted) 80 else 0)
+        drain()
+        verifyRange(60, 40, pagedList)
+        verifyZeroInteractions(boundaryCallback)
+
+        // ...load rest of data, still no dispatch...
+        pagedList.loadAround(if (mCounted) 60 else 0)
+        drain()
+        pagedList.loadAround(if (mCounted) 40 else 0)
+        drain()
+        pagedList.loadAround(if (mCounted) 20 else 0)
+        drain()
+        verifyRange(0, 100, pagedList)
+        verifyZeroInteractions(boundaryCallback)
+
+        // ... finally try prepend, see 0 items, which will dispatch front callback
+        pagedList.loadAround(0)
+        drain()
+        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(100))
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
     private fun drain() {
         var executed: Boolean
         do {
diff --git a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
index 27b80ab..6524008 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
@@ -23,6 +23,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
@@ -41,37 +43,27 @@
         }
     }
 
-    private class TestTiledSource : TiledDataSource<Item>() {
-        override fun countItems(): Int {
-            return ITEMS.size
-        }
-
-        override fun loadRange(startPosition: Int, count: Int): List<Item> {
-            val endPosition = Math.min(ITEMS.size, startPosition + count)
-            return ITEMS.subList(startPosition, endPosition)
-        }
-    }
-
-    private fun verifyRange(list: List<Item>, vararg loadedPages: Int) {
+    private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int, expected: List<Item> = ITEMS) {
         val loadedPageList = loadedPages.asList()
-        assertEquals(ITEMS.size, list.size)
+        assertEquals(expected.size, list.size)
         for (i in list.indices) {
             if (loadedPageList.contains(i / PAGE_SIZE)) {
-                assertSame("Index $i", ITEMS[i], list[i])
+                assertSame("Index $i", expected[i], list[i])
             } else {
                 assertNull("Index $i", list[i])
             }
         }
     }
 
-    private fun createTiledPagedList(loadPosition: Int, initPages: Int,
-            prefetchDistance: Int = PAGE_SIZE): TiledPagedList<Item> {
-        val source = TestTiledSource()
+    private fun createTiledPagedList(loadPosition: Int, initPageCount: Int,
+            prefetchDistance: Int = PAGE_SIZE,
+            listData: List<Item> = ITEMS,
+            boundaryCallback: PagedList.BoundaryCallback<Item>? = null): TiledPagedList<Item> {
         return TiledPagedList(
-                source, mMainThread, mBackgroundThread,
+                ListDataSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
                 PagedList.Config.Builder()
                         .setPageSize(PAGE_SIZE)
-                        .setInitialLoadSizeHint(PAGE_SIZE * initPages)
+                        .setInitialLoadSizeHint(PAGE_SIZE * initPageCount)
                         .setPrefetchDistance(prefetchDistance)
                         .build(),
                 loadPosition)
@@ -94,87 +86,87 @@
 
     @Test
     fun initialLoad_onePage() {
-        val pagedList = createTiledPagedList(0, 1)
-        verifyRange(pagedList, 0, 1)
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
+        verifyLoadedPages(pagedList, 0, 1)
     }
 
     @Test
     fun initialLoad_onePageOffset() {
-        val pagedList = createTiledPagedList(10, 1)
-        verifyRange(pagedList, 0, 1)
+        val pagedList = createTiledPagedList(loadPosition = 10, initPageCount = 1)
+        verifyLoadedPages(pagedList, 0, 1)
     }
 
     @Test
     fun initialLoad_full() {
-        val pagedList = createTiledPagedList(0, 100)
-        verifyRange(pagedList, 0, 1, 2, 3, 4)
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 100)
+        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
     }
 
     @Test
     fun initialLoad_end() {
-        val pagedList = createTiledPagedList(44, 2)
-        verifyRange(pagedList, 3, 4)
+        val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
+        verifyLoadedPages(pagedList, 3, 4)
     }
 
     @Test
     fun initialLoad_multiple() {
-        val pagedList = createTiledPagedList(9, 2)
-        verifyRange(pagedList, 0, 1)
+        val pagedList = createTiledPagedList(loadPosition = 9, initPageCount = 2)
+        verifyLoadedPages(pagedList, 0, 1)
     }
 
     @Test
     fun initialLoad_offset() {
-        val pagedList = createTiledPagedList(41, 2)
-        verifyRange(pagedList, 3, 4)
+        val pagedList = createTiledPagedList(loadPosition = 41, initPageCount = 2)
+        verifyLoadedPages(pagedList, 3, 4)
     }
 
     @Test
     fun append() {
-        val pagedList = createTiledPagedList(0, 1)
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(null, callback)
-        verifyRange(pagedList, 0, 1)
+        verifyLoadedPages(pagedList, 0, 1)
         verifyZeroInteractions(callback)
 
         pagedList.loadAround(15)
 
-        verifyRange(pagedList, 0, 1)
+        verifyLoadedPages(pagedList, 0, 1)
 
         drain()
 
-        verifyRange(pagedList, 0, 1, 2)
+        verifyLoadedPages(pagedList, 0, 1, 2)
         verify(callback).onChanged(20, 10)
         verifyNoMoreInteractions(callback)
     }
 
     @Test
     fun prepend() {
-        val pagedList = createTiledPagedList(44, 2)
+        val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(null, callback)
-        verifyRange(pagedList, 3, 4)
+        verifyLoadedPages(pagedList, 3, 4)
         verifyZeroInteractions(callback)
 
         pagedList.loadAround(35)
         drain()
 
-        verifyRange(pagedList, 2, 3, 4)
+        verifyLoadedPages(pagedList, 2, 3, 4)
         verify<PagedList.Callback>(callback).onChanged(20, 10)
         verifyNoMoreInteractions(callback)
     }
 
     @Test
     fun loadWithGap() {
-        val pagedList = createTiledPagedList(0, 1)
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(null, callback)
-        verifyRange(pagedList, 0, 1)
+        verifyLoadedPages(pagedList, 0, 1)
         verifyZeroInteractions(callback)
 
         pagedList.loadAround(44)
         drain()
 
-        verifyRange(pagedList, 0, 1, 3, 4)
+        verifyLoadedPages(pagedList, 0, 1, 3, 4)
         verify(callback).onChanged(30, 10)
         verify(callback).onChanged(40, 5)
         verifyNoMoreInteractions(callback)
@@ -182,45 +174,47 @@
 
     @Test
     fun tinyPrefetchTest() {
-        val pagedList = createTiledPagedList(0, 1, 1)
+        val pagedList = createTiledPagedList(
+                loadPosition = 0, initPageCount = 1, prefetchDistance = 1)
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(null, callback)
-        verifyRange(pagedList, 0, 1)
+        verifyLoadedPages(pagedList, 0, 1)
         verifyZeroInteractions(callback)
 
         pagedList.loadAround(33)
         drain()
 
-        verifyRange(pagedList, 0, 1, 3)
+        verifyLoadedPages(pagedList, 0, 1, 3)
         verify(callback).onChanged(30, 10)
         verifyNoMoreInteractions(callback)
 
         pagedList.loadAround(44)
         drain()
 
-        verifyRange(pagedList, 0, 1, 3, 4)
+        verifyLoadedPages(pagedList, 0, 1, 3, 4)
         verify(callback).onChanged(40, 5)
         verifyNoMoreInteractions(callback)
     }
 
     @Test
     fun appendCallbackAddedLate() {
-        val pagedList = createTiledPagedList(0, 1, 0)
-        verifyRange(pagedList, 0, 1)
+        val pagedList = createTiledPagedList(
+                loadPosition = 0, initPageCount = 1, prefetchDistance = 0)
+        verifyLoadedPages(pagedList, 0, 1)
 
         pagedList.loadAround(25)
         drain()
-        verifyRange(pagedList, 0, 1, 2)
+        verifyLoadedPages(pagedList, 0, 1, 2)
 
         // snapshot at 30 items
         val snapshot = pagedList.snapshot()
-        verifyRange(snapshot, 0, 1, 2)
+        verifyLoadedPages(snapshot, 0, 1, 2)
 
         pagedList.loadAround(35)
         pagedList.loadAround(44)
         drain()
-        verifyRange(pagedList, 0, 1, 2, 3, 4)
-        verifyRange(snapshot, 0, 1, 2)
+        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+        verifyLoadedPages(snapshot, 0, 1, 2)
 
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(snapshot, callback)
@@ -230,22 +224,23 @@
 
     @Test
     fun prependCallbackAddedLate() {
-        val pagedList = createTiledPagedList(44, 2, 0)
-        verifyRange(pagedList, 3, 4)
+        val pagedList = createTiledPagedList(
+                loadPosition = 44, initPageCount = 2, prefetchDistance = 0)
+        verifyLoadedPages(pagedList, 3, 4)
 
         pagedList.loadAround(25)
         drain()
-        verifyRange(pagedList, 2, 3, 4)
+        verifyLoadedPages(pagedList, 2, 3, 4)
 
         // snapshot at 30 items
         val snapshot = pagedList.snapshot()
-        verifyRange(snapshot, 2, 3, 4)
+        verifyLoadedPages(snapshot, 2, 3, 4)
 
         pagedList.loadAround(15)
         pagedList.loadAround(5)
         drain()
-        verifyRange(pagedList, 0, 1, 2, 3, 4)
-        verifyRange(snapshot, 2, 3, 4)
+        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+        verifyLoadedPages(snapshot, 2, 3, 4)
 
         val callback = mock(PagedList.Callback::class.java)
         pagedList.addWeakCallback(snapshot, callback)
@@ -257,7 +252,7 @@
     fun placeholdersDisabled() {
         // disable placeholders with config, so we create a contiguous version of the pagedlist
         val pagedList = PagedList.Builder<Int, Item>()
-                .setDataSource(TestTiledSource())
+                .setDataSource(ListDataSource(ITEMS))
                 .setMainThreadExecutor(mMainThread)
                 .setBackgroundThreadExecutor(mBackgroundThread)
                 .setConfig(PagedList.Config.Builder()
@@ -278,6 +273,98 @@
         assertEquals(0, contiguousPagedList.mStorage.trailingNullCount)
     }
 
+    @Test
+    fun boundaryCallback_empty() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
+                listData = ArrayList(), boundaryCallback = boundaryCallback)
+        assertEquals(0, pagedList.size)
+
+        // nothing yet
+        verifyNoMoreInteractions(boundaryCallback)
+
+        // onZeroItemsLoaded posted, since creation often happens on BG thread
+        drain()
+        verify(boundaryCallback).onZeroItemsLoaded()
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
+    @Test
+    fun boundaryCallback_immediate() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
+                listData = ITEMS.subList(0, 2), boundaryCallback = boundaryCallback)
+        assertEquals(2, pagedList.size)
+
+        // nothing yet
+        verifyZeroInteractions(boundaryCallback)
+
+        // callbacks posted, since creation often happens on BG thread
+        drain()
+        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS[0]), eq(2))
+        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS[1]), eq(2))
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
+    @Test
+    fun boundaryCallback_delayedUntilLoaded() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createTiledPagedList(loadPosition = 20, initPageCount = 1,
+                boundaryCallback = boundaryCallback)
+        verifyLoadedPages(pagedList, 1, 2) // 0, 3, and 4 not loaded yet
+
+        // nothing yet, even after drain
+        verifyZeroInteractions(boundaryCallback)
+        drain()
+        verifyZeroInteractions(boundaryCallback)
+
+        pagedList.loadAround(0)
+        pagedList.loadAround(44)
+
+        // still nothing, since items aren't loaded...
+        verifyZeroInteractions(boundaryCallback)
+
+        drain()
+        // first/last items loaded now, so callbacks dispatched
+        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(45))
+        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS.last()), eq(45))
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
+    @Test
+    fun boundaryCallback_delayedUntilNearbyAccess() {
+        @Suppress("UNCHECKED_CAST")
+        val boundaryCallback =
+                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 5,
+                prefetchDistance = 2, boundaryCallback = boundaryCallback)
+        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+
+        // all items loaded, but no access near ends, so no callbacks
+        verifyZeroInteractions(boundaryCallback)
+        drain()
+        verifyZeroInteractions(boundaryCallback)
+
+        pagedList.loadAround(0)
+        pagedList.loadAround(44)
+
+        // callbacks not posted immediately
+        verifyZeroInteractions(boundaryCallback)
+
+        drain()
+
+        // items accessed, so now posted callbacks are run
+        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(45))
+        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS.last()), eq(45))
+        verifyNoMoreInteractions(boundaryCallback)
+    }
+
     private fun drain() {
         var executed: Boolean
         do {
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java
index 237cc14..974eab9 100644
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java
+++ b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java
@@ -19,7 +19,7 @@
 import android.arch.lifecycle.LiveData;
 import android.arch.lifecycle.ViewModel;
 import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
 
 /**
@@ -41,16 +41,19 @@
 
     LiveData<PagedList<Item>> getLivePagedList() {
         if (mLivePagedList == null) {
-            mLivePagedList = new LivePagedListProvider<Integer, Item>() {
-                @Override
-                protected DataSource<Integer, Item> createDataSource() {
-                    ItemDataSource newDataSource = new ItemDataSource();
-                    synchronized (mDataSourceLock) {
-                        mDataSource = newDataSource;
-                        return mDataSource;
-                    }
-                }
-            }.create(0, 20);
+            mLivePagedList = new LivePagedListBuilder<Integer, Item>()
+                    .setPagingConfig(20)
+                    .setDataSourceFactory(new DataSource.Factory<Integer, Item>() {
+                        @Override
+                        public DataSource<Integer, Item> create() {
+                            ItemDataSource newDataSource = new ItemDataSource();
+                            synchronized (mDataSourceLock) {
+                                mDataSource = newDataSource;
+                                return mDataSource;
+                            }
+                        }
+                    })
+                    .build();
         }
 
         return mLivePagedList;
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/ListDataSource.kt b/paging/runtime/src/androidTest/java/android/arch/paging/ListDataSource.kt
deleted file mode 100644
index b206d2e..0000000
--- a/paging/runtime/src/androidTest/java/android/arch/paging/ListDataSource.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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
-
-class ListDataSource<T> constructor(private val list: List<T>) : TiledDataSource<T>() {
-
-    override fun countItems(): Int {
-        return list.size
-    }
-
-    override fun loadRange(startPosition: Int, count: Int): List<T> {
-        val endExclusive = Math.min(list.size, startPosition + count)
-        return list.subList(startPosition, endExclusive)
-    }
-}
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt b/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
index 9e95316..735a61f 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
@@ -20,12 +20,12 @@
 import android.support.v7.recyclerview.extensions.DiffCallback
 import android.support.v7.recyclerview.extensions.ListAdapterConfig
 import android.support.v7.util.ListUpdateCallback
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertFalse
-import junit.framework.Assert.assertNotNull
-import junit.framework.Assert.assertNull
-import junit.framework.Assert.assertTrue
-import junit.framework.Assert.fail
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
index 86a52ab..c2e5ec7 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
@@ -17,7 +17,7 @@
 package android.arch.paging
 
 class StringPagedList constructor(leadingNulls: Int, trailingNulls: Int, vararg items: String)
-        : PagedList<String>(PagedStorage<Int, String>(), TestExecutor(), TestExecutor(),
+        : PagedList<String>(PagedStorage<Int, String>(), TestExecutor(), TestExecutor(), null,
                 PagedList.Config.Builder().setPageSize(1).build()), PagedStorage.Callback {
     init {
         @Suppress("UNCHECKED_CAST")
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
new file mode 100644
index 0000000..ee1810b
--- /dev/null
+++ b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
@@ -0,0 +1,159 @@
+/*
+ * 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.arch.core.executor.ArchTaskExecutor;
+import android.arch.lifecycle.ComputableLiveData;
+import android.arch.lifecycle.LiveData;
+import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+public class LivePagedListBuilder<Key, Value> {
+    private Key mInitialLoadKey;
+    private PagedList.Config mConfig;
+    private DataSource.Factory<Key, Value> mDataSourceFactory;
+    private PagedList.BoundaryCallback mBoundaryCallback;
+    private Executor mMainThreadExecutor;
+    private Executor mBackgroundThreadExecutor;
+
+    @SuppressWarnings("WeakerAccess")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
+        mInitialLoadKey = key;
+        return this;
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setPagingConfig(@NonNull PagedList.Config config) {
+        mConfig = config;
+        return this;
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setPagingConfig(int pageSize) {
+        mConfig = new PagedList.Config.Builder().setPageSize(pageSize).build();
+        return this;
+    }
+
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setDataSourceFactory(
+            @NonNull DataSource.Factory<Key, Value> dataSourceFactory) {
+        mDataSourceFactory = dataSourceFactory;
+        return this;
+    }
+
+    @SuppressWarnings("unused")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setBoundaryCallback(
+            @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
+        mBoundaryCallback = boundaryCallback;
+        return this;
+    }
+
+    @SuppressWarnings("unused")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setMainThreadExecutor(
+            @NonNull Executor mainThreadExecutor) {
+        mMainThreadExecutor = mainThreadExecutor;
+        return this;
+    }
+
+    @SuppressWarnings("unused")
+    @NonNull
+    public LivePagedListBuilder<Key, Value> setBackgroundThreadExecutor(
+            @NonNull Executor backgroundThreadExecutor) {
+        mBackgroundThreadExecutor = backgroundThreadExecutor;
+        return this;
+    }
+
+    @NonNull
+    public LiveData<PagedList<Value>> build() {
+        if (mConfig == null) {
+            throw new IllegalArgumentException("PagedList.Config must be provided");
+        }
+        if (mDataSourceFactory == null) {
+            throw new IllegalArgumentException("DataSource.Factory must be provided");
+        }
+        if (mMainThreadExecutor == null) {
+            mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
+        }
+        if (mBackgroundThreadExecutor == null) {
+            mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
+        }
+
+        return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
+                mMainThreadExecutor, mBackgroundThreadExecutor);
+    }
+
+    @AnyThread
+    @NonNull
+    public static <Key, Value> LiveData<PagedList<Value>> create(
+            @Nullable final Key initialLoadKey,
+            @NonNull final PagedList.Config config,
+            @Nullable final PagedList.BoundaryCallback boundaryCallback,
+            @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
+            @NonNull final Executor mainThreadExecutor,
+            @NonNull final Executor backgroundThreadExecutor) {
+        return new ComputableLiveData<PagedList<Value>>() {
+            @Nullable
+            private PagedList<Value> mList;
+            @Nullable
+            private DataSource<Key, Value> mDataSource;
+
+            private final DataSource.InvalidatedCallback mCallback =
+                    new DataSource.InvalidatedCallback() {
+                        @Override
+                        public void onInvalidated() {
+                            invalidate();
+                        }
+                    };
+
+            @Override
+            protected PagedList<Value> compute() {
+                @Nullable Key initializeKey = initialLoadKey;
+                if (mList != null) {
+                    //noinspection unchecked
+                    initializeKey = (Key) mList.getLastKey();
+                }
+
+                do {
+                    if (mDataSource != null) {
+                        mDataSource.removeInvalidatedCallback(mCallback);
+                    }
+
+                    mDataSource = dataSourceFactory.create();
+                    mDataSource.addInvalidatedCallback(mCallback);
+
+                    mList = new PagedList.Builder<Key, Value>()
+                            .setDataSource(mDataSource)
+                            .setMainThreadExecutor(mainThreadExecutor)
+                            .setBackgroundThreadExecutor(backgroundThreadExecutor)
+                            .setBoundaryCallback(boundaryCallback)
+                            .setConfig(config)
+                            .setInitialKey(initializeKey)
+                            .build();
+                } while (mList.isDetached());
+                return mList;
+            }
+        }.getLiveData();
+    }
+}
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
index 07dd84b..e0a03cb 100644
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
+++ b/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
@@ -16,8 +16,6 @@
 
 package android.arch.paging;
 
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.ComputableLiveData;
 import android.arch.lifecycle.LiveData;
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
@@ -53,8 +51,23 @@
  * @see PagedListAdapter
  * @see DataSource
  * @see PagedList
+ *
+ * @deprecated To construct a {@code LiveData<PagedList>}, use {@link LivePagedListBuilder}, which
+ * provides the same construction capability with more customization, and better defaults. The role
+ * of DataSource construction has been separated out to {@link DataSource.Factory} to access or
+ * provide a self-invalidating sequence of DataSources. If you were acquiring this from Room, you
+ * can switch to having your Dao return a {@link DataSource.Factory} instead, and create a LiveData
+ * of PagedList with a {@link LivePagedListBuilder}.
  */
-public abstract class LivePagedListProvider<Key, Value> {
+// NOTE: Room 1.0 depends on this class, so it should not be removed
+// until Room switches to using DataSource.Factory directly
+@Deprecated
+public abstract class LivePagedListProvider<Key, Value> implements DataSource.Factory<Key, Value> {
+
+    @Override
+    public DataSource<Key, Value> create() {
+        return createDataSource();
+    }
 
     /**
      * Construct a new data source to be wrapped in a new PagedList, which will be returned
@@ -80,10 +93,11 @@
     @AnyThread
     @NonNull
     public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) {
-        return create(initialLoadKey,
-                new PagedList.Config.Builder()
-                        .setPageSize(pageSize)
-                        .build());
+        return new LivePagedListBuilder<Key, Value>()
+                .setInitialLoadKey(initialLoadKey)
+                .setPagingConfig(pageSize)
+                .setDataSourceFactory(this)
+                .build();
     }
 
     /**
@@ -100,49 +114,12 @@
      */
     @AnyThread
     @NonNull
-    public LiveData<PagedList<Value>> create(@Nullable final Key initialLoadKey,
-            final PagedList.Config config) {
-        return new ComputableLiveData<PagedList<Value>>() {
-            @Nullable
-            private PagedList<Value> mList;
-            @Nullable
-            private DataSource<Key, Value> mDataSource;
-
-            private final DataSource.InvalidatedCallback mCallback =
-                    new DataSource.InvalidatedCallback() {
-                @Override
-                public void onInvalidated() {
-                    invalidate();
-                }
-            };
-
-            @Override
-            protected PagedList<Value> compute() {
-                @Nullable Key initializeKey = initialLoadKey;
-                if (mList != null) {
-                    //noinspection unchecked
-                    initializeKey = (Key) mList.getLastKey();
-                }
-
-                do {
-                    if (mDataSource != null) {
-                        mDataSource.removeInvalidatedCallback(mCallback);
-                    }
-
-                    mDataSource = createDataSource();
-                    mDataSource.addInvalidatedCallback(mCallback);
-
-                    mList = new PagedList.Builder<Key, Value>()
-                            .setDataSource(mDataSource)
-                            .setMainThreadExecutor(ArchTaskExecutor.getMainThreadExecutor())
-                            .setBackgroundThreadExecutor(
-                                    ArchTaskExecutor.getIOThreadExecutor())
-                            .setConfig(config)
-                            .setInitialKey(initializeKey)
-                            .build();
-                } while (mList.isDetached());
-                return mList;
-            }
-        }.getLiveData();
+    public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey,
+            @NonNull PagedList.Config config) {
+        return new LivePagedListBuilder<Key, Value>()
+                .setInitialLoadKey(initialLoadKey)
+                .setPagingConfig(config)
+                .setDataSourceFactory(this)
+                .build();
     }
 }
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
index 93c02ea..89b9c2e 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
@@ -113,6 +113,13 @@
 public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
         extends RecyclerView.Adapter<VH> {
     private final PagedListAdapterHelper<T> mHelper;
+    private final PagedListAdapterHelper.PagedListListener<T> mListener =
+            new PagedListAdapterHelper.PagedListListener<T>() {
+        @Override
+        public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
+            PagedListAdapter.this.onCurrentListChanged(currentList);
+        }
+    };
 
     /**
      * Creates a PagedListAdapter with default threading and
@@ -125,11 +132,13 @@
      */
     protected PagedListAdapter(@NonNull DiffCallback<T> diffCallback) {
         mHelper = new PagedListAdapterHelper<>(this, diffCallback);
+        mHelper.mListener = mListener;
     }
 
     @SuppressWarnings("unused, WeakerAccess")
     protected PagedListAdapter(@NonNull ListAdapterConfig<T> config) {
         mHelper = new PagedListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this), config);
+        mHelper.mListener = mListener;
     }
 
     /**
@@ -167,4 +176,22 @@
     public PagedList<T> getCurrentList() {
         return mHelper.getCurrentList();
     }
+
+    /**
+     * Called when the current PagedList is updated.
+     * <p>
+     * This may be dispatched as part of {@link #setList(PagedList)} if a background diff isn't
+     * needed (such as when the first list is passed, or the list is cleared). In either case,
+     * PagedListAdapter will simply call
+     * {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}.
+     * <p>
+     * This method will <em>not</em>be called when the Adapter switches from presenting a PagedList
+     * to a snapshot version of the PagedList during a diff. This means you cannot observe each
+     * PagedList via this method.
+     *
+     * @param currentList new PagedList being displayed, may be null.
+     */
+    @SuppressWarnings("WeakerAccess")
+    public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
+    }
 }
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
index abcff41..51a6e37 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
@@ -123,6 +123,14 @@
     private final ListUpdateCallback mUpdateCallback;
     private final ListAdapterConfig<T> mConfig;
 
+    // TODO: REAL API
+    interface PagedListListener<T> {
+        void onCurrentListChanged(@Nullable PagedList<T> currentList);
+    }
+
+    @Nullable
+    PagedListListener<T> mListener;
+
     private boolean mIsContiguous;
 
     private PagedList<T> mPagedList;
@@ -247,6 +255,9 @@
             }
             // dispatch update callback after updating mPagedList/mSnapshot
             mUpdateCallback.onRemoved(0, removedCount);
+            if (mListener != null) {
+                mListener.onCurrentListChanged(null);
+            }
             return;
         }
 
@@ -257,6 +268,10 @@
 
             // dispatch update callback after updating mPagedList/mSnapshot
             mUpdateCallback.onInserted(0, pagedList.size());
+
+            if (mListener != null) {
+                mListener.onCurrentListChanged(pagedList);
+            }
             return;
         }
 
@@ -311,6 +326,9 @@
                 previousSnapshot.mStorage, newList.mStorage, diffResult);
 
         newList.addWeakCallback(diffSnapshot, mPagedListCallback);
+        if (mListener != null) {
+            mListener.onCurrentListChanged(mPagedList);
+        }
     }
 
     /**
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 665a1ae..7e7a333 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -17,6 +17,7 @@
 package android.arch.persistence.room.integration.testapp.dao;
 
 import android.arch.lifecycle.LiveData;
+import android.arch.paging.DataSource;
 import android.arch.paging.LivePagedListProvider;
 import android.arch.paging.TiledDataSource;
 import android.arch.persistence.room.Dao;
@@ -184,7 +185,10 @@
     }
 
     @Query("SELECT * FROM user where mAge > :age")
-    public abstract LivePagedListProvider<Integer, User> loadPagedByAge(int age);
+    public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
+
+    @Query("SELECT * FROM user where mAge > :age")
+    public abstract LivePagedListProvider<Integer, User> loadPagedByAge_legacy(int age);
 
     @Query("SELECT * FROM user ORDER BY mAge DESC")
     public abstract TiledDataSource<User> loadUsersByAgeDesc();
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
similarity index 75%
rename from room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java
rename to room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
index df70a17..c546531 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
@@ -28,6 +28,7 @@
 import android.arch.lifecycle.LifecycleRegistry;
 import android.arch.lifecycle.LiveData;
 import android.arch.lifecycle.Observer;
+import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
 import android.arch.persistence.room.integration.testapp.test.TestDatabaseTest;
 import android.arch.persistence.room.integration.testapp.test.TestUtil;
@@ -46,15 +47,54 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+@LargeTest
 @RunWith(AndroidJUnit4.class)
-public class LivePagedListProviderTest extends TestDatabaseTest {
+public class DataSourceFactoryTest extends TestDatabaseTest {
     @Rule
     public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
 
+    private interface LivePagedListFactory {
+        LiveData<PagedList<User>> create();
+    }
+
     @Test
-    @LargeTest
     public void getUsersAsPagedList()
             throws InterruptedException, ExecutionException, TimeoutException {
+        validateUsersAsPagedList(new LivePagedListFactory() {
+            @Override
+            public LiveData<PagedList<User>> create() {
+                return new LivePagedListBuilder<Integer, User>()
+                        .setPagingConfig(new PagedList.Config.Builder()
+                                .setPageSize(10)
+                                .setPrefetchDistance(1)
+                                .setInitialLoadSizeHint(10).build())
+                        .setDataSourceFactory(mUserDao.loadPagedByAge(3))
+                        .build();
+            }
+        });
+    }
+
+
+    // TODO: delete this and factory abstraction when LivePagedListProvider is removed
+    @Test
+    public void getUsersAsPagedList_legacyLivePagedListProvider()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        validateUsersAsPagedList(new LivePagedListFactory() {
+            @Override
+            public LiveData<PagedList<User>> create() {
+                return mUserDao.loadPagedByAge_legacy(3).create(
+                        0,
+                        new PagedList.Config.Builder()
+                                .setPageSize(10)
+                                .setPrefetchDistance(1)
+                                .setInitialLoadSizeHint(10)
+                                .build());
+            }
+        });
+    }
+
+    private void validateUsersAsPagedList(LivePagedListFactory factory)
+            throws InterruptedException, ExecutionException, TimeoutException {
         mDatabase.beginTransaction();
         try {
             for (int i = 0; i < 100; i++) {
@@ -67,12 +107,8 @@
             mDatabase.endTransaction();
         }
         assertThat(mUserDao.count(), is(100));
-        final LiveData<PagedList<User>> livePagedUsers = mUserDao.loadPagedByAge(3).create(
-                0,
-                new PagedList.Config.Builder()
-                        .setPageSize(10)
-                        .setPrefetchDistance(1)
-                        .setInitialLoadSizeHint(10).build());
+
+        final LiveData<PagedList<User>> livePagedUsers = factory.create();
 
         final TestLifecycleOwner testOwner = new TestLifecycleOwner();
         testOwner.handleEvent(Lifecycle.Event.ON_CREATE);
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
index 854c862..291cfd6 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -25,7 +25,8 @@
 import android.arch.lifecycle.Lifecycle;
 import android.arch.lifecycle.LiveData;
 import android.arch.lifecycle.Observer;
-import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.DataSource;
+import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
 import android.arch.paging.TiledDataSource;
 import android.arch.persistence.room.Dao;
@@ -202,7 +203,10 @@
 
     @Test
     public void pagedList() {
-        LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
+        LiveData<PagedList<Entity1>> pagedList = new LivePagedListBuilder<Integer, Entity1>()
+                .setDataSourceFactory(mDao.pagedList())
+                .setPagingConfig(10)
+                .build();
         observeForever(pagedList);
         drain();
         assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
@@ -366,7 +370,7 @@
 
         List<Entity1WithChildren> withRelation();
 
-        LivePagedListProvider<Integer, Entity1> pagedList();
+        DataSource.Factory<Integer, Entity1> pagedList();
 
         TiledDataSource<Entity1> dataSource();
 
@@ -406,7 +410,7 @@
 
         @Override
         @Query(SELECT_ALL)
-        LivePagedListProvider<Integer, Entity1> pagedList();
+        DataSource.Factory<Integer, Entity1> pagedList();
 
         @Override
         @Query(SELECT_ALL)
@@ -448,7 +452,7 @@
         @Override
         @Transaction
         @Query(SELECT_ALL)
-        LivePagedListProvider<Integer, Entity1> pagedList();
+        DataSource.Factory<Integer, Entity1> pagedList();
 
         @Override
         @Transaction
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
index 320b2cd..89d16b7 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
@@ -21,7 +21,7 @@
 import android.arch.lifecycle.AndroidViewModel;
 import android.arch.lifecycle.LiveData;
 import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
 import android.arch.persistence.room.Room;
 import android.arch.persistence.room.integration.testapp.database.Customer;
@@ -81,30 +81,30 @@
         });
     }
 
+    private static <K> LiveData<PagedList<Customer>> getLivePagedList(
+            K initialLoadKey, DataSource.Factory<K, Customer> dataSourceFactory) {
+        return new LivePagedListBuilder<K, Customer>()
+                .setInitialLoadKey(initialLoadKey)
+                .setPagingConfig(new PagedList.Config.Builder()
+                        .setPageSize(10)
+                        .setEnablePlaceholders(false)
+                        .build())
+                .setDataSourceFactory(dataSourceFactory)
+                .build();
+    }
+
     LiveData<PagedList<Customer>> getLivePagedList(int position) {
         if (mLiveCustomerList == null) {
-            mLiveCustomerList = mDatabase.getCustomerDao()
-                    .loadPagedAgeOrder().create(position,
-                            new PagedList.Config.Builder()
-                                    .setPageSize(10)
-                                    .setEnablePlaceholders(false)
-                                    .build());
+            mLiveCustomerList =
+                    getLivePagedList(position, mDatabase.getCustomerDao().loadPagedAgeOrder());
         }
         return mLiveCustomerList;
     }
 
     LiveData<PagedList<Customer>> getLivePagedList(String key) {
         if (mLiveCustomerList == null) {
-            mLiveCustomerList = new LivePagedListProvider<String, Customer>() {
-                @Override
-                protected DataSource<String, Customer> createDataSource() {
-                    return new LastNameAscCustomerDataSource(mDatabase);
-                }
-            }.create(key,
-                    new PagedList.Config.Builder()
-                            .setPageSize(10)
-                            .setEnablePlaceholders(false)
-                            .build());
+            mLiveCustomerList =
+                    getLivePagedList(key, LastNameAscCustomerDataSource.factory(mDatabase));
         }
         return mLiveCustomerList;
     }
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
index b5df914..db45dc4 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
@@ -16,7 +16,7 @@
 
 package android.arch.persistence.room.integration.testapp.database;
 
-import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.DataSource;
 import android.arch.persistence.room.Dao;
 import android.arch.persistence.room.Insert;
 import android.arch.persistence.room.Query;
@@ -44,12 +44,11 @@
     void insertAll(Customer[] customers);
 
     /**
-     * @return LivePagedListProvider of customers, ordered by last name. Call
-     * {@link LivePagedListProvider#create(Object, android.arch.paging.PagedList.Config)} to
-     * get a LiveData of PagedLists.
+     * @return DataSource.Factory of customers, ordered by last name. Use
+     * {@link android.arch.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
      */
     @Query("SELECT * FROM customer ORDER BY mLastName ASC")
-    LivePagedListProvider<Integer, Customer> loadPagedAgeOrder();
+    DataSource.Factory<Integer, Customer> loadPagedAgeOrder();
 
     /**
      * @return number of customers
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
index 1bc731a..a38d6ae 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
@@ -15,6 +15,7 @@
  */
 package android.arch.persistence.room.integration.testapp.database;
 
+import android.arch.paging.DataSource;
 import android.arch.paging.KeyedDataSource;
 import android.arch.persistence.room.InvalidationTracker;
 import android.support.annotation.NonNull;
@@ -32,10 +33,19 @@
     private final InvalidationTracker.Observer mObserver;
     private SampleDatabase mDb;
 
+    public static Factory<String, Customer> factory(final SampleDatabase db) {
+        return new Factory<String, Customer>() {
+            @Override
+            public DataSource<String, Customer> create() {
+                return new LastNameAscCustomerDataSource(db);
+            }
+        };
+    }
+
     /**
      * Create a DataSource from the customer table of the given database
      */
-    public LastNameAscCustomerDataSource(SampleDatabase db) {
+    private LastNameAscCustomerDataSource(SampleDatabase db) {
         mDb = db;
         mCustomerDao = db.getCustomerDao();
         mObserver = new InvalidationTracker.Observer("customer") {