Merge "PagedList storage and data access refactor" into oc-mr1-support-27.0-dev
am: b376ec7f51

Change-Id: Id340f6696bd236af0e03009129105b4d4f106fff
diff --git a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java b/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
index 664ab16..0656490 100644
--- a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
@@ -21,7 +21,6 @@
 import android.support.annotation.WorkerThread;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -75,7 +74,6 @@
             if (result.size() != loadSize) {
                 throw new IllegalStateException("invalid number of items returned.");
             }
-            Collections.reverse(result);
         }
         return result;
     }
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
index afcc208..be9da20 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
@@ -26,21 +26,65 @@
 /** @hide */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
-    /**
-     * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED.
-     *
-     * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED
-     * if difficult or undesired to compute.
-     */
-    public int countItems() {
-        return COUNT_UNDEFINED;
-    }
-
     @Override
     boolean isContiguous() {
         return true;
     }
 
+    void loadInitial(Key key, int pageSize, boolean enablePlaceholders,
+            PageResult.Receiver<Key, Value> receiver) {
+        NullPaddedList<Value> initial = loadInitial(key, pageSize, enablePlaceholders);
+        if (initial != null) {
+            receiver.onPageResult(new PageResult<>(
+                    PageResult.INIT,
+                    new Page<Key, Value>(initial.mList),
+                    initial.getLeadingNullCount(),
+                    initial.getTrailingNullCount(),
+                    initial.getPositionOffset()));
+        } else {
+            receiver.onPageResult(new PageResult<Key, Value>(
+                    PageResult.INIT, null, 0, 0, 0));
+        }
+    }
+
+    void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+            PageResult.Receiver<Key, Value> receiver) {
+        List<Value> list = loadAfter(currentEndIndex, currentEndItem, pageSize);
+
+        Page<Key, Value> page = list != null
+                ? new Page<Key, Value>(list) : null;
+
+        receiver.postOnPageResult(new PageResult<>(
+                PageResult.APPEND, page, 0, 0, 0));
+    }
+
+    void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
+            PageResult.Receiver<Key, Value> receiver) {
+        List<Value> list = loadBefore(currentBeginIndex, currentBeginItem, pageSize);
+
+        Page<Key, Value> page = list != null
+                ? new Page<Key, Value>(list) : null;
+
+        receiver.postOnPageResult(new PageResult<>(
+                PageResult.PREPEND, page, 0, 0, 0));
+    }
+
+    /**
+     * Get the key from either the position, or item, or null if position/item invalid.
+     * <p>
+     * Position may not match passed item's position - if trying to query the key from a position
+     * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed.
+     */
+    abstract Key getKey(int position, Value item);
+
+    @Nullable
+    abstract List<Value> loadAfterImpl(int currentEndIndex,
+            @NonNull Value currentEndItem, int pageSize);
+
+    @Nullable
+    abstract List<Value> loadBeforeImpl(int currentBeginIndex,
+            @NonNull Value currentBeginItem, int pageSize);
+
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @WorkerThread
@@ -48,21 +92,7 @@
     public abstract NullPaddedList<Value> loadInitial(
             Key key, int initialLoadSize, boolean enablePlaceholders);
 
-    /**
-     * Load data after the given position / 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 number loaded than reduce.
-     *
-     * @param currentEndIndex Load items after this index, starting with currentEndIndex + 1.
-     * @param currentEndItem  Load items after this item, can be used for precise querying based on
-     *                        item contents.
-     * @param pageSize        Suggested number of items to load.
-     * @return List of items, starting at position currentEndIndex + 1. Null if the data source is
-     * no longer valid, and should not be queried again.
-     *
-     * @hide
-     */
+    /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @WorkerThread
     @Nullable
@@ -78,24 +108,7 @@
         return list;
     }
 
-    @Nullable
-    abstract List<Value> loadAfterImpl(int currentEndIndex,
-            @NonNull Value currentEndItem, int pageSize);
-
-    /**
-     * Load data before the given position / 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 number loaded than reduce.
-     *
-     * @param currentBeginIndex Load items before this index, starting with currentBeginIndex - 1.
-     * @param currentBeginItem  Load items after this item, can be used for precise querying based
-     *                          on item contents.
-     * @param pageSize          Suggested number of items to load.
-     * @return List of items, in descending order, starting at position currentBeginIndex - 1.
-     *
-     * @hide
-     */
+    /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @WorkerThread
     @Nullable
@@ -111,15 +124,4 @@
         return list;
 
     }
-
-    @Nullable
-    abstract List<Value> loadBeforeImpl(int currentBeginIndex,
-            @NonNull Value currentBeginItem, int pageSize);
-
-    /**
-     * Get the key from either the position, or item. Position may not match passed item's position,
-     * if trying to query the key from a position that isn't yet loaded, so a fallback item must be
-     * used.
-     */
-    abstract Key getKey(int position, Value item);
 }
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 d8907c3..7835dbe 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
@@ -16,101 +16,136 @@
 
 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.RestrictTo;
-import android.support.annotation.WorkerThread;
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ContiguousPagedList<T> extends NullPaddedList<T> {
-
-    private final ContiguousDataSource<?, T> mDataSource;
-    private final Executor mMainThreadExecutor;
-    private final Executor mBackgroundThreadExecutor;
-    private final Config mConfig;
-
+class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
+    private final ContiguousDataSource<K, V> mDataSource;
     private boolean mPrependWorkerRunning = false;
     private boolean mAppendWorkerRunning = false;
 
     private int mPrependItemsRequested = 0;
     private int mAppendItemsRequested = 0;
 
-    private int mLastLoad = 0;
-    private T mLastItem = null;
+    @SuppressWarnings("unchecked")
+    private final PagedStorage<K, V> mKeyedStorage = (PagedStorage<K, V>) mStorage;
 
-    private AtomicBoolean mDetached = new AtomicBoolean(false);
+    private final PageResult.Receiver<K, V> mReceiver = new PageResult.Receiver<K, V>() {
+        @AnyThread
+        @Override
+        public void postOnPageResult(@NonNull final PageResult<K, V> pageResult) {
+            // NOTE: if we're already on main thread, this can delay page receive by a frame
+            mMainThreadExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    onPageResult(pageResult);
+                }
+            });
+        }
 
-    private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+        @MainThread
+        @Override
+        public void onPageResult(@NonNull PageResult<K, V> pageResult) {
+            if (pageResult.page == null) {
+                detach();
+                return;
+            }
 
-    @WorkerThread
-    <K> ContiguousPagedList(@NonNull ContiguousDataSource<K, T> dataSource,
+            if (isDetached()) {
+                // No op, have detached
+                return;
+            }
+
+            Page<K, V> page = pageResult.page;
+            if (pageResult.type == PageResult.INIT) {
+                mKeyedStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
+                        pageResult.positionOffset, ContiguousPagedList.this);
+                notifyInserted(0, mKeyedStorage.size());
+            } else if (pageResult.type == PageResult.APPEND) {
+                mKeyedStorage.appendPage(page, ContiguousPagedList.this);
+            } else if (pageResult.type == PageResult.PREPEND) {
+                mKeyedStorage.prependPage(page, ContiguousPagedList.this);
+            }
+        }
+    };
+
+    ContiguousPagedList(
+            @NonNull ContiguousDataSource<K, V> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
-            Config config,
-            @Nullable K key) {
-        super();
-
+            @NonNull Config config,
+            final @Nullable K key) {
+        super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor, config);
         mDataSource = dataSource;
-        mMainThreadExecutor = mainThreadExecutor;
-        mBackgroundThreadExecutor = backgroundThreadExecutor;
-        mConfig = config;
-        NullPaddedList<T> initialState = dataSource.loadInitial(
-                key, config.mInitialLoadSizeHint, config.mEnablePlaceholders);
 
-        if (initialState != null) {
-            mPositionOffset = initialState.getPositionOffset();
+        // blocking init just triggers the initial load on the construction thread -
+        // Could still be posted with callback, if desired.
+        mDataSource.loadInitial(key,
+                mConfig.mInitialLoadSizeHint,
+                mConfig.mEnablePlaceholders,
+                mReceiver);
+    }
 
-            mLeadingNullCount = initialState.getLeadingNullCount();
-            mList = new ArrayList<>(initialState.mList);
-            mTrailingNullCount = initialState.getTrailingNullCount();
+    @MainThread
+    @Override
+    void dispatchUpdatesSinceSnapshot(
+            @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
 
-            if (initialState.getLeadingNullCount() == 0
-                    && initialState.getTrailingNullCount() == 0
-                    && config.mPrefetchDistance < 1) {
-                throw new IllegalArgumentException("Null padding is required to support the 0"
-                        + " prefetch case - require either null items or prefetching to fetch"
-                        + " beyond initial load.");
-            }
+        final PagedStorage<?, V> snapshot = pagedListSnapshot.mStorage;
 
-            if (initialState.size() != 0) {
-                mLastLoad = mLeadingNullCount + mList.size() / 2;
-                mLastItem = mList.get(mList.size() / 2);
-            }
-        } else {
-            mList = new ArrayList<>();
-            detach();
+        final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
+        final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended();
+
+        final int previousTrailing = snapshot.getTrailingNullCount();
+        final int previousLeading = snapshot.getLeadingNullCount();
+
+        // Validate that the snapshot looks like a previous version of this list - if it's not,
+        // we can't be sure we'll dispatch callbacks safely
+        if (newlyAppended < 0
+                || newlyPrepended < 0
+                || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
+                || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
+                || (mStorage.getStorageCount()
+                        != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) {
+            throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
+                    + " to be a snapshot of this PagedList");
         }
-        if (mList.size() == 0) {
-            // Empty initial state, so don't try and fetch data.
-            mPrependWorkerRunning = true;
-            mAppendWorkerRunning = true;
+
+        if (newlyAppended != 0) {
+            final int changedCount = Math.min(previousTrailing, newlyAppended);
+            final int addedCount = newlyAppended - changedCount;
+
+            final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount();
+            if (changedCount != 0) {
+                callback.onChanged(endPosition, changedCount);
+            }
+            if (addedCount != 0) {
+                callback.onInserted(endPosition + changedCount, addedCount);
+            }
+        }
+        if (newlyPrepended != 0) {
+            final int changedCount = Math.min(previousLeading, newlyPrepended);
+            final int addedCount = newlyPrepended - changedCount;
+
+            if (changedCount != 0) {
+                callback.onChanged(previousLeading, changedCount);
+            }
+            if (addedCount != 0) {
+                callback.onInserted(0, addedCount);
+            }
         }
     }
 
+    @MainThread
     @Override
-    public T get(int index) {
-        T item = super.get(index);
-        if (item != null) {
-            mLastItem = item;
-        }
-        return item;
-    }
-
-    @Override
-    public void loadAround(int index) {
-        mLastLoad = index + mPositionOffset;
-
-        int prependItems = mConfig.mPrefetchDistance - (index - mLeadingNullCount);
-        int appendItems = index + mConfig.mPrefetchDistance - (mLeadingNullCount + mList.size());
+    protected void loadAroundInternal(int index) {
+        int prependItems = mConfig.mPrefetchDistance - (index - mStorage.getLeadingNullCount());
+        int appendItems = index + mConfig.mPrefetchDistance
+                - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
 
         mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
         if (mPrependItemsRequested > 0) {
@@ -123,21 +158,6 @@
         }
     }
 
-    @Override
-    public int getLoadedCount() {
-        return mList.size();
-    }
-
-    @Override
-    public int getLeadingNullCount() {
-        return mLeadingNullCount;
-    }
-
-    @Override
-    public int getTrailingNullCount() {
-        return mTrailingNullCount;
-    }
-
     @MainThread
     private void schedulePrepend() {
         if (mPrependWorkerRunning) {
@@ -145,29 +165,17 @@
         }
         mPrependWorkerRunning = true;
 
-        final int position = mLeadingNullCount + mPositionOffset;
-        final T item = mList.get(0);
+        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();
         mBackgroundThreadExecutor.execute(new Runnable() {
             @Override
             public void run() {
-                if (mDetached.get()) {
+                if (isDetached()) {
                     return;
                 }
-
-                final List<T> data = mDataSource.loadBefore(position, item, mConfig.mPageSize);
-                if (data != null) {
-                    mMainThreadExecutor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mDetached.get()) {
-                                return;
-                            }
-                            prependImpl(data);
-                        }
-                    });
-                } else {
-                    detach();
-                }
+                mDataSource.loadBefore(position, item, mConfig.mPageSize, mReceiver);
             }
         });
     }
@@ -179,56 +187,44 @@
         }
         mAppendWorkerRunning = true;
 
-        final int position = mLeadingNullCount + mList.size() - 1 + mPositionOffset;
-        final T item = mList.get(mList.size() - 1);
+        final int position = mStorage.getLeadingNullCount()
+                + 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();
         mBackgroundThreadExecutor.execute(new Runnable() {
             @Override
             public void run() {
-                if (mDetached.get()) {
+                if (isDetached()) {
                     return;
                 }
-
-                final List<T> data = mDataSource.loadAfter(position, item, mConfig.mPageSize);
-                if (data != null) {
-                    mMainThreadExecutor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mDetached.get()) {
-                                return;
-                            }
-                            appendImpl(data);
-                        }
-                    });
-                } else {
-                    detach();
-                }
+                mDataSource.loadAfter(position, item, mConfig.mPageSize, mReceiver);
             }
         });
     }
 
+    @Override
+    boolean isContiguous() {
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Object getLastKey() {
+        return mDataSource.getKey(mLastLoad, mLastItem);
+    }
+
     @MainThread
-    private void prependImpl(List<T> before) {
-        final int count = before.size();
-        if (count == 0) {
-            // Nothing returned from source, stop loading in this direction
-            return;
-        }
+    @Override
+    public void onInitialized(int count) {
+        notifyInserted(0, count);
+    }
 
-        Collections.reverse(before);
-        mList.addAll(0, before);
-
-        final int changedCount = Math.min(mLeadingNullCount, count);
-        final int addedCount = count - changedCount;
-
-        if (changedCount != 0) {
-            mLeadingNullCount -= changedCount;
-        }
-        mPositionOffset -= addedCount;
-        mNumberPrepended += count;
-
-
-        // only try to post more work after fully prepended (with offsets / null counts updated)
-        mPrependItemsRequested -= count;
+    @MainThread
+    @Override
+    public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) {
+        // consider whether to post more work, now that a page is fully prepended
+        mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount;
         mPrependWorkerRunning = false;
         if (mPrependItemsRequested > 0) {
             // not done prepending, keep going
@@ -236,39 +232,16 @@
         }
 
         // finally dispatch callbacks, after prepend may have already been scheduled
-        for (WeakReference<Callback> weakRef : mCallbacks) {
-            Callback callback = weakRef.get();
-            if (callback != null) {
-                if (changedCount != 0) {
-                    callback.onChanged(mLeadingNullCount, changedCount);
-                }
-                if (addedCount != 0) {
-                    callback.onInserted(0, addedCount);
-                }
-            }
-        }
+        notifyChanged(leadingNulls, changedCount);
+        notifyInserted(0, addedCount);
     }
 
     @MainThread
-    private void appendImpl(List<T> after) {
-        final int count = after.size();
-        if (count == 0) {
-            // Nothing returned from source, stop loading in this direction
-            return;
-        }
+    @Override
+    public void onPageAppended(int endPosition, int changedCount, int addedCount) {
+        // consider whether to post more work, now that a page is fully appended
 
-        mList.addAll(after);
-
-        final int changedCount = Math.min(mTrailingNullCount, count);
-        final int addedCount = count - changedCount;
-
-        if (changedCount != 0) {
-            mTrailingNullCount -= changedCount;
-        }
-        mNumberAppended += count;
-
-        // only try to post more work after fully appended (with null counts updated)
-        mAppendItemsRequested -= count;
+        mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
         mAppendWorkerRunning = false;
         if (mAppendItemsRequested > 0) {
             // not done appending, keep going
@@ -276,100 +249,19 @@
         }
 
         // finally dispatch callbacks, after append may have already been scheduled
-        for (WeakReference<Callback> weakRef : mCallbacks) {
-            Callback callback = weakRef.get();
-            if (callback != null) {
-                final int endPosition = mLeadingNullCount + mList.size() - count;
-                if (changedCount != 0) {
-                    callback.onChanged(endPosition, changedCount);
-                }
-                if (addedCount != 0) {
-                    callback.onInserted(endPosition + changedCount, addedCount);
-                }
-            }
-        }
+        notifyChanged(endPosition, changedCount);
+        notifyInserted(endPosition + changedCount, addedCount);
     }
 
+    @MainThread
     @Override
-    public boolean isImmutable() {
-        // TODO: return true if had nulls, and now getLoadedCount() == size(). Is that safe?
-        // Currently we don't prevent DataSources from returning more items than their null counts
-        return isDetached();
+    public void onPagePlaceholderInserted(int pageIndex) {
+        throw new IllegalStateException("Tiled callback on ContiguousPagedList");
     }
 
+    @MainThread
     @Override
-    public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
-            @NonNull Callback callback) {
-        NullPaddedList<T> snapshot = (NullPaddedList<T>) previousSnapshot;
-        if (snapshot != this && snapshot != null) {
-            final int newlyAppended = mNumberAppended - snapshot.getNumberAppended();
-            final int newlyPrepended = mNumberPrepended - snapshot.getNumberPrepended();
-
-            final int previousTrailing = snapshot.getTrailingNullCount();
-            final int previousLeading = snapshot.getLeadingNullCount();
-
-            // Validate that the snapshot looks like a previous version of this list - if it's not,
-            // we can't be sure we'll dispatch callbacks safely
-            if (newlyAppended < 0
-                    || newlyPrepended < 0
-                    || mTrailingNullCount != Math.max(previousTrailing - newlyAppended, 0)
-                    || mLeadingNullCount != Math.max(previousLeading - newlyPrepended, 0)
-                    || snapshot.getLoadedCount() + newlyAppended + newlyPrepended != mList.size()) {
-                throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
-                        + " to be a snapshot of this list");
-            }
-
-            if (newlyAppended != 0) {
-                final int changedCount = Math.min(previousTrailing, newlyAppended);
-                final int addedCount = newlyAppended - changedCount;
-
-                final int endPosition =
-                        snapshot.getLeadingNullCount() + snapshot.getLoadedCount();
-                if (changedCount != 0) {
-                    callback.onChanged(endPosition, changedCount);
-                }
-                if (addedCount != 0) {
-                    callback.onInserted(endPosition + changedCount, addedCount);
-                }
-            }
-            if (newlyPrepended != 0) {
-                final int changedCount = Math.min(previousLeading, newlyPrepended);
-                final int addedCount = newlyPrepended - changedCount;
-
-                if (changedCount != 0) {
-                    callback.onChanged(previousLeading, changedCount);
-                }
-                if (addedCount != 0) {
-                    callback.onInserted(0, addedCount);
-                }
-            }
-        }
-        mCallbacks.add(new WeakReference<>(callback));
-    }
-
-    @Override
-    public void removeWeakCallback(@NonNull Callback callback) {
-        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-            Callback currentCallback = mCallbacks.get(i).get();
-            if (currentCallback == null || currentCallback == callback) {
-                mCallbacks.remove(i);
-            }
-        }
-    }
-
-    @Override
-    public boolean isDetached() {
-        return mDetached.get();
-    }
-
-    @SuppressWarnings("WeakerAccess")
-    public void detach() {
-        mDetached.set(true);
-    }
-
-    @Nullable
-    @Override
-    public Object getLastKey() {
-        return mDataSource.getKey(mLastLoad, mLastItem);
+    public void onPageInserted(int start, int count) {
+        throw new IllegalStateException("Tiled callback on ContiguousPagedList");
     }
 }
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 48fbec5..524e570 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -17,6 +17,7 @@
 package android.arch.paging;
 
 import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
 
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -60,15 +61,6 @@
     public static int COUNT_UNDEFINED = -1;
 
     /**
-     * Number of items that this DataSource can provide in total, or {@link #COUNT_UNDEFINED}.
-     *
-     * @return number of items that this DataSource can provide in total, or
-     * {@link #COUNT_UNDEFINED} if expensive or undesired to compute.
-     */
-    @WorkerThread
-    public abstract int countItems();
-
-    /**
      * Returns true if the data source guaranteed to produce a contiguous set of items,
      * never producing gaps.
      */
@@ -111,7 +103,7 @@
      */
     @AnyThread
     @SuppressWarnings("WeakerAccess")
-    public void addInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
         mOnInvalidatedCallbacks.add(onInvalidatedCallback);
     }
 
@@ -122,7 +114,7 @@
      */
     @AnyThread
     @SuppressWarnings("WeakerAccess")
-    public void removeInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
         mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
     }
 
diff --git a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
index 8cf6829..0d45294 100644
--- a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
@@ -103,10 +103,6 @@
  * @param <Value> Type of items being loaded by the DataSource.
  */
 public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
-    @Override
-    public final int countItems() {
-        return 0; // method not called, can't be overridden
-    }
 
     @Nullable
     @Override
@@ -118,7 +114,14 @@
     @Override
     List<Value> loadBeforeImpl(
             int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize) {
-        return loadBefore(getKey(currentBeginItem), 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
@@ -191,6 +194,8 @@
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @WorkerThread
+    @Override
     public NullPaddedList<Value> loadInitial(
             @Nullable Key key, int initialLoadSize, boolean enablePlaceholders) {
         if (isInvalid()) {
diff --git a/paging/common/src/main/java/android/arch/paging/NullPaddedList.java b/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
index 4300030..c7b0b23 100644
--- a/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
+++ b/paging/common/src/main/java/android/arch/paging/NullPaddedList.java
@@ -16,11 +16,9 @@
 
 package android.arch.paging;
 
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 
-import java.util.ArrayList;
+import java.util.AbstractList;
 import java.util.List;
 
 /**
@@ -31,18 +29,11 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class NullPaddedList<Type> extends PagedList<Type> {
+public class NullPaddedList<Type> extends AbstractList<Type> {
     List<Type> mList;
-    int mTrailingNullCount;
-    int mLeadingNullCount;
-    int mPositionOffset;
-
-    // track the items prepended/appended since the PagedList was initialized
-    int mNumberPrepended;
-    int mNumberAppended;
-
-    NullPaddedList() {
-    }
+    private int mTrailingNullCount;
+    private int mLeadingNullCount;
+    private int mPositionOffset;
 
     @Override
     public String toString() {
@@ -91,20 +82,6 @@
         mPositionOffset = positionOffset;
     }
 
-    /**
-     * Create a copy of the passed NullPaddedList.
-     *
-     * @param other Other list to copy.
-     */
-    NullPaddedList(NullPaddedList<Type> other) {
-        mLeadingNullCount = other.getLeadingNullCount();
-        mList = other.isImmutable() ? other.mList : new ArrayList<>(other.mList);
-        mTrailingNullCount = other.getTrailingNullCount();
-
-        mNumberPrepended = other.getNumberPrepended();
-        mNumberAppended = other.getNumberAppended();
-    }
-
     // --------------- PagedList API ---------------
 
     @Override
@@ -124,46 +101,12 @@
     }
 
     @Override
-    public void loadAround(int index) {
-        // do nothing - immutable, so no fetching will be done
-    }
-
-    @Override
     public final int size() {
         return getLoadedCount() + getLeadingNullCount() + getTrailingNullCount();
     }
 
-    public boolean isImmutable() {
-        return true;
-    }
-
-    @Override
-    public PagedList<Type> snapshot() {
-        if (isImmutable()) {
-            return this;
-        }
-        return new NullPaddedList<>(this);
-    }
-
-    @Override
-    boolean isContiguous() {
-        return true;
-    }
-
-    @Override
-    public void addWeakCallback(@Nullable PagedList<Type> previousSnapshot,
-            @NonNull Callback callback) {
-        // no op, immutable
-    }
-
-    @Override
-    public void removeWeakCallback(Callback callback) {
-        // no op, immutable
-    }
-
     // --------------- Contiguous API ---------------
 
-    @Override
     public int getPositionOffset() {
         return mPositionOffset;
     }
@@ -194,12 +137,4 @@
     public int getTrailingNullCount() {
         return mTrailingNullCount;
     }
-
-    int getNumberPrepended() {
-        return mNumberPrepended;
-    }
-
-    int getNumberAppended() {
-        return mNumberAppended;
-    }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/Page.java b/paging/common/src/main/java/android/arch/paging/Page.java
new file mode 100644
index 0000000..e9890ed
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/Page.java
@@ -0,0 +1,51 @@
+/*
+ * 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.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.List;
+
+/**
+ * Immutable class representing a page of data loaded from a DataSource.
+ * <p>
+ * Optionally stores before/after keys for cases where they cannot be computed, but the DataSource
+ * can provide them as part of loading a page.
+ * <p>
+ * A page's list must never be modified.
+ */
+class Page<K, V> {
+    @SuppressWarnings("WeakerAccess")
+    @Nullable
+    public final K beforeKey;
+    @NonNull
+    public final List<V> items;
+    @SuppressWarnings("WeakerAccess")
+    @Nullable
+    public K afterKey;
+
+    Page(@NonNull List<V> items) {
+        this(null, items, null);
+    }
+
+    Page(@Nullable K beforeKey, @NonNull List<V> items, @Nullable K afterKey) {
+        this.beforeKey = beforeKey;
+        this.items = items;
+        this.afterKey = afterKey;
+    }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PageArrayList.java b/paging/common/src/main/java/android/arch/paging/PageArrayList.java
deleted file mode 100644
index b90d055..0000000
--- a/paging/common/src/main/java/android/arch/paging/PageArrayList.java
+++ /dev/null
@@ -1,130 +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;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class PageArrayList<T> extends PagedList<T> {
-    // partial list of pages, doesn't include pages below the lowest accessed, or above the highest
-    final ArrayList<List<T>> mPages;
-
-    // to access page at index N, do mPages.get(N - mPageIndexOffset), but do bounds checking first!
-    int mPageIndexOffset;
-
-    final int mPageSize;
-    final int mCount;
-    final int mMaxPageCount;
-
-    PageArrayList(int pageSize, int count) {
-        mPages = new ArrayList<>();
-        mPageSize = pageSize;
-        mCount = count;
-        mMaxPageCount = (mCount + mPageSize - 1) / mPageSize;
-    }
-
-    private PageArrayList(PageArrayList<T> other) {
-        mPages = other.isImmutable() ? other.mPages : new ArrayList<>(other.mPages);
-        mPageIndexOffset = other.mPageIndexOffset;
-        mPageSize = other.mPageSize;
-        mCount = other.size();
-        mMaxPageCount = other.mMaxPageCount;
-    }
-
-    @Override
-    public T get(int index) {
-        if (index < 0 || index >= mCount) {
-            throw new IllegalArgumentException();
-        }
-
-        int localPageIndex = getLocalPageIndex(index);
-
-        List<T> page = getPage(localPageIndex);
-
-        if (page == null) {
-            // page empty
-            return null;
-        }
-
-        return page.get(index % mPageSize);
-    }
-
-    @Nullable
-    private List<T> getPage(int localPageIndex) {
-        if (localPageIndex < 0 || localPageIndex >= mPages.size()) {
-            // page not present
-            return null;
-        }
-
-        return mPages.get(localPageIndex);
-    }
-
-    private int getLocalPageIndex(int index) {
-        return index / mPageSize - mPageIndexOffset;
-    }
-
-    @Override
-    public void loadAround(int index) {
-        // do nothing - immutable, so no fetching will be done
-    }
-
-    @Override
-    public int size() {
-        return mCount;
-    }
-
-    @Override
-    public boolean isImmutable() {
-        return true;
-    }
-
-    boolean hasPage(int pageIndex) {
-        final int localPageIndex = pageIndex - mPageIndexOffset;
-        List<T> page = getPage(localPageIndex);
-        return page != null && page.size() != 0;
-    }
-
-    @Override
-    public PagedList<T> snapshot() {
-        if (isImmutable()) {
-            return this;
-        }
-        return new PageArrayList<>(this);
-    }
-
-    @Override
-    boolean isContiguous() {
-        return false;
-    }
-
-    @Override
-    public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
-            @NonNull Callback callback) {
-        // no op, immutable
-    }
-
-    @Override
-    public void removeWeakCallback(Callback callback) {
-        // no op, immutable
-    }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PageResult.java b/paging/common/src/main/java/android/arch/paging/PageResult.java
new file mode 100644
index 0000000..a4090f6
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/PageResult.java
@@ -0,0 +1,56 @@
+/*
+ * 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.MainThread;
+import android.support.annotation.NonNull;
+
+class PageResult<K, V> {
+    static final int INIT = 0;
+
+    // contiguous results
+    static final int APPEND = 1;
+    static final int PREPEND = 2;
+
+    // non-contiguous, tile result
+    static final int TILE = 3;
+
+    public final int type;
+    public final Page<K, V> page;
+    @SuppressWarnings("WeakerAccess")
+    public final int leadingNulls;
+    @SuppressWarnings("WeakerAccess")
+    public final int trailingNulls;
+    @SuppressWarnings("WeakerAccess")
+    public final int positionOffset;
+
+    PageResult(int type, Page<K, V> page, int leadingNulls, int trailingNulls, int positionOffset) {
+        this.type = type;
+        this.page = page;
+        this.leadingNulls = leadingNulls;
+        this.trailingNulls = trailingNulls;
+        this.positionOffset = positionOffset;
+    }
+
+    interface Receiver<K, V> {
+        @AnyThread
+        void postOnPageResult(@NonNull PageResult<K, V> pageResult);
+        @MainThread
+        void onPageResult(@NonNull PageResult<K, V> pageResult);
+    }
+}
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 6a31b68..1f07bfa 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -20,9 +20,12 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 
+import java.lang.ref.WeakReference;
 import java.util.AbstractList;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Lazy loading list that pages in content from a {@link DataSource}.
@@ -90,9 +93,28 @@
  * @param <T> The type of the entries in the list.
  */
 public abstract class PagedList<T> extends AbstractList<T> {
-    // Since we currently rely on implementation details of two implementations,
-    // prevent external subclassing
-    PagedList() {
+    final Executor mMainThreadExecutor;
+    final Executor mBackgroundThreadExecutor;
+    final Config mConfig;
+
+    @NonNull
+    final PagedStorage<?, T> mStorage;
+
+    int mLastLoad = 0;
+    T mLastItem = null;
+
+    private final AtomicBoolean mDetached = new AtomicBoolean(false);
+
+    protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+
+    PagedList(@NonNull PagedStorage<?, T> storage,
+            @NonNull Executor mainThreadExecutor,
+            @NonNull Executor backgroundThreadExecutor,
+            @NonNull Config config) {
+        mStorage = storage;
+        mMainThreadExecutor = mainThreadExecutor;
+        mBackgroundThreadExecutor = backgroundThreadExecutor;
+        mConfig = config;
     }
 
     /**
@@ -280,7 +302,13 @@
      */
     @Override
     @Nullable
-    public abstract T get(int index);
+    public T get(int index) {
+        T item = mStorage.get(index);
+        if (item != null) {
+            mLastItem = item;
+        }
+        return item;
+    }
 
 
     /**
@@ -288,7 +316,10 @@
      *
      * @param index Index at which to load.
      */
-    public abstract void loadAround(int index);
+    public void loadAround(int index) {
+        mLastLoad = index + getPositionOffset();
+        loadAroundInternal(index);
+    }
 
 
     /**
@@ -297,7 +328,9 @@
      * @return Current total size of the list.
      */
     @Override
-    public abstract int size();
+    public int size() {
+        return mStorage.size();
+    }
 
     /**
      * Returns whether the list is immutable. Immutable lists may not become mutable again, and may
@@ -305,15 +338,25 @@
      *
      * @return True if the PagedList is immutable.
      */
-    public abstract boolean isImmutable();
+    @SuppressWarnings("WeakerAccess")
+    public boolean isImmutable() {
+        return isDetached();
+    }
 
     /**
      * Returns an immutable snapshot of the PagedList. If this PagedList is already
      * immutable, it will be returned.
      *
-     * @return Immutable snapshot of PagedList, which may be the PagedList itself.
+     * @return Immutable snapshot of PagedList data.
      */
-    public abstract List<T> snapshot();
+    @NonNull
+    public List<T> snapshot() {
+        if (isImmutable()) {
+            return this;
+        }
+
+        return new SnapshotPagedList<>(this);
+    }
 
     abstract boolean isContiguous();
 
@@ -328,9 +371,7 @@
      * @return Key of position most recently passed to {@link #loadAround(int)}.
      */
     @Nullable
-    public Object getLastKey() {
-        return null;
-    }
+    public abstract Object getLastKey();
 
     /**
      * True if the PagedList has detached the DataSource it was loading from, and will no longer
@@ -338,8 +379,9 @@
      *
      * @return True if the data source is detached.
      */
+    @SuppressWarnings("WeakerAccess")
     public boolean isDetached() {
-        return true;
+        return mDetached.get();
     }
 
     /**
@@ -349,7 +391,9 @@
      * signal to stop loading. The PagedList will continue to present existing data, but will not
      * initiate new loads.
      */
+    @SuppressWarnings("WeakerAccess")
     public void detach() {
+        mDetached.set(true);
     }
 
     /**
@@ -361,7 +405,7 @@
      * If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0.
      */
     public int getPositionOffset() {
-        return 0;
+        return mStorage.getPositionOffset();
     }
 
     /**
@@ -385,16 +429,68 @@
      * @param callback         Callback to dispatch to.
      * @see #removeWeakCallback(Callback)
      */
-    public abstract void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
-            @NonNull Callback callback);
+    @SuppressWarnings("WeakerAccess")
+    public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
+        if (previousSnapshot != null && previousSnapshot != this) {
+            PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
+            //noinspection unchecked
+            dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
+        }
 
+        // first, clean up any empty weak refs
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            Callback currentCallback = mCallbacks.get(i).get();
+            if (currentCallback == null) {
+                mCallbacks.remove(i);
+            }
+        }
+
+        // then add the new one
+        mCallbacks.add(new WeakReference<>(callback));
+    }
     /**
      * Removes a previously added callback.
      *
      * @param callback Callback, previously added.
-     * @see #addWeakCallback(PagedList, Callback)
+     * @see #addWeakCallback(List, Callback)
      */
-    public abstract void removeWeakCallback(Callback callback);
+    @SuppressWarnings("WeakerAccess")
+    public void removeWeakCallback(@NonNull Callback callback) {
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            Callback currentCallback = mCallbacks.get(i).get();
+            if (currentCallback == null || currentCallback == callback) {
+                // found callback, or empty weak ref
+                mCallbacks.remove(i);
+            }
+        }
+    }
+
+    void notifyInserted(int position, int count) {
+        if (count != 0) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                Callback callback = mCallbacks.get(i).get();
+                if (callback != null) {
+                    callback.onInserted(position, count);
+                }
+            }
+        }
+    }
+
+    void notifyChanged(int position, int count) {
+        if (count != 0) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                Callback callback = mCallbacks.get(i).get();
+                if (callback != null) {
+                    callback.onChanged(position, count);
+                }
+            }
+        }
+    }
+
+    abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
+            @NonNull Callback callback);
+
+    abstract void loadAroundInternal(int index);
 
     /**
      * Callback signaling when content is loaded into the list.
@@ -545,10 +641,15 @@
              * Defines how many items to load when first load occurs, if you are using a
              * {@link KeyedDataSource}.
              * <p>
-             * If you are using an {@link TiledDataSource}, this value is currently ignored.
-             * Otherwise, this value will be passed to
-             * {@link KeyedDataSource#loadInitial(int)} to load a (typically) larger amount
-             * of data on first load.
+             * This value is typically larger than page size, so on first load data there's a large
+             * enough range of content loaded to cover small scrolls.
+             * <p>
+             * If used with a {@link TiledDataSource}, this value is rounded to the nearest number
+             * of pages, with a minimum of two pages, and loaded with a single call to
+             * {@link TiledDataSource#loadRange(int, int)}.
+             * <p>
+             * If used with a {@link KeyedDataSource}, this value will be passed to
+             * {@link KeyedDataSource#loadInitial(int)}.
              * <p>
              * If not set, defaults to three times page size.
              *
diff --git a/paging/common/src/main/java/android/arch/paging/PagedStorage.java b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
new file mode 100644
index 0000000..7f91290
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
@@ -0,0 +1,433 @@
+/*
+ * 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.NonNull;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+final class PagedStorage<K, V> extends AbstractList<V> {
+    // Always set
+    private int mLeadingNullCount;
+    /**
+     * List of pages in storage.
+     *
+     * Two storage modes:
+     *
+     * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled().
+     *     Safe to access any item in any page.
+     *
+     * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true.
+     *     mPages may have nulls, or placeholder (empty) pages while content is loading.
+     */
+    private final ArrayList<Page<K, V>> mPages;
+    private int mTrailingNullCount;
+
+    private int mPositionOffset;
+    /**
+     * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in
+     * {@link #mPages} may be null, but this value still counts them.
+     */
+    private int mStorageCount;
+
+    // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set
+    private int mPageSize;
+
+    private int mNumberPrepended;
+    private int mNumberAppended;
+
+    // only used in tiling case
+    private Page<K, V> mPlaceholderPage;
+
+    PagedStorage() {
+        mLeadingNullCount = 0;
+        mPages = new ArrayList<>();
+        mTrailingNullCount = 0;
+        mPositionOffset = 0;
+        mStorageCount = 0;
+        mPageSize = 1;
+        mNumberPrepended = 0;
+        mNumberAppended = 0;
+    }
+
+    PagedStorage(int leadingNulls, Page<K, V> page, int trailingNulls) {
+        this();
+        init(leadingNulls, page, trailingNulls, 0);
+    }
+
+    private PagedStorage(PagedStorage<K, V> other) {
+        mLeadingNullCount = other.mLeadingNullCount;
+        mPages = new ArrayList<>(other.mPages);
+        mTrailingNullCount = other.mTrailingNullCount;
+        mPositionOffset = other.mPositionOffset;
+        mStorageCount = other.mStorageCount;
+        mPageSize = other.mPageSize;
+        mNumberPrepended = other.mNumberPrepended;
+        mNumberAppended = other.mNumberAppended;
+
+        // preserve placeholder page so we can locate placeholder pages if needed later
+        mPlaceholderPage = other.mPlaceholderPage;
+    }
+
+    PagedStorage<K, V> snapshot() {
+        return new PagedStorage<>(this);
+    }
+
+    private void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset) {
+        mLeadingNullCount = leadingNulls;
+        mPages.clear();
+        mPages.add(page);
+        mTrailingNullCount = trailingNulls;
+
+        mPositionOffset = positionOffset;
+        mStorageCount = page.items.size();
+
+        // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
+        // even if it will break if nulls convert.
+        mPageSize = page.items.size();
+
+        mNumberPrepended = 0;
+        mNumberAppended = 0;
+    }
+
+    void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset,
+            @NonNull Callback callback) {
+        init(leadingNulls, page, trailingNulls, positionOffset);
+        callback.onInitialized(size());
+    }
+
+    @Override
+    public V get(int i) {
+        if (i < 0 || i >= size()) {
+            throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
+        }
+
+        // is it definitely outside 'mPages'?
+        int localIndex = i - mLeadingNullCount;
+        if (localIndex < 0 || localIndex >= mStorageCount) {
+            return null;
+        }
+
+        int localPageIndex;
+        int pageInternalIndex;
+
+        if (isTiled()) {
+            // it's inside mPages, and we're tiled. Jump to correct tile.
+            localPageIndex = localIndex / mPageSize;
+            pageInternalIndex = localIndex % mPageSize;
+        } else {
+            // it's inside mPages, but page sizes aren't regular. Walk to correct tile.
+            // Pages can only be null while tiled, so accessing page count is safe.
+            pageInternalIndex = localIndex;
+            final int localPageCount = mPages.size();
+            for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
+                int pageSize = mPages.get(localPageIndex).items.size();
+                if (pageSize > pageInternalIndex) {
+                    // stop, found the page
+                    break;
+                }
+                pageInternalIndex -= pageSize;
+            }
+        }
+
+        Page<?, V> page = mPages.get(localPageIndex);
+        if (page == null || page.items.size() == 0) {
+            // can only occur in tiled case, with untouched inner/placeholder pages
+            return null;
+        }
+        return page.items.get(pageInternalIndex);
+    }
+
+    /**
+     * Returns true if all pages are the same size, except for the last, which may be smaller
+     */
+    boolean isTiled() {
+        return mPageSize > 0;
+    }
+
+    int getLeadingNullCount() {
+        return mLeadingNullCount;
+    }
+
+    int getTrailingNullCount() {
+        return mTrailingNullCount;
+    }
+
+    int getStorageCount() {
+        return mStorageCount;
+    }
+
+    int getNumberAppended() {
+        return mNumberAppended;
+    }
+
+    int getNumberPrepended() {
+        return mNumberPrepended;
+    }
+
+    int getPageCount() {
+        return mPages.size();
+    }
+
+    interface Callback {
+        void onInitialized(int count);
+        void onPagePrepended(int leadingNulls, int changed, int added);
+        void onPageAppended(int endPosition, int changed, int added);
+        void onPagePlaceholderInserted(int pageIndex);
+        void onPageInserted(int start, int count);
+    }
+
+    int getPositionOffset() {
+        return mPositionOffset;
+    }
+
+    @Override
+    public int size() {
+        return mLeadingNullCount + mStorageCount + mTrailingNullCount;
+    }
+
+    int computeLeadingNulls() {
+        int total = mLeadingNullCount;
+        final int pageCount = mPages.size();
+        for (int i = 0; i < pageCount; i++) {
+            Page page = mPages.get(i);
+            if (page != null && page != mPlaceholderPage) {
+                break;
+            }
+            total += mPageSize;
+        }
+        return total;
+    }
+
+    int computeTrailingNulls() {
+        int total = mTrailingNullCount;
+        for (int i = mPages.size() - 1; i >= 0; i--) {
+            Page page = mPages.get(i);
+            if (page != null && page != mPlaceholderPage) {
+                break;
+            }
+            total += mPageSize;
+        }
+        return total;
+    }
+
+    // ---------------- Contiguous API -------------------
+
+    V getFirstContiguousItem() {
+        // 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() {
+        // 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);
+        return page.items.get(page.items.size() - 1);
+    }
+
+    public void prependPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
+        final int count = page.items.size();
+        if (count == 0) {
+            // Nothing returned from source, stop loading in this direction
+            return;
+        }
+        if (mPageSize > 0 && count != mPageSize) {
+            if (mPages.size() == 1 && count > mPageSize) {
+                // prepending to a single item - update current page size to that of 'inner' page
+                mPageSize = count;
+            } else {
+                // no longer tiled
+                mPageSize = -1;
+            }
+        }
+
+        mPages.add(0, page);
+        mStorageCount += count;
+
+        final int changedCount = Math.min(mLeadingNullCount, count);
+        final int addedCount = count - changedCount;
+
+        if (changedCount != 0) {
+            mLeadingNullCount -= changedCount;
+        }
+        mPositionOffset -= addedCount;
+        mNumberPrepended += count;
+
+        callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
+    }
+
+    public void appendPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
+        final int count = page.items.size();
+        if (count == 0) {
+            // Nothing returned from source, stop loading in this direction
+            return;
+        }
+
+        if (mPageSize > 0) {
+            // if the previous page was smaller than mPageSize,
+            // or if this page is larger than the previous, disable tiling
+            if (mPages.get(mPages.size() - 1).items.size() != mPageSize
+                    || count > mPageSize) {
+                mPageSize = -1;
+            }
+        }
+
+        mPages.add(page);
+        mStorageCount += count;
+
+        final int changedCount = Math.min(mTrailingNullCount, count);
+        final int addedCount = count - changedCount;
+
+        if (changedCount != 0) {
+            mTrailingNullCount -= changedCount;
+        }
+        mNumberAppended += count;
+        callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
+                changedCount, addedCount);
+    }
+
+    // ------------------ Non-Contiguous API (tiling required) ----------------------
+
+    public void insertPage(int position, @NonNull Page<K, V> page, Callback callback) {
+        final int newPageSize = page.items.size();
+        if (newPageSize != mPageSize) {
+            // differing page size is OK in 2 cases, when the page is being added:
+            // 1) to the end (in which case, ignore new smaller size)
+            // 2) only the last page has been added so far (in which case, adopt new bigger size)
+
+            int size = size();
+            boolean addingLastPage = position == (size - size % mPageSize)
+                    && newPageSize < mPageSize;
+            boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
+                    && newPageSize > mPageSize;
+
+            // OK only if existing single page, and it's the last one
+            if (!onlyEndPagePresent && !addingLastPage) {
+                throw new IllegalArgumentException("page introduces incorrect tiling");
+            }
+            if (onlyEndPagePresent) {
+                mPageSize = newPageSize;
+            }
+        }
+
+        int pageIndex = position / mPageSize;
+
+        allocatePageRange(pageIndex, pageIndex);
+
+        int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
+
+        Page<K, V> oldPage = mPages.get(localPageIndex);
+        if (oldPage != null && oldPage != mPlaceholderPage) {
+            throw new IllegalArgumentException(
+                    "Invalid position " + position + ": data already loaded");
+        }
+        mPages.set(localPageIndex, page);
+        callback.onPageInserted(position, page.items.size());
+    }
+
+    private Page<K, V> getPlaceholderPage() {
+        if (mPlaceholderPage == null) {
+            @SuppressWarnings("unchecked")
+            List<V> list = Collections.emptyList();
+            mPlaceholderPage = new Page<>(null, list, null);
+        }
+        return mPlaceholderPage;
+    }
+
+    private void allocatePageRange(final int minimumPage, final int maximumPage) {
+        int leadingNullPages = mLeadingNullCount / mPageSize;
+
+        if (minimumPage < leadingNullPages) {
+            for (int i = 0; i < leadingNullPages - minimumPage; i++) {
+                mPages.add(0, null);
+            }
+            int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize;
+            mStorageCount += newStorageAllocated;
+            mLeadingNullCount -= newStorageAllocated;
+
+            leadingNullPages = minimumPage;
+        }
+        if (maximumPage >= leadingNullPages + mPages.size()) {
+            int newStorageAllocated = Math.min(mTrailingNullCount,
+                    (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize);
+            for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) {
+                mPages.add(mPages.size(), null);
+            }
+            mStorageCount += newStorageAllocated;
+            mTrailingNullCount -= newStorageAllocated;
+        }
+    }
+
+    public void allocatePlaceholders(int index, int prefetchDistance,
+            int pageSize, Callback callback) {
+        if (pageSize != mPageSize) {
+            if (pageSize < mPageSize) {
+                throw new IllegalArgumentException("Page size cannot be reduced");
+            }
+            if (mPages.size() != 1 || mTrailingNullCount != 0) {
+                // not in single, last page allocated case - can't change page size
+                throw new IllegalArgumentException(
+                        "Page size can change only if last page is only one present");
+            }
+            mPageSize = pageSize;
+        }
+
+        final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
+        int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
+        int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
+
+        allocatePageRange(minimumPage, maximumPage);
+        int leadingNullPages = mLeadingNullCount / mPageSize;
+        for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
+            int localPageIndex = pageIndex - leadingNullPages;
+            if (mPages.get(localPageIndex) == null) {
+                mPages.set(localPageIndex, getPlaceholderPage());
+                callback.onPagePlaceholderInserted(pageIndex);
+            }
+        }
+    }
+
+    public boolean hasPage(int pageSize, int index) {
+        // NOTE: we pass pageSize here to avoid in case mPageSize
+        // not fully initialized (when last page only one loaded)
+        int leadingNullPages = mLeadingNullCount / pageSize;
+
+        if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) {
+            return false;
+        }
+
+        Page<K, V> page = mPages.get(index - leadingNullPages);
+
+        return page != null && page != mPlaceholderPage;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount
+                + ", storage " + mStorageCount
+                + ", trailing " + getTrailingNullCount());
+
+        for (int i = 0; i < mPages.size(); i++) {
+            ret.append(" ").append(mPages.get(i));
+        }
+        return ret.toString();
+    }
+}
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
index deb51e9..c538cb6 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -42,6 +42,17 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public abstract class PositionalDataSource<Value> extends ContiguousDataSource<Integer, Value> {
+
+    /**
+     * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED.
+     *
+     * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED
+     * if difficult or undesired to compute.
+     */
+    public int countItems() {
+        return COUNT_UNDEFINED;
+    }
+
     @Nullable
     @Override
     List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) {
@@ -55,16 +66,7 @@
         return loadBefore(currentBeginIndex - 1, pageSize);
     }
 
-
-    /**
-     * Load initial data, starting after the passed position.
-     *
-     * @param position Index just before the data to be loaded.
-     * @param initialLoadSize Suggested number of items to load.
-     * @return List of initial items, representing data starting at position. Null if the
-     *         DataSource is no longer valid, and should not be queried again.
-     * @hide
-     */
+    /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @WorkerThread
     @Nullable
@@ -118,6 +120,9 @@
 
     @Override
     Integer getKey(int position, Value item) {
+        if (position < 0) {
+            return null;
+        }
         return position;
     }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
new file mode 100644
index 0000000..7e965a0
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
@@ -0,0 +1,64 @@
+/*
+ * 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.NonNull;
+import android.support.annotation.Nullable;
+
+class SnapshotPagedList<T> extends PagedList<T> {
+    private final boolean mContiguous;
+    private final Object mLastKey;
+
+    SnapshotPagedList(@NonNull PagedList<T> pagedList) {
+        super(pagedList.mStorage.snapshot(),
+                pagedList.mMainThreadExecutor,
+                pagedList.mBackgroundThreadExecutor,
+                pagedList.mConfig);
+        mContiguous = pagedList.isContiguous();
+        mLastKey = pagedList.getLastKey();
+    }
+
+    @Override
+    public boolean isImmutable() {
+        return true;
+    }
+
+    @Override
+    public boolean isDetached() {
+        return true;
+    }
+
+    @Override
+    boolean isContiguous() {
+        return mContiguous;
+    }
+
+    @Nullable
+    @Override
+    public Object getLastKey() {
+        return mLastKey;
+    }
+
+    @Override
+    void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
+            @NonNull Callback callback) {
+    }
+
+    @Override
+    void loadAroundInternal(int index) {
+    }
+}
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 36be423..61dead3 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
@@ -19,6 +19,7 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -92,7 +93,6 @@
      * @return Number of items this DataSource can provide. Must be <code>0</code> or greater.
      */
     @WorkerThread
-    @Override
     public abstract int countItems();
 
     @Override
@@ -118,7 +118,61 @@
     @WorkerThread
     public abstract List<Type> loadRange(int startPosition, int count);
 
-    final List<Type> loadRangeWrapper(int startPosition, int count) {
+    /**
+     * blocking, and splits pages
+     */
+    void loadRangeInitial(int startPosition, int count, int pageSize, int itemCount,
+            PageResult.Receiver<Integer, Type> receiver) {
+
+        if (itemCount == 0) {
+            // no data to load, just immediately return empty
+            receiver.onPageResult(new PageResult<>(
+                    PageResult.INIT, new Page<Integer, Type>(Collections.<Type>emptyList()),
+                    0, 0, startPosition));
+            return;
+        }
+
+
+        List<Type> list = loadRangeWrapper(startPosition, count);
+
+        count = Math.min(count, itemCount - startPosition);
+
+        if (list == null) {
+            // invalid data, pass to receiver
+            receiver.onPageResult(new PageResult<Integer, Type>(
+                    PageResult.INIT, null, 0, 0, startPosition));
+            return;
+        }
+
+        if (list.size() != count) {
+            throw new IllegalStateException("Invalid list, requested size: " + count
+                    + ", returned size: " + list.size());
+        }
+
+        // emit the results as multiple pages
+        int pageCount = (count + (pageSize - 1)) / pageSize;
+        for (int i = 0; i < pageCount; i++) {
+            int beginInclusive = i * pageSize;
+            int endExclusive = Math.min(count, (i + 1) * pageSize);
+
+            Page<Integer, Type> page = new Page<>(list.subList(beginInclusive, endExclusive));
+
+            int leadingNulls = startPosition + beginInclusive;
+            int trailingNulls = itemCount - leadingNulls - page.items.size();
+            receiver.onPageResult(new PageResult<>(
+                    PageResult.INIT, page, leadingNulls, trailingNulls, 0));
+        }
+    }
+
+    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;
+        receiver.postOnPageResult(new PageResult<>(
+                PageResult.TILE, page, startPosition, 0, 0));
+    }
+
+    private List<Type> loadRangeWrapper(int startPosition, int count) {
         if (isInvalid()) {
             return null;
         }
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 a2fc064..c45d029 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
@@ -16,214 +16,100 @@
 
 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.RestrictTo;
 import android.support.annotation.WorkerThread;
 
-import java.lang.ref.WeakReference;
-import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class TiledPagedList<T> extends PageArrayList<T> {
+class TiledPagedList<T> extends PagedList<T>
+        implements PagedStorage.Callback {
 
     private final TiledDataSource<T> mDataSource;
-    private final Executor mMainThreadExecutor;
-    private final Executor mBackgroundThreadExecutor;
-    private final Config mConfig;
 
-    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
-    private final List<T> mLoadingPlaceholder = new AbstractList<T>() {
+    @SuppressWarnings("unchecked")
+    private final PagedStorage<Integer, T> mKeyedStorage = (PagedStorage<Integer, T>) mStorage;
+
+    private final PageResult.Receiver<Integer, T> mReceiver =
+            new PageResult.Receiver<Integer, T>() {
+        @AnyThread
         @Override
-        public T get(int i) {
-            return null;
+        public void postOnPageResult(@NonNull final PageResult<Integer, T> pageResult) {
+            // NOTE: if we're already on main thread, this can delay page receive by a frame
+            mMainThreadExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    onPageResult(pageResult);
+                }
+            });
         }
 
+        @MainThread
         @Override
-        public int size() {
-            return 0;
+        public void onPageResult(@NonNull PageResult<Integer, T> pageResult) {
+            if (pageResult.page == null) {
+                detach();
+                return;
+            }
+
+            if (isDetached()) {
+                // No op, have detached
+                return;
+            }
+
+            if (mStorage.getPageCount() == 0) {
+                mKeyedStorage.init(
+                        pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
+                        pageResult.positionOffset, TiledPagedList.this);
+            } else {
+                mKeyedStorage.insertPage(pageResult.leadingNulls, pageResult.page,
+                        TiledPagedList.this);
+            }
         }
     };
 
-    private int mLastLoad = -1;
-
-    private AtomicBoolean mDetached = new AtomicBoolean(false);
-
-    private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
-
     @WorkerThread
     TiledPagedList(@NonNull TiledDataSource<T> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
-            Config config,
+            @NonNull Config config,
             int position) {
-        super(config.mPageSize, dataSource.countItems());
-
+        super(new PagedStorage<Integer, T>(),
+                mainThreadExecutor, backgroundThreadExecutor, config);
         mDataSource = dataSource;
-        mMainThreadExecutor = mainThreadExecutor;
-        mBackgroundThreadExecutor = backgroundThreadExecutor;
-        mConfig = config;
 
-        position = Math.min(Math.max(0, position), mCount);
+        final int pageSize = mConfig.mPageSize;
 
-        int firstPage = position / mPageSize;
-        List<T> firstPageData = dataSource.loadRangeWrapper(firstPage * mPageSize, mPageSize);
-        if (firstPageData != null) {
-            mPageIndexOffset = firstPage;
-            mPages.add(firstPageData);
-            mLastLoad = position;
-        } else {
-            detach();
-            return;
-        }
+        final int itemCount = mDataSource.countItems();
+        final int firstLoadSize = Math.min(itemCount,
+                (Math.max(mConfig.mInitialLoadSizeHint / pageSize, 2)) * pageSize);
+        final int firstLoadPosition = computeFirstLoadPosition(
+                position, firstLoadSize, pageSize, itemCount);
 
-        int secondPage = (position % mPageSize < mPageSize / 2) ? firstPage - 1 : firstPage + 1;
-        if (secondPage < 0 || secondPage > mMaxPageCount) {
-            // no second page to load
-            return;
-        }
-        List<T> secondPageData = dataSource.loadRangeWrapper(secondPage * mPageSize, mPageSize);
-        if (secondPageData != null) {
-            boolean before = secondPage < firstPage;
-            mPages.add(before ? 0 : 1, secondPageData);
-            if (before) {
-                mPageIndexOffset--;
-            }
-            return;
-        }
-        detach();
+        mDataSource.loadRangeInitial(firstLoadPosition, firstLoadSize, pageSize,
+                itemCount, mReceiver);
+    }
+
+    static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) {
+        int idealStart = position - firstLoadSize / 2;
+
+        int roundedPageStart = Math.round(idealStart / pageSize) * pageSize;
+
+        // minimum start position is 0
+        roundedPageStart = Math.max(0, roundedPageStart);
+
+        // maximum start pos is that which will encompass end of list
+        int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
+        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
+
+        return roundedPageStart;
     }
 
     @Override
-    public void loadAround(int index) {
-        mLastLoad = index;
-
-        int minimumPage = Math.max((index - mConfig.mPrefetchDistance) / mPageSize, 0);
-        int maximumPage = Math.min((index + mConfig.mPrefetchDistance) / mPageSize,
-                mMaxPageCount - 1);
-
-        if (minimumPage < mPageIndexOffset) {
-            for (int i = 0; i < mPageIndexOffset - minimumPage; i++) {
-                mPages.add(0, null);
-            }
-            mPageIndexOffset = minimumPage;
-        }
-        if (maximumPage >= mPageIndexOffset + mPages.size()) {
-            for (int i = mPages.size(); i <= maximumPage - mPageIndexOffset; i++) {
-                mPages.add(mPages.size(), null);
-            }
-        }
-        for (int i = minimumPage; i <= maximumPage; i++) {
-            scheduleLoadPage(i);
-        }
-    }
-
-    private void scheduleLoadPage(final int pageIndex) {
-        final int localPageIndex = pageIndex - mPageIndexOffset;
-
-        if (mPages.get(localPageIndex) != null) {
-            // page is present in list, and non-null - don't need to load
-            return;
-        }
-        mPages.set(localPageIndex, mLoadingPlaceholder);
-
-        mBackgroundThreadExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mDetached.get()) {
-                    return;
-                }
-                final List<T> data = mDataSource.loadRangeWrapper(
-                        pageIndex * mPageSize, mPageSize);
-                if (data != null) {
-                    mMainThreadExecutor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mDetached.get()) {
-                                return;
-                            }
-                            loadPageImpl(pageIndex, data);
-                        }
-                    });
-                } else {
-                    detach();
-                }
-            }
-        });
-
-    }
-
-    private void loadPageImpl(int pageIndex, List<T> data) {
-        int localPageIndex = pageIndex - mPageIndexOffset;
-
-        if (mPages.get(localPageIndex) != mLoadingPlaceholder) {
-            throw new IllegalStateException("Data inserted before requested.");
-        }
-        mPages.set(localPageIndex, data);
-        for (WeakReference<Callback> weakRef : mCallbacks) {
-            Callback callback = weakRef.get();
-            if (callback != null) {
-                callback.onChanged(pageIndex * mPageSize, data.size());
-            }
-        }
-    }
-
-    @Override
-    public boolean isImmutable() {
-        // TODO: consider counting loaded pages, return true if mLoadedPages == mMaxPageCount
-        // Note: could at some point want to support growing past max count, or grow dynamically
-        return isDetached();
-    }
-
-    @Override
-    public void addWeakCallback(@Nullable PagedList<T> previousSnapshot,
-            @NonNull Callback callback) {
-        PageArrayList<T> snapshot = (PageArrayList<T>) previousSnapshot;
-        if (snapshot != this && snapshot != null) {
-            // loop through each page and signal the callback for any pages that are present now,
-            // but not in the snapshot.
-            for (int i = 0; i < mPages.size(); i++) {
-                int pageIndex = i + mPageIndexOffset;
-                int pageCount = 0;
-                // count number of consecutive pages that were added since the snapshot...
-                while (pageCount < mPages.size()
-                        && hasPage(pageIndex + pageCount)
-                        && !snapshot.hasPage(pageIndex + pageCount)) {
-                    pageCount++;
-                }
-                // and signal them all at once to the callback
-                if (pageCount > 0) {
-                    callback.onChanged(pageIndex * mPageSize, mPageSize * pageCount);
-                    i += pageCount - 1;
-                }
-            }
-        }
-        mCallbacks.add(new WeakReference<>(callback));
-    }
-
-    @Override
-    public void removeWeakCallback(@NonNull Callback callback) {
-        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-            Callback currentCallback = mCallbacks.get(i).get();
-            if (currentCallback == null || currentCallback == callback) {
-                mCallbacks.remove(i);
-            }
-        }
-    }
-
-    @Override
-    public boolean isDetached() {
-        return mDetached.get();
-    }
-
-    @Override
-    public void detach() {
-        mDetached.set(true);
+    boolean isContiguous() {
+        return false;
     }
 
     @Nullable
@@ -231,4 +117,72 @@
     public Object getLastKey() {
         return mLastLoad;
     }
+
+    @Override
+    protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
+            @NonNull Callback callback) {
+        //noinspection UnnecessaryLocalVariable
+        final PagedStorage<?, T> snapshot = pagedListSnapshot.mStorage;
+
+        // loop through each page and signal the callback for any pages that are present now,
+        // but not in the snapshot.
+        final int pageSize = mConfig.mPageSize;
+        final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize;
+        final int pageCount = mStorage.getPageCount();
+        for (int i = 0; i < pageCount; i++) {
+            int pageIndex = i + leadingNullPages;
+            int updatedPages = 0;
+            // count number of consecutive pages that were added since the snapshot...
+            while (updatedPages < mStorage.getPageCount()
+                    && mStorage.hasPage(pageSize, pageIndex + updatedPages)
+                    && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) {
+                updatedPages++;
+            }
+            // and signal them all at once to the callback
+            if (updatedPages > 0) {
+                callback.onChanged(pageIndex * pageSize, pageSize * updatedPages);
+                i += updatedPages - 1;
+            }
+        }
+    }
+
+    @Override
+    protected void loadAroundInternal(int index) {
+        mStorage.allocatePlaceholders(index, mConfig.mPrefetchDistance, mConfig.mPageSize, this);
+    }
+
+    @Override
+    public void onInitialized(int count) {
+        notifyInserted(0, count);
+    }
+
+    @Override
+    public void onPagePrepended(int leadingNulls, int changed, int added) {
+        throw new IllegalStateException("Contiguous callback on TiledPagedList");
+    }
+
+    @Override
+    public void onPageAppended(int endPosition, int changed, int added) {
+        throw new IllegalStateException("Contiguous callback on TiledPagedList");
+    }
+
+    @Override
+    public void onPagePlaceholderInserted(final int pageIndex) {
+        // placeholder means initialize a load
+        mBackgroundThreadExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (isDetached()) {
+                    return;
+                }
+                final int pageSize = mConfig.mPageSize;
+                mDataSource.loadRange(pageIndex * pageSize, pageSize, mReceiver);
+            }
+        });
+    }
+
+    @Override
+    public void onPageInserted(int start, int count) {
+        notifyChanged(start, count);
+    }
 }
diff --git a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
index ee7ea6a..43f556a 100644
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
+++ b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.java
@@ -16,6 +16,7 @@
 
 package android.arch.paging;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.mockito.Mockito.mock;
@@ -109,6 +110,7 @@
 
     private void verifyRange(int start, int count, NullPaddedList<Item> actual) {
         if (mCounted) {
+            //noinspection UnnecessaryLocalVariable
             int expectedLeading = start;
             int expectedTrailing = ITEMS.size() - start - count;
             assertEquals(ITEMS.size(), actual.size());
@@ -132,6 +134,38 @@
         }
     }
 
+    @SuppressWarnings("SuspiciousSystemArraycopy")
+    private void verifyRange(int start, int count, PagedStorage<?, Item> actual) {
+        if (mCounted) {
+            Item[] expected = new Item[ITEMS.size()];
+            System.arraycopy(ITEMS.toArray(), start, expected, start, count);
+            assertArrayEquals(expected, actual.toArray());
+
+            //noinspection UnnecessaryLocalVariable
+            int expectedLeading = start;
+            int expectedTrailing = ITEMS.size() - start - count;
+            assertEquals(ITEMS.size(), actual.size());
+            assertEquals(ITEMS.size() - expectedLeading - expectedTrailing,
+                    actual.getStorageCount());
+            assertEquals(expectedLeading, actual.getLeadingNullCount());
+            assertEquals(expectedTrailing, actual.getTrailingNullCount());
+
+        } else {
+            Item[] expected = new Item[count];
+            System.arraycopy(ITEMS.toArray(), start, expected, 0, count);
+            assertArrayEquals(expected, actual.toArray());
+
+            assertEquals(count, actual.size());
+            assertEquals(actual.size(), actual.getStorageCount());
+            assertEquals(0, actual.getLeadingNullCount());
+            assertEquals(0, actual.getTrailingNullCount());
+        }
+    }
+
+    private void verifyRange(int start, int count, PagedList<Item> actual) {
+        verifyRange(start, count, actual.mStorage);
+    }
+
     private void verifyCallback(PagedList.Callback callback, int countedPosition,
             int uncountedPosition) {
         if (mCounted) {
@@ -154,7 +188,7 @@
     }
 
 
-    private ContiguousPagedList<Item> createCountedPagedList(
+    private ContiguousPagedList<Integer, Item> createCountedPagedList(
             PagedList.Config config, int initialPosition) {
         TestSource source = new TestSource();
         return new ContiguousPagedList<>(
@@ -163,7 +197,7 @@
                 initialPosition);
     }
 
-    private ContiguousPagedList<Item> createCountedPagedList(int initialPosition) {
+    private ContiguousPagedList<Integer, Item> createCountedPagedList(int initialPosition) {
         return createCountedPagedList(
                 new PagedList.Config.Builder()
                         .setInitialLoadSizeHint(40)
@@ -174,8 +208,14 @@
     }
 
     @Test
+    public void construct() {
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
+        verifyRange(0, 40, pagedList);
+    }
+
+    @Test
     public void append() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
         verifyRange(0, 40, pagedList);
@@ -192,7 +232,7 @@
 
     @Test
     public void prepend() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(80);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(80);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
         verifyRange(60, 40, pagedList);
@@ -208,7 +248,7 @@
 
     @Test
     public void outwards() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(50);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(50);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
         verifyRange(30, 40, pagedList);
@@ -231,7 +271,7 @@
 
     @Test
     public void multiAppend() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
         verifyRange(0, 40, pagedList);
@@ -248,7 +288,7 @@
 
     @Test
     public void distantPrefetch() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(
                 new PagedList.Config.Builder()
                         .setInitialLoadSizeHint(10)
                         .setPageSize(10)
@@ -274,7 +314,7 @@
 
     @Test
     public void appendCallbackAddedLate() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(0);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(0);
         verifyRange(0, 40, pagedList);
 
         pagedList.loadAround(35);
@@ -282,7 +322,7 @@
         verifyRange(0, 60, pagedList);
 
         // snapshot at 60 items
-        NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot();
+        PagedList<Item> snapshot = (PagedList<Item>) pagedList.snapshot();
         verifyRange(0, 60, snapshot);
 
 
@@ -300,7 +340,7 @@
 
     @Test
     public void prependCallbackAddedLate() {
-        ContiguousPagedList<Item> pagedList = createCountedPagedList(80);
+        ContiguousPagedList<Integer, Item> pagedList = createCountedPagedList(80);
         verifyRange(60, 40, pagedList);
 
         pagedList.loadAround(mCounted ? 65 : 5);
@@ -308,7 +348,7 @@
         verifyRange(40, 60, pagedList);
 
         // snapshot at 60 items
-        NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot();
+        PagedList<Item> snapshot = (PagedList<Item>) pagedList.snapshot();
         verifyRange(40, 60, snapshot);
 
 
diff --git a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
index 92ab3c5..0625694 100644
--- a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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 org.junit.Assert.assertEquals
@@ -5,6 +21,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
 
 @RunWith(JUnit4::class)
 class KeyedDataSourceTest {
@@ -156,6 +174,29 @@
         assertEquals(0, initialLoad.trailingNullCount)
     }
 
+    // ----- Other behavior -----
+
+    @Test
+    fun loadBefore() {
+        val dataSource = ItemDataSource()
+        @Suppress("UNCHECKED_CAST")
+        val receiver = Mockito.mock(PageResult.Receiver::class.java)
+                as PageResult.Receiver<Key, Item>
+
+        dataSource.loadBefore(5, ITEMS_BY_NAME_ID[5], 5, receiver)
+
+        @Suppress("UNCHECKED_CAST")
+        val argument = ArgumentCaptor.forClass(PageResult::class.java)
+                as ArgumentCaptor<PageResult<Key, Item>>
+        Mockito.verify(receiver).postOnPageResult(argument.capture())
+        Mockito.verifyNoMoreInteractions(receiver)
+
+        val observed = argument.value
+
+        assertEquals(PageResult.PREPEND, observed.type)
+        assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed.page.items)
+    }
+
     internal data class Key(val name: String, val id: Int)
 
     internal data class Item(
diff --git a/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java b/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java
deleted file mode 100644
index 135e640..0000000
--- a/paging/common/src/test/java/android/arch/paging/PageArrayListTest.java
+++ /dev/null
@@ -1,49 +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;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(JUnit4.class)
-public class PageArrayListTest {
-    @Test
-    public void simple() {
-        List<String> data = Arrays.asList("A", "B", "C", "D", "E", "F");
-        PageArrayList<String> list = new PageArrayList<>(2, data.size());
-
-        assertEquals(2, list.mPageSize);
-        assertEquals(data.size(), list.size());
-        assertEquals(3, list.mMaxPageCount);
-
-        for (int i = 0; i < data.size(); i++) {
-            assertEquals(null, list.get(i));
-        }
-        for (int i = 0; i < data.size(); i += list.mPageSize) {
-            list.mPages.add(data.subList(i, i + 2));
-        }
-        for (int i = 0; i < data.size(); i++) {
-            assertEquals(data.get(i), list.get(i));
-        }
-    }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
new file mode 100644
index 0000000..92b6c87
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
@@ -0,0 +1,409 @@
+/*
+ * 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 org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class PagedStorageTest {
+    private fun createPage(vararg strings: String): Page<Int, String> {
+        return Page(strings.asList())
+    }
+
+    @Test
+    fun construct() {
+        val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+        assertArrayEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
+        assertEquals(6, storage.size)
+    }
+
+    @Test
+    fun appendFill() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(2, createPage("a", "b"), 2)
+        storage.appendPage(createPage("c", "d"), callback)
+
+
+        assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+        verify(callback).onPageAppended(4, 2, 0)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun appendAdd() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(2, createPage("a", "b"), 0)
+        storage.appendPage(createPage("c", "d"), callback)
+
+        assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+        verify(callback).onPageAppended(4, 0, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun appendFillAdd() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+        // change 2 nulls into c, d
+        storage.appendPage(createPage("c", "d"), callback)
+
+        assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+        verify(callback).onPageAppended(4, 2, 0)
+        verifyNoMoreInteractions(callback)
+
+        // append e, f
+        storage.appendPage(createPage("e", "f"), callback)
+
+        assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
+        verify(callback).onPageAppended(6, 0, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun prependFill() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(2, createPage("c", "d"), 2)
+        storage.prependPage(createPage("a", "b"), callback)
+
+        assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+        verify(callback).onPagePrepended(0, 2, 0)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun prependAdd() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(0, createPage("c", "d"), 2)
+        storage.prependPage(createPage("a", "b"), callback)
+
+
+        assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+        verify(callback).onPagePrepended(0, 0, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun prependFillAdd() {
+        val callback = mock(PagedStorage.Callback::class.java)
+
+        val storage = PagedStorage(2, createPage("e", "f"), 2)
+
+        // change 2 nulls into c, d
+        storage.prependPage(createPage("c", "d"), callback)
+
+        assertArrayEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
+        verify(callback).onPagePrepended(0, 2, 0)
+        verifyNoMoreInteractions(callback)
+
+        // prepend a, b
+        storage.prependPage(createPage("a", "b"), callback)
+
+        assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
+        verify(callback).onPagePrepended(0, 0, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun isTiled_addend_smallerPageIsNotLast() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(0, createPage("a", "a"), 0)
+        assertTrue(storage.isTiled)
+
+        storage.appendPage(createPage("a", "a"), callback)
+        assertTrue(storage.isTiled)
+
+        storage.appendPage(createPage("a"), callback)
+        assertTrue(storage.isTiled)
+
+        // no matter what we append here, we're no longer tiled
+        storage.appendPage(createPage("a", "a"), callback)
+        assertFalse(storage.isTiled)
+    }
+
+    @Test
+    fun isTiled_append_growingSizeDisable() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(0, createPage("a", "a"), 0)
+        assertTrue(storage.isTiled)
+
+        // page size can't grow from append
+        storage.appendPage(createPage("a", "a", "a"), callback)
+        assertFalse(storage.isTiled)
+    }
+
+    @Test
+    fun isTiled_prepend_smallerPage() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(0, createPage("a"), 0)
+        assertTrue(storage.isTiled)
+
+        storage.prependPage(createPage("a", "a"), callback)
+        assertTrue(storage.isTiled)
+
+        storage.prependPage(createPage("a", "a"), callback)
+        assertTrue(storage.isTiled)
+
+        storage.prependPage(createPage("a"), callback)
+        assertFalse(storage.isTiled)
+    }
+
+    @Test
+    fun isTiled_prepend_smallerThanInitialPage() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(0, createPage("a", "a"), 0)
+        assertTrue(storage.isTiled)
+
+        storage.prependPage(createPage("a"), callback)
+        assertFalse(storage.isTiled)
+    }
+
+    @Test
+    fun get_tiled() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(1, createPage("a", "b"), 5)
+        assertTrue(storage.isTiled)
+
+        storage.appendPage(createPage("c", "d"), callback)
+        storage.appendPage(createPage("e", "f"), callback)
+
+        assertTrue(storage.isTiled)
+        assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+    }
+
+    @Test
+    fun get_nonTiled() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage(1, createPage("a"), 6)
+        assertTrue(storage.isTiled)
+
+        storage.appendPage(createPage("b", "c"), callback)
+        storage.appendPage(createPage("d", "e", "f"), callback)
+
+        assertFalse(storage.isTiled)
+        assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+    }
+
+    @Test
+    fun insertOne() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+        verify(callback).onInitialized(7)
+        verifyNoMoreInteractions(callback)
+
+        storage.insertPage(4, createPage("e", "f"), callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf(null, null, "c", "d", "e", "f", null), storage.toArray())
+        verify(callback).onPageInserted(4, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun insertThree() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+        verify(callback).onInitialized(7)
+        verifyNoMoreInteractions(callback)
+
+        // first, insert 1st page
+        storage.insertPage(0, createPage("a", "b"), callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf("a", "b", "c", "d", null, null, null), storage.toArray())
+        verify(callback).onPageInserted(0, 2)
+        verifyNoMoreInteractions(callback)
+
+        // then 3rd page
+        storage.insertPage(4, createPage("e", "f"), callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null), storage.toArray())
+        verify(callback).onPageInserted(4, 2)
+        verifyNoMoreInteractions(callback)
+
+        // then last, small page
+        storage.insertPage(6, createPage("g"), callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", "g"), storage.toArray())
+        verify(callback).onPageInserted(6, 1)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test
+    fun insertLastFirst() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(6, createPage("g"), 0, 0, callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf(null, null, null, null, null, null, "g"), storage.toArray())
+        verify(callback).onInitialized(7)
+        verifyNoMoreInteractions(callback)
+
+        // insert 1st page
+        storage.insertPage(0, createPage("a", "b"), callback)
+
+        assertEquals(7, storage.size)
+        assertArrayEquals(arrayOf("a", "b", null, null, null, null, "g"), storage.toArray())
+        verify(callback).onPageInserted(0, 2)
+        verifyNoMoreInteractions(callback)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun insertFailure_decreaseLast() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c", "d"), 0, 0, callback)
+
+        // should throw, page too small
+        storage.insertPage(0, createPage("a"), callback)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun insertFailure_increase() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(0, createPage("a", "b"), 3, 0, callback)
+
+        // should throw, page too big
+        storage.insertPage(2, createPage("c", "d", "e"), callback)
+    }
+
+    @Test
+    fun allocatePlaceholders_simple() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c"), 2, 0, callback)
+
+        verify(callback).onInitialized(5)
+
+        storage.allocatePlaceholders(2, 1, 1, callback)
+
+        verify(callback).onPagePlaceholderInserted(1)
+        verify(callback).onPagePlaceholderInserted(3)
+        verifyNoMoreInteractions(callback)
+
+        assertArrayEquals(arrayOf(null, null, "c", null, null), storage.toArray())
+    }
+
+    @Test
+    fun allocatePlaceholders_adoptPageSize() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(4, createPage("e"), 0, 0, callback)
+
+        verify(callback).onInitialized(5)
+
+        storage.allocatePlaceholders(0, 2, 2, callback)
+
+        verify(callback).onPagePlaceholderInserted(0)
+        verify(callback).onPagePlaceholderInserted(1)
+        verifyNoMoreInteractions(callback)
+
+        assertArrayEquals(arrayOf(null, null, null, null, "e"), storage.toArray())
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun allocatePlaceholders_cannotShrinkPageSize() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(4, createPage("e", "f"), 0, 0, callback)
+
+        verify(callback).onInitialized(6)
+
+        storage.allocatePlaceholders(0, 2, 1, callback)
+    }
+
+
+    @Test(expected = IllegalArgumentException::class)
+    fun allocatePlaceholders_cannotAdoptPageSize() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c", "d"), 2, 0, callback)
+
+        verify(callback).onInitialized(6)
+
+        storage.allocatePlaceholders(0, 2, 3, callback)
+    }
+
+    @Test
+    fun get_placeholdersMulti() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+        assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+
+        storage.allocatePlaceholders(0, 10, 2, callback)
+
+        // allocating placeholders shouldn't affect result of get
+        assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+    }
+
+    @Test
+    fun hasPage() {
+        val callback = mock(PagedStorage.Callback::class.java)
+        val storage = PagedStorage<Int, String>()
+
+        storage.init(4, createPage("e"), 0, 0, callback)
+
+        assertFalse(storage.hasPage(1, 0))
+        assertFalse(storage.hasPage(1, 1))
+        assertFalse(storage.hasPage(1, 2))
+        assertFalse(storage.hasPage(1, 3))
+        assertTrue(storage.hasPage(1, 4))
+
+        assertFalse(storage.hasPage(2, 0))
+        assertFalse(storage.hasPage(2, 1))
+        assertTrue(storage.hasPage(2, 2))
+    }
+}
diff --git a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
new file mode 100644
index 0000000..593ccd0
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
@@ -0,0 +1,46 @@
+package android.arch.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.Collections
+
+
+@RunWith(JUnit4::class)
+class TiledDataSourceTest {
+    @Test
+    fun loadInitialEmpty() {
+        @Suppress("UNCHECKED_CAST")
+        val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Int, String>
+        val dataSource = EmptyDataSource()
+        dataSource.loadRangeInitial(0, 0, 1, 0, receiver)
+
+        @Suppress("UNCHECKED_CAST")
+        val argument = ArgumentCaptor.forClass(PageResult::class.java)
+                as ArgumentCaptor<PageResult<Int, String>>
+        verify(receiver).onPageResult(argument.capture())
+        verifyNoMoreInteractions(receiver)
+
+        val observed = argument.value
+
+        assertEquals(PageResult.INIT, observed.type)
+        assertEquals(Collections.EMPTY_LIST, observed.page.items)
+    }
+
+    class EmptyDataSource : TiledDataSource<String>() {
+        override fun countItems(): Int {
+            return 0
+        }
+
+        override fun loadRange(startPosition: Int, count: Int): List<String> {
+            @Suppress("UNCHECKED_CAST")
+            return Collections.EMPTY_LIST as List<String>
+        }
+    }
+}
\ No newline at end of file
diff --git a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
index 4ad02e1..22bfd1f 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
+++ b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.java
@@ -78,75 +78,107 @@
         }
     }
 
-    private void verifyRange(PageArrayList<Item> list, Integer... loadedPages) {
+    private void verifyRange(List<Item> list, Integer... loadedPages) {
         List<Integer> loadedPageList = Arrays.asList(loadedPages);
         assertEquals(ITEMS.size(), list.size());
         for (int i = 0; i < list.size(); i++) {
             if (loadedPageList.contains(i / PAGE_SIZE)) {
-                assertSame(ITEMS.get(i), list.get(i));
+                assertSame("Index " + i, ITEMS.get(i), list.get(i));
             } else {
-                assertEquals(null, list.get(i));
+                assertEquals("Index " + i, null, list.get(i));
             }
         }
     }
-    private TiledPagedList<Item> createTiledPagedList(int loadPosition) {
-        return createTiledPagedList(loadPosition, PAGE_SIZE);
+    private TiledPagedList<Item> createTiledPagedList(int loadPosition, int initPages) {
+        return createTiledPagedList(loadPosition, initPages, PAGE_SIZE);
     }
 
-    private TiledPagedList<Item> createTiledPagedList(int loadPosition, int prefetchDistance) {
+    private TiledPagedList<Item> createTiledPagedList(int loadPosition, int initPages,
+            int prefetchDistance) {
         TestTiledSource source = new TestTiledSource();
         return new TiledPagedList<>(
                 source, mMainThread, mBackgroundThread,
                 new PagedList.Config.Builder()
                         .setPageSize(PAGE_SIZE)
+                        .setInitialLoadSizeHint(PAGE_SIZE * initPages)
                         .setPrefetchDistance(prefetchDistance)
                         .build(),
                 loadPosition);
     }
 
     @Test
-    public void initialLoad() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(0);
-        verifyRange(pagedList, 0);
+    public void computeFirstLoadPosition_zero() {
+        assertEquals(0, TiledPagedList.computeFirstLoadPosition(0, 30, 10, 100));
+    }
+
+    @Test
+    public void computeFirstLoadPosition_requestedPositionIncluded() {
+        assertEquals(0, TiledPagedList.computeFirstLoadPosition(10, 10, 10, 100));
+    }
+
+    @Test
+    public void computeFirstLoadPosition_endAdjusted() {
+        assertEquals(70, TiledPagedList.computeFirstLoadPosition(99, 30, 10, 100));
+    }
+
+    @Test
+    public void initialLoad_onePage() {
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
+        verifyRange(pagedList, 0, 1);
+    }
+
+    @Test
+    public void initialLoad_onePageOffset() {
+        TiledPagedList<Item> pagedList = createTiledPagedList(10, 1);
+        verifyRange(pagedList, 0, 1);
+    }
+
+    @Test
+    public void initialLoad_full() {
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 100);
+        verifyRange(pagedList, 0, 1, 2, 3, 4);
     }
 
     @Test
     public void initialLoad_end() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(44);
+        TiledPagedList<Item> pagedList = createTiledPagedList(44, 2);
         verifyRange(pagedList, 3, 4);
     }
 
     @Test
     public void initialLoad_multiple() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(9);
+        TiledPagedList<Item> pagedList = createTiledPagedList(9, 2);
         verifyRange(pagedList, 0, 1);
     }
 
     @Test
     public void initialLoad_offset() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(41);
+        TiledPagedList<Item> pagedList = createTiledPagedList(41, 2);
         verifyRange(pagedList, 3, 4);
     }
 
     @Test
     public void append() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(0);
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
-        verifyRange(pagedList, 0);
+        verifyRange(pagedList, 0, 1);
         verifyZeroInteractions(callback);
 
-        pagedList.loadAround(5);
-        drain();
+        pagedList.loadAround(15);
 
         verifyRange(pagedList, 0, 1);
-        verify(callback).onChanged(10, 10);
+
+        drain();
+
+        verifyRange(pagedList, 0, 1, 2);
+        verify(callback).onChanged(20, 10);
         verifyNoMoreInteractions(callback);
     }
 
     @Test
     public void prepend() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(44);
+        TiledPagedList<Item> pagedList = createTiledPagedList(44, 2);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
         verifyRange(pagedList, 3, 4);
@@ -162,16 +194,16 @@
 
     @Test
     public void loadWithGap() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(0);
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
-        verifyRange(pagedList, 0);
+        verifyRange(pagedList, 0, 1);
         verifyZeroInteractions(callback);
 
         pagedList.loadAround(44);
         drain();
 
-        verifyRange(pagedList, 0, 3, 4);
+        verifyRange(pagedList, 0, 1, 3, 4);
         verify(callback).onChanged(30, 10);
         verify(callback).onChanged(40, 5);
         verifyNoMoreInteractions(callback);
@@ -179,58 +211,56 @@
 
     @Test
     public void tinyPrefetchTest() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1);
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1, 1);
         PagedList.Callback callback = mock(PagedList.Callback.class);
         pagedList.addWeakCallback(null, callback);
-        verifyRange(pagedList, 0); // just 4 loaded
+        verifyRange(pagedList, 0, 1);
         verifyZeroInteractions(callback);
 
-        pagedList.loadAround(23);
+        pagedList.loadAround(33);
         drain();
 
-        verifyRange(pagedList, 0, 2);
-        verify(callback).onChanged(20, 10);
+        verifyRange(pagedList, 0, 1, 3);
+        verify(callback).onChanged(30, 10);
         verifyNoMoreInteractions(callback);
 
         pagedList.loadAround(44);
         drain();
 
-        verifyRange(pagedList, 0, 2, 4);
+        verifyRange(pagedList, 0, 1, 3, 4);
         verify(callback).onChanged(40, 5);
         verifyNoMoreInteractions(callback);
     }
 
     @Test
     public void appendCallbackAddedLate() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(0, 0);
-        verifyRange(pagedList, 0);
-
-        pagedList.loadAround(15);
-        drain();
+        TiledPagedList<Item> pagedList = createTiledPagedList(0, 1, 0);
         verifyRange(pagedList, 0, 1);
 
-        // snapshot at 20 items
-        PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot();
-        verifyRange(snapshot, 0, 1);
-
-
         pagedList.loadAround(25);
-        pagedList.loadAround(35);
         drain();
-        verifyRange(pagedList, 0, 1, 2, 3);
-        verifyRange(snapshot, 0, 1);
+        verifyRange(pagedList, 0, 1, 2);
+
+        // snapshot at 30 items
+        List<Item> snapshot = pagedList.snapshot();
+        verifyRange(snapshot, 0, 1, 2);
+
+        pagedList.loadAround(35);
+        pagedList.loadAround(44);
+        drain();
+        verifyRange(pagedList, 0, 1, 2, 3, 4);
+        verifyRange(snapshot, 0, 1, 2);
 
         PagedList.Callback callback = mock(
                 PagedList.Callback.class);
         pagedList.addWeakCallback(snapshot, callback);
-        verify(callback).onChanged(20, 20);
+        verify(callback).onChanged(30, 20);
         verifyNoMoreInteractions(callback);
     }
 
-
     @Test
     public void prependCallbackAddedLate() {
-        TiledPagedList<Item> pagedList = createTiledPagedList(44, 0);
+        TiledPagedList<Item> pagedList = createTiledPagedList(44, 2, 0);
         verifyRange(pagedList, 3, 4);
 
         pagedList.loadAround(25);
@@ -238,10 +268,9 @@
         verifyRange(pagedList, 2, 3, 4);
 
         // snapshot at 30 items
-        PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot();
+        List<Item> snapshot = pagedList.snapshot();
         verifyRange(snapshot, 2, 3, 4);
 
-
         pagedList.loadAround(15);
         pagedList.loadAround(5);
         drain();
@@ -272,10 +301,11 @@
 
         assertTrue(pagedList.isContiguous());
 
-        ContiguousPagedList<Item> contiguousPagedList = (ContiguousPagedList<Item>) pagedList;
-        assertEquals(0, contiguousPagedList.getLeadingNullCount());
-        assertEquals(PAGE_SIZE, contiguousPagedList.mList.size());
-        assertEquals(0, contiguousPagedList.getTrailingNullCount());
+        ContiguousPagedList<Integer, Item> contiguousPagedList =
+                (ContiguousPagedList<Integer, Item>) pagedList;
+        assertEquals(0, contiguousPagedList.mStorage.getLeadingNullCount());
+        assertEquals(PAGE_SIZE, contiguousPagedList.mStorage.getStorageCount());
+        assertEquals(0, contiguousPagedList.mStorage.getTrailingNullCount());
     }
 
     private void drain() {
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
similarity index 61%
rename from paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java
rename to paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
index 4f221b3..8cb9224 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/ContiguousDiffHelperTest.java
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.java
@@ -16,6 +16,9 @@
 
 package android.arch.paging;
 
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,11 +32,12 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
+
+import java.util.Arrays;
 
 @SmallTest
 @RunWith(JUnit4.class)
-public class ContiguousDiffHelperTest {
+public class PagedStorageDiffHelperTest {
     private interface CallbackValidator {
         void validate(ListUpdateCallback callback);
     }
@@ -51,13 +55,18 @@
         }
     };
 
-    private void validateTwoListDiff(StringPagedList oldList, StringPagedList newList,
-            CallbackValidator callbackValidator) {
-        DiffUtil.DiffResult diffResult = ContiguousDiffHelper.computeDiff(oldList, newList,
-                DIFF_CALLBACK, false);
+    public static Page<Integer, String> createPage(String... items) {
+        return new Page<>(Arrays.asList(items));
+    }
 
-        ListUpdateCallback listUpdateCallback = Mockito.mock(ListUpdateCallback.class);
-        ContiguousDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult);
+    private static void validateTwoListDiff(PagedStorage<?, String> oldList,
+            PagedStorage<?, String> newList,
+            CallbackValidator callbackValidator) {
+        DiffUtil.DiffResult diffResult = PagedStorageDiffHelper.computeDiff(
+                oldList, newList, DIFF_CALLBACK);
+
+        ListUpdateCallback listUpdateCallback = mock(ListUpdateCallback.class);
+        PagedStorageDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult);
 
         callbackValidator.validate(listUpdateCallback);
     }
@@ -65,8 +74,35 @@
     @Test
     public void sameListNoUpdates() {
         validateTwoListDiff(
-                new StringPagedList(5, 5, "a", "b", "c"),
-                new StringPagedList(5, 5, "a", "b", "c"),
+                new PagedStorage<>(5, createPage("a", "b", "c"), 5),
+                new PagedStorage<>(5, createPage("a", "b", "c"), 5),
+                new CallbackValidator() {
+                    @Override
+                    public void validate(ListUpdateCallback callback) {
+                        verifyZeroInteractions(callback);
+                    }
+                }
+        );
+    }
+
+    @Test
+    public void sameListNoUpdatesPlaceholder() {
+        PagedStorage<Integer, String> storageNoPlaceholder =
+                new PagedStorage<>(0, createPage("a", "b", "c"), 10);
+
+        PagedStorage<Integer, String> storageWithPlaceholder =
+                new PagedStorage<>(0, createPage("a", "b", "c"), 10);
+        storageWithPlaceholder.allocatePlaceholders(3, 0, 3,
+                /* ignored */ mock(PagedStorage.Callback.class));
+
+        // even though one has placeholders, and null counts are different...
+        assertEquals(10, storageNoPlaceholder.getTrailingNullCount());
+        assertEquals(7, storageWithPlaceholder.getTrailingNullCount());
+
+        // ... should be no interactions, since content still same
+        validateTwoListDiff(
+                storageNoPlaceholder,
+                storageWithPlaceholder,
                 new CallbackValidator() {
                     @Override
                     public void validate(ListUpdateCallback callback) {
@@ -79,8 +115,8 @@
     @Test
     public void appendFill() {
         validateTwoListDiff(
-                new StringPagedList(5, 5, "a", "b"),
-                new StringPagedList(5, 4, "a", "b", "c"),
+                new PagedStorage<>(5, createPage("a", "b"), 5),
+                new PagedStorage<>(5, createPage("a", "b", "c"), 4),
                 new CallbackValidator() {
                     @Override
                     public void validate(ListUpdateCallback callback) {
@@ -96,8 +132,8 @@
     @Test
     public void prependFill() {
         validateTwoListDiff(
-                new StringPagedList(5, 5, "b", "c"),
-                new StringPagedList(4, 5, "a", "b", "c"),
+                new PagedStorage<>(5, createPage("b", "c"), 5),
+                new PagedStorage<>(4, createPage("a", "b", "c"), 5),
                 new CallbackValidator() {
                     @Override
                     public void validate(ListUpdateCallback callback) {
@@ -113,8 +149,8 @@
     @Test
     public void change() {
         validateTwoListDiff(
-                new StringPagedList(5, 5, "a1", "b1", "c1"),
-                new StringPagedList(5, 5, "a2", "b1", "c2"),
+                new PagedStorage<>(5, createPage("a1", "b1", "c1"), 5),
+                new PagedStorage<>(5, createPage("a2", "b1", "c2"), 5),
                 new CallbackValidator() {
                     @Override
                     public void validate(ListUpdateCallback callback) {
@@ -125,4 +161,5 @@
                 }
         );
     }
+
 }
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
index 5318d38..880d5e9 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.java
@@ -16,10 +16,60 @@
 
 package android.arch.paging;
 
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
 import java.util.Arrays;
 
-public class StringPagedList extends NullPaddedList<String> {
-    public StringPagedList(int leadingNulls, int trailingNulls, String... items) {
-        super(leadingNulls, Arrays.asList(items), trailingNulls);
+public class StringPagedList extends PagedList<String> implements PagedStorage.Callback {
+    StringPagedList(int leadingNulls, int trailingNulls, String... items) {
+        super(new PagedStorage<Integer, String>(),
+                null, null, null);
+        PagedStorage<Integer, String> keyedStorage = (PagedStorage<Integer, String>) mStorage;
+        keyedStorage.init(leadingNulls,
+                new Page<Integer, String>(null, Arrays.asList(items), null),
+                trailingNulls,
+                0,
+                this);
+    }
+
+    @Override
+    boolean isContiguous() {
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Object getLastKey() {
+        return null;
+    }
+
+    @Override
+    protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<String> storageSnapshot,
+            @NonNull Callback callback) {
+    }
+
+    @Override
+    protected void loadAroundInternal(int index) {
+    }
+
+    @Override
+    public void onInitialized(int count) {
+    }
+
+    @Override
+    public void onPagePrepended(int leadingNulls, int changed, int added) {
+    }
+
+    @Override
+    public void onPageAppended(int endPosition, int changed, int added) {
+    }
+
+    @Override
+    public void onPagePlaceholderInserted(int pageIndex) {
+    }
+
+    @Override
+    public void onPageInserted(int start, int count) {
     }
 }
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 c7b61d9..0007a2e 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
@@ -25,8 +25,6 @@
 import android.support.v7.util.ListUpdateCallback;
 import android.support.v7.widget.RecyclerView;
 
-import java.util.List;
-
 /**
  * Helper object for mapping a {@link PagedList} into a
  * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}.
@@ -123,12 +121,10 @@
     private final ListUpdateCallback mUpdateCallback;
     private final ListAdapterConfig<T> mConfig;
 
-    // true if our listener is detached from mList, because it's been snapshotted
-    private boolean mUpdateScheduled;
-
     private boolean mIsContiguous;
 
-    private PagedList<T> mList;
+    private PagedList<T> mPagedList;
+    private PagedList<T> mSnapshot;
 
     // Max generation of currently scheduled runnable
     private int mMaxScheduledGeneration;
@@ -182,12 +178,17 @@
     @SuppressWarnings("WeakerAccess")
     @Nullable
     public T getItem(int index) {
-        if (mList == null) {
-            throw new IndexOutOfBoundsException("Item count is zero, getItem() call is invalid");
+        if (mPagedList == null) {
+            if (mSnapshot == null) {
+                throw new IndexOutOfBoundsException(
+                        "Item count is zero, getItem() call is invalid");
+            } else {
+                return mSnapshot.get(index);
+            }
         }
 
-        mList.loadAround(index);
-        return mList.get(index);
+        mPagedList.loadAround(index);
+        return mPagedList.get(index);
     }
 
     /**
@@ -198,7 +199,11 @@
      */
     @SuppressWarnings("WeakerAccess")
     public int getItemCount() {
-        return mList == null ? 0 : mList.size();
+        if (mPagedList != null) {
+            return mPagedList.size();
+        }
+
+        return mSnapshot == null ? 0 : mSnapshot.size();
     }
 
     /**
@@ -212,7 +217,7 @@
      */
     public void setList(final PagedList<T> pagedList) {
         if (pagedList != null) {
-            if (mList == null) {
+            if (mPagedList == null && mSnapshot == null) {
                 mIsContiguous = pagedList.isContiguous();
             } else {
                 if (pagedList.isContiguous() != mIsContiguous) {
@@ -222,7 +227,7 @@
             }
         }
 
-        if (pagedList == mList) {
+        if (pagedList == mPagedList) {
             // nothing to do
             return;
         }
@@ -231,49 +236,51 @@
         final int runGeneration = ++mMaxScheduledGeneration;
 
         if (pagedList == null) {
-            mUpdateCallback.onRemoved(0, mList.size());
-            mList.removeWeakCallback(mPagedListCallback);
-            mList = null;
+            mUpdateCallback.onRemoved(0, getItemCount());
+            if (mPagedList != null) {
+                mPagedList.removeWeakCallback(mPagedListCallback);
+                mPagedList = null;
+            } else if (mSnapshot != null) {
+                mSnapshot = null;
+            }
             return;
         }
 
-        if (mList == null) {
+        if (mPagedList == null && mSnapshot == null) {
             // fast simple first insert
             mUpdateCallback.onInserted(0, pagedList.size());
-            mList = pagedList;
+            mPagedList = pagedList;
             pagedList.addWeakCallback(null, mPagedListCallback);
             return;
         }
 
-        if (!mList.isImmutable()) {
+        if (mPagedList != null) {
             // first update scheduled on this list, so capture mPages as a snapshot, removing
             // callbacks so we don't have resolve updates against a moving target
-            mList.removeWeakCallback(mPagedListCallback);
-            mList = (PagedList<T>) mList.snapshot();
+            mPagedList.removeWeakCallback(mPagedListCallback);
+            mSnapshot = (PagedList<T>) mPagedList.snapshot();
+            mPagedList = null;
         }
 
-        final PagedList<T> oldSnapshot = mList;
-        final List<T> newSnapshot = pagedList.snapshot();
-        mUpdateScheduled = true;
+        if (mSnapshot == null || mPagedList != null) {
+            throw new IllegalStateException("must be in snapshot state to diff");
+        }
+
+        final PagedList<T> oldSnapshot = mSnapshot;
+        final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
         mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
             @Override
             public void run() {
                 final DiffUtil.DiffResult result;
-                if (mIsContiguous) {
-                    result = ContiguousDiffHelper.computeDiff(
-                            (NullPaddedList<T>) oldSnapshot, (NullPaddedList<T>) newSnapshot,
-                            mConfig.getDiffCallback(), true);
-                } else {
-                    result = SparseDiffHelper.computeDiff(
-                            (PageArrayList<T>) oldSnapshot, (PageArrayList<T>) newSnapshot,
-                            mConfig.getDiffCallback(), true);
-                }
+                result = PagedStorageDiffHelper.computeDiff(
+                        oldSnapshot.mStorage,
+                        newSnapshot.mStorage,
+                        mConfig.getDiffCallback());
 
                 mConfig.getMainThreadExecutor().execute(new Runnable() {
                     @Override
                     public void run() {
                         if (mMaxScheduledGeneration == runGeneration) {
-                            mUpdateScheduled = false;
                             latchPagedList(pagedList, newSnapshot, result);
                         }
                     }
@@ -283,16 +290,17 @@
     }
 
     private void latchPagedList(
-            PagedList<T> newList, List<T> diffSnapshot,
+            PagedList<T> newList, PagedList<T> diffSnapshot,
             DiffUtil.DiffResult diffResult) {
-        if (mIsContiguous) {
-            ContiguousDiffHelper.dispatchDiff(mUpdateCallback,
-                    (NullPaddedList<T>) mList, (ContiguousPagedList<T>) newList, diffResult);
-        } else {
-            SparseDiffHelper.dispatchDiff(mUpdateCallback, diffResult);
+        if (mSnapshot == null || mPagedList != null) {
+            throw new IllegalStateException("must be in snapshot state to apply diff");
         }
-        mList = newList;
-        newList.addWeakCallback((PagedList<T>) diffSnapshot, mPagedListCallback);
+
+        PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
+                mSnapshot.mStorage, newList.mStorage, diffResult);
+        mPagedList = newList;
+        mSnapshot = null;
+        newList.addWeakCallback(diffSnapshot, mPagedListCallback);
     }
 
     /**
@@ -307,6 +315,9 @@
     @SuppressWarnings("WeakerAccess")
     @Nullable
     public PagedList<T> getCurrentList() {
-        return mList;
+        if (mSnapshot != null) {
+            return mSnapshot;
+        }
+        return mPagedList;
     }
 }
diff --git a/paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
similarity index 74%
rename from paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java
rename to paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
index 7dd194b..6fc7039 100644
--- a/paging/runtime/src/main/java/android/arch/paging/ContiguousDiffHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
@@ -16,36 +16,31 @@
 
 package android.arch.paging;
 
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
 import android.support.v7.recyclerview.extensions.DiffCallback;
 import android.support.v7.util.DiffUtil;
 import android.support.v7.util.ListUpdateCallback;
 
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ContiguousDiffHelper {
-    private ContiguousDiffHelper() {
+class PagedStorageDiffHelper {
+    private PagedStorageDiffHelper() {
     }
 
-    @NonNull
     static <T> DiffUtil.DiffResult computeDiff(
-            final NullPaddedList<T> oldList, final NullPaddedList<T> newList,
-            final DiffCallback<T> diffCallback, boolean detectMoves) {
+            final PagedStorage<?, T> oldList,
+            final PagedStorage<?, T> newList,
+            final DiffCallback<T> diffCallback) {
+        final int oldOffset = oldList.computeLeadingNulls();
+        final int newOffset = newList.computeLeadingNulls();
 
-        if (!oldList.isImmutable()) {
-            throw new IllegalArgumentException("list must be immutable to safely perform diff");
-        }
-        if (!newList.isImmutable()) {
-            throw new IllegalArgumentException("list must be immutable to safely perform diff");
-        }
+        final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls();
+        final int newSize = newList.size() - newOffset - newList.computeTrailingNulls();
+
         return DiffUtil.calculateDiff(new DiffUtil.Callback() {
             @Nullable
             @Override
             public Object getChangePayload(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.mList.get(oldItemPosition);
-                T newItem = newList.mList.get(newItemPosition);
+                T oldItem = oldList.get(oldItemPosition + oldOffset);
+                T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
                 if (oldItem == null || newItem == null) {
                     return null;
                 }
@@ -54,21 +49,22 @@
 
             @Override
             public int getOldListSize() {
-                return oldList.mList.size();
+                return oldSize;
             }
 
             @Override
             public int getNewListSize() {
-                return newList.mList.size();
+                return newSize;
             }
 
             @Override
             public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.mList.get(oldItemPosition);
-                T newItem = newList.mList.get(newItemPosition);
+                T oldItem = oldList.get(oldItemPosition + oldOffset);
+                T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
                 if (oldItem == newItem) {
                     return true;
                 }
+                //noinspection SimplifiableIfStatement
                 if (oldItem == null || newItem == null) {
                     return false;
                 }
@@ -77,18 +73,19 @@
 
             @Override
             public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.mList.get(oldItemPosition);
-                T newItem = newList.mList.get(newItemPosition);
+                T oldItem = oldList.get(oldItemPosition + oldOffset);
+                T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
                 if (oldItem == newItem) {
                     return true;
                 }
+                //noinspection SimplifiableIfStatement
                 if (oldItem == null || newItem == null) {
                     return false;
                 }
 
                 return diffCallback.areContentsTheSame(oldItem, newItem);
             }
-        }, detectMoves);
+        }, true);
     }
 
     private static class OffsettingListUpdateCallback implements ListUpdateCallback {
@@ -134,21 +131,25 @@
      * immediately after dispatching this diff.
      */
     static <T> void dispatchDiff(ListUpdateCallback callback,
-            final NullPaddedList<T> oldList, final NullPaddedList<T> newList,
+            final PagedStorage<?, T> oldList,
+            final PagedStorage<?, T> newList,
             final DiffUtil.DiffResult diffResult) {
 
-        if (oldList.getLeadingNullCount() == 0
-                && oldList.getTrailingNullCount() == 0
-                && newList.getLeadingNullCount() == 0
-                && newList.getTrailingNullCount() == 0) {
+        final int trailingOld = oldList.computeTrailingNulls();
+        final int trailingNew = newList.computeTrailingNulls();
+        final int leadingOld = oldList.computeLeadingNulls();
+        final int leadingNew = newList.computeLeadingNulls();
+
+        if (trailingOld == 0
+                && trailingNew == 0
+                && leadingOld == 0
+                && leadingNew == 0) {
             // Simple case, dispatch & return
             diffResult.dispatchUpdatesTo(callback);
             return;
         }
 
         // First, remove or insert trailing nulls
-        final int trailingOld = oldList.getTrailingNullCount();
-        final int trailingNew = newList.getTrailingNullCount();
         if (trailingOld > trailingNew) {
             int count = trailingOld - trailingNew;
             callback.onRemoved(oldList.size() - count, count);
@@ -157,8 +158,6 @@
         }
 
         // Second, remove or insert leading nulls
-        final int leadingOld = oldList.getLeadingNullCount();
-        final int leadingNew = newList.getLeadingNullCount();
         if (leadingOld > leadingNew) {
             callback.onRemoved(0, leadingOld - leadingNew);
         } else if (leadingOld < leadingNew) {
diff --git a/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java
deleted file mode 100644
index fe47897..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/SparseDiffHelper.java
+++ /dev/null
@@ -1,99 +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;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.v7.recyclerview.extensions.DiffCallback;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.util.ListUpdateCallback;
-
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class SparseDiffHelper {
-    private SparseDiffHelper() {
-    }
-
-    @NonNull
-    static <T> DiffUtil.DiffResult computeDiff(
-            final PageArrayList<T> oldList, final PageArrayList<T> newList,
-            final DiffCallback<T> diffCallback, boolean detectMoves) {
-
-        if (!oldList.isImmutable()) {
-            throw new IllegalArgumentException("list must be immutable to safely perform diff");
-        }
-        if (!newList.isImmutable()) {
-            throw new IllegalArgumentException("list must be immutable to safely perform diff");
-        }
-        return DiffUtil.calculateDiff(new DiffUtil.Callback() {
-            @Nullable
-            @Override
-            public Object getChangePayload(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.get(oldItemPosition);
-                T newItem = newList.get(newItemPosition);
-                if (oldItem == null || newItem == null) {
-                    return null;
-                }
-                return diffCallback.getChangePayload(oldItem, newItem);
-            }
-
-            @Override
-            public int getOldListSize() {
-                return oldList.size();
-            }
-
-            @Override
-            public int getNewListSize() {
-                return newList.size();
-            }
-
-            @Override
-            public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.get(oldItemPosition);
-                T newItem = newList.get(newItemPosition);
-                if (oldItem == newItem) {
-                    return true;
-                }
-                if (oldItem == null || newItem == null) {
-                    return false;
-                }
-                return diffCallback.areItemsTheSame(oldItem, newItem);
-            }
-
-            @Override
-            public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
-                T oldItem = oldList.get(oldItemPosition);
-                T newItem = newList.get(newItemPosition);
-                if (oldItem == newItem) {
-                    return true;
-                }
-                if (oldItem == null || newItem == null) {
-                    return false;
-                }
-
-                return diffCallback.areContentsTheSame(oldItem, newItem);
-            }
-        }, detectMoves);
-    }
-
-    static <T> void dispatchDiff(ListUpdateCallback callback,
-            final DiffUtil.DiffResult diffResult) {
-        // Simple case, dispatch & return
-        diffResult.dispatchUpdatesTo(callback);
-    }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
index e11117e..2735c05 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java
@@ -166,17 +166,13 @@
 
         p = dataSource.loadBefore(15, list.get(0), 10);
         assertNotNull(p);
-        for (User u : p) {
-            list.add(0, u);
-        }
+        list.addAll(0, p);
 
         assertArrayEquals(Arrays.copyOfRange(expected, 5, 35), list.toArray());
 
         p = dataSource.loadBefore(5, list.get(0), 10);
         assertNotNull(p);
-        for (User u : p) {
-            list.add(0, u);
-        }
+        list.addAll(0, p);
 
         assertArrayEquals(Arrays.copyOfRange(expected, 0, 35), list.toArray());
     }
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 dcf98c9..854c862 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
@@ -205,18 +205,18 @@
         LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
         observeForever(pagedList);
         drain();
-        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
 
         mDao.insert(new Entity1(1, "foo"));
         drain();
         //noinspection ConstantConditions
         assertThat(pagedList.getValue().size(), is(1));
-        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 3 : 1);
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1);
 
         mDao.insert(new Entity1(2, "bar"));
         drain();
         assertThat(pagedList.getValue().size(), is(2));
-        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 5 : 2);
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2);
     }
 
     @Test
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
index 818c46b..cdd464e 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
@@ -86,6 +86,7 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
         PagedList<Customer> list = mAdapter.getCurrentList();
         if (list == null) {
             // Can't find anything to restore
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 9d40237..b5df914 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
@@ -59,7 +59,7 @@
 
     // Keyed
 
-    @Query("SELECT * from customer ORDER BY mLastName ASC LIMIT :limit")
+    @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit")
     List<Customer> customerNameInitial(int limit);
 
     @Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit")