Merge changes from topic "am-989a33ff-7fb5-484a-ad22-f8209a1ea3dc"

* changes:
  [automerger] Use explicit function instead of lambda. am: 461cc7c0bd
  Use explicit function instead of lambda.
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
index d16045f..13faf24 100644
--- a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
@@ -96,6 +96,8 @@
             statement.bindLong(index, (Byte) arg);
         } else if (arg instanceof String) {
             statement.bindString(index, (String) arg);
+        } else if (arg instanceof Boolean) {
+            statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
         } else {
             throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
                     + " Supported types: null, byte[], float, double, long, int, short, byte,"
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt
index 95130e8..512770a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt
@@ -44,8 +44,8 @@
     }
 
     private fun createSelect(resultFields: Set<String>): String {
-        return "SELECT ${resultFields.joinToString(",")}" +
+        return "SELECT ${resultFields.joinToString(",") {"`$it`"}}" +
                 " FROM `${entity.tableName}`" +
-                " WHERE ${entityField.columnName} IN (:args)"
+                " WHERE `${entityField.columnName}` IN (:args)"
     }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 768f64a..1db5b46 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -118,6 +118,10 @@
     @Query("select * from user where mId = :id")
     public abstract LiveData<User> liveUserById(int id);
 
+    @Transaction
+    @Query("select * from user where mId = :id")
+    public abstract LiveData<User> liveUserByIdInTransaction(int id);
+
     @Query("select * from user where mName LIKE '%' || :name || '%' ORDER BY mId DESC")
     public abstract LiveData<List<User>> liveUsersListByName(String name);
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java
new file mode 100644
index 0000000..70581f2
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018 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.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Collections.singletonList;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.ForeignKey;
+import android.arch.persistence.room.Index;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Relation;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.Transaction;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RelationWithReservedKeywordTest {
+    private MyDatabase mDb;
+
+    @Before
+    public void initDb() {
+        mDb = Room.inMemoryDatabaseBuilder(
+                InstrumentationRegistry.getTargetContext(),
+                MyDatabase.class).build();
+    }
+
+    @Test
+    public void loadRelation() {
+        Category category = new Category(1, "cat1");
+        mDb.getDao().insert(category);
+        Topic topic = new Topic(2, 1, "foo");
+        mDb.getDao().insert(topic);
+        List<CategoryWithTopics> categoryWithTopics = mDb.getDao().loadAll();
+        assertThat(categoryWithTopics.size(), is(1));
+        assertThat(categoryWithTopics.get(0).category, is(category));
+        assertThat(categoryWithTopics.get(0).topics, is(singletonList(topic)));
+    }
+
+    @Entity(tableName = "categories")
+    static class Category {
+
+        @PrimaryKey(autoGenerate = true)
+        public final long id;
+
+        public final String name;
+
+        Category(long id, String name) {
+            this.id = id;
+            this.name = name;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Category category = (Category) o;
+            return id == category.id
+                    && Objects.equals(name, category.name);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id, name);
+        }
+    }
+
+    @Dao
+    interface MyDao {
+        @Transaction
+        @Query("SELECT * FROM categories")
+        List<CategoryWithTopics> loadAll();
+
+        @Insert
+        void insert(Category... categories);
+
+        @Insert
+        void insert(Topic... topics);
+    }
+
+    @Database(
+            entities = {Category.class, Topic.class},
+            version = 1,
+            exportSchema = false)
+    abstract static class MyDatabase extends RoomDatabase {
+        abstract MyDao getDao();
+    }
+
+
+    @SuppressWarnings("WeakerAccess")
+    static class CategoryWithTopics {
+        @Embedded
+        public Category category;
+
+        @Relation(
+                parentColumn = "id",
+                entityColumn = "category_id",
+                entity = Topic.class)
+        public List<Topic> topics;
+    }
+
+    @Entity(
+            tableName = "topics",
+            foreignKeys = @ForeignKey(
+                    entity = Category.class,
+                    parentColumns = "id",
+                    childColumns = "category_id",
+                    onDelete = ForeignKey.CASCADE),
+            indices = @Index("category_id"))
+    static class Topic {
+
+        @PrimaryKey(autoGenerate = true)
+        public final long id;
+
+        @ColumnInfo(name = "category_id")
+        public final long categoryId;
+
+        public final String to;
+
+        Topic(long id, long categoryId, String to) {
+            this.id = id;
+            this.categoryId = categoryId;
+            this.to = to;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Topic topic = (Topic) o;
+            return id == topic.id
+                    && categoryId == topic.categoryId
+                    && Objects.equals(to, topic.to);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id, categoryId, to);
+        }
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java
index 8d8e0e8..5bc01ce 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java
@@ -17,10 +17,12 @@
 package android.arch.persistence.room.integration.testapp.test;
 
 import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalToIgnoringCase;
+import static org.hamcrest.Matchers.hasSize;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
@@ -39,6 +41,7 @@
 import android.database.Cursor;
 import android.support.annotation.NonNull;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
 import android.support.test.runner.AndroidJUnit4;
@@ -125,7 +128,18 @@
         LiveData<User> user1 = dao.liveUserById(1);
         Observer<User> observer = startObserver(user1);
         dao.insert(TestUtil.createUser(1));
-        verify(observer, timeout(30000).atLeastOnce())
+        verify(observer, timeout(3000).atLeastOnce())
+                .onChanged(argThat(user -> user != null && user.getId() == 1));
+        stopObserver(user1, observer);
+    }
+
+    @Test
+    public void observeLiveDataWithTransaction() {
+        UserDao dao = mDatabase.getUserDao();
+        LiveData<User> user1 = dao.liveUserByIdInTransaction(1);
+        Observer<User> observer = startObserver(user1);
+        dao.insert(TestUtil.createUser(1));
+        verify(observer, timeout(3000).atLeastOnce())
                 .onChanged(argThat(user -> user != null && user.getId() == 1));
         stopObserver(user1, observer);
     }
@@ -159,15 +173,12 @@
         final UserDao dao = mDatabase.getUserDao();
         final User user1 = TestUtil.createUser(1);
         dao.insert(user1);
-        Future<Boolean> future;
         try {
             mDatabase.beginTransaction();
             dao.delete(user1);
             ExecutorService executor = Executors.newSingleThreadExecutor();
-            future = executor.submit(() -> {
-                assertThat(dao.load(1), is(equalTo(user1)));
-                return true;
-            });
+            Future<?> future = executor.submit(() ->
+                    assertThat(dao.load(1), is(equalTo(user1))));
             future.get();
             mDatabase.setTransactionSuccessful();
         } finally {
@@ -177,21 +188,52 @@
     }
 
     @Test
+    @LargeTest
+    public void observeInvalidationInBackground() throws InterruptedException, ExecutionException {
+        final UserDao dao = mDatabase.getUserDao();
+        final User user1 = TestUtil.createUser(1);
+        final CountDownLatch observerRegistered = new CountDownLatch(1);
+        final CountDownLatch onInvalidatedCalled = new CountDownLatch(1);
+        dao.insert(user1);
+        Future future;
+        try {
+            mDatabase.beginTransaction();
+            dao.delete(user1);
+            future = Executors.newSingleThreadExecutor().submit(() -> {
+                // Adding this observer will be blocked by the surrounding transaction.
+                mDatabase.getInvalidationTracker().addObserver(
+                        new InvalidationTracker.Observer("User") {
+                            @Override
+                            public void onInvalidated(@NonNull Set<String> tables) {
+                                onInvalidatedCalled.countDown(); // This should not happen
+                            }
+                        });
+                observerRegistered.countDown();
+            });
+            mDatabase.setTransactionSuccessful();
+        } finally {
+            assertThat(observerRegistered.getCount(), is(1L));
+            mDatabase.endTransaction();
+        }
+        assertThat(dao.count(), is(0));
+        assertThat(observerRegistered.await(3000, TimeUnit.MILLISECONDS), is(true));
+        future.get();
+        assertThat(onInvalidatedCalled.await(500, TimeUnit.MILLISECONDS), is(false));
+    }
+
+    @Test
     public void invalidation() throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
         mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("User") {
             @Override
             public void onInvalidated(@NonNull Set<String> tables) {
+                assertThat(tables, hasSize(1));
+                assertThat(tables, hasItem("User"));
                 latch.countDown();
             }
         });
         mDatabase.getUserDao().insert(TestUtil.createUser(1));
-        latch.await(3000, TimeUnit.MILLISECONDS);
-        for (int i = 0; i < 10; i++) {
-            // This can (occasionally) detect if there is an recursive loop in InvalidationTracker
-            // invalidating itself by running its refresh query in a transaction.
-            assertThat(mDatabase.inTransaction(), is(false));
-        }
+        assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
     }
 
     private static <T> Observer<T> startObserver(LiveData<T> liveData) {
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java b/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
index 86054ce..7bb4fb2 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
@@ -159,6 +159,7 @@
             } finally {
                 database.endTransaction();
             }
+            syncTriggers(database);
             mCleanupStatement = database.compileStatement(CLEANUP_SQL);
             mInitialized = true;
         }
@@ -219,6 +220,7 @@
      *
      * @param observer The observer which listens the database for changes.
      */
+    @WorkerThread
     public void addObserver(@NonNull Observer observer) {
         final String[] tableNames = observer.mTables;
         int[] tableIds = new int[tableNames.length];
@@ -240,7 +242,7 @@
             currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
         }
         if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
-            ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
+            syncTriggers();
         }
     }
 
@@ -265,65 +267,17 @@
      * @param observer The observer to remove.
      */
     @SuppressWarnings("WeakerAccess")
+    @WorkerThread
     public void removeObserver(@NonNull final Observer observer) {
         ObserverWrapper wrapper;
         synchronized (mObserverMap) {
             wrapper = mObserverMap.remove(observer);
         }
         if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
-            ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
+            syncTriggers();
         }
     }
 
-    private Runnable mSyncTriggers = new Runnable() {
-        @Override
-        public void run() {
-            if (mDatabase.inTransaction()) {
-                // we won't run this inside another transaction.
-                return;
-            }
-            if (!ensureInitialization()) {
-                return;
-            }
-            try {
-                // This method runs in a while loop because while changes are synced to db, another
-                // runnable may be skipped. If we cause it to skip, we need to do its work.
-                while (true) {
-                    // there is a potential race condition where another mSyncTriggers runnable
-                    // can start running right after we get the tables list to sync.
-                    final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
-                    if (tablesToSync == null) {
-                        return;
-                    }
-                    final int limit = tablesToSync.length;
-                    final SupportSQLiteDatabase writableDatabase = mDatabase.getOpenHelper()
-                            .getWritableDatabase();
-                    try {
-                        writableDatabase.beginTransaction();
-                        for (int tableId = 0; tableId < limit; tableId++) {
-                            switch (tablesToSync[tableId]) {
-                                case ObservedTableTracker.ADD:
-                                    startTrackingTable(writableDatabase, tableId);
-                                    break;
-                                case ObservedTableTracker.REMOVE:
-                                    stopTrackingTable(writableDatabase, tableId);
-                                    break;
-                            }
-                        }
-                        writableDatabase.setTransactionSuccessful();
-                    } finally {
-                        writableDatabase.endTransaction();
-                    }
-                    mObservedTableTracker.onSyncCompleted();
-                }
-            } catch (IllegalStateException | SQLiteException exception) {
-                // may happen if db is closed. just log.
-                Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
-                        exception);
-            }
-        }
-    };
-
     private boolean ensureInitialization() {
         if (!mDatabase.isOpen()) {
             return false;
@@ -444,6 +398,53 @@
         mRefreshRunnable.run();
     }
 
+    void syncTriggers(SupportSQLiteDatabase database) {
+        if (database.inTransaction()) {
+            // we won't run this inside another transaction.
+            return;
+        }
+        try {
+            // This method runs in a while loop because while changes are synced to db, another
+            // runnable may be skipped. If we cause it to skip, we need to do its work.
+            while (true) {
+                Lock closeLock = mDatabase.getCloseLock();
+                closeLock.lock();
+                try {
+                    // there is a potential race condition where another mSyncTriggers runnable
+                    // can start running right after we get the tables list to sync.
+                    final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
+                    if (tablesToSync == null) {
+                        return;
+                    }
+                    final int limit = tablesToSync.length;
+                    try {
+                        database.beginTransaction();
+                        for (int tableId = 0; tableId < limit; tableId++) {
+                            switch (tablesToSync[tableId]) {
+                                case ObservedTableTracker.ADD:
+                                    startTrackingTable(database, tableId);
+                                    break;
+                                case ObservedTableTracker.REMOVE:
+                                    stopTrackingTable(database, tableId);
+                                    break;
+                            }
+                        }
+                        database.setTransactionSuccessful();
+                    } finally {
+                        database.endTransaction();
+                    }
+                    mObservedTableTracker.onSyncCompleted();
+                } finally {
+                    closeLock.unlock();
+                }
+            }
+        } catch (IllegalStateException | SQLiteException exception) {
+            // may happen if db is closed. just log.
+            Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+                    exception);
+        }
+    }
+
     /**
      * Called by RoomDatabase before each beginTransaction call.
      * <p>
@@ -453,7 +454,10 @@
      * This api should eventually be public.
      */
     void syncTriggers() {
-        mSyncTriggers.run();
+        if (!mDatabase.isOpen()) {
+            return;
+        }
+        syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
     }
 
     /**
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
index b2a9f0e..90131ed 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
@@ -243,8 +243,9 @@
      */
     public void beginTransaction() {
         assertNotMainThread();
-        mInvalidationTracker.syncTriggers();
-        mOpenHelper.getWritableDatabase().beginTransaction();
+        SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
+        mInvalidationTracker.syncTriggers(database);
+        database.beginTransaction();
     }
 
     /**
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java b/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
index d7474fd..ca091d5 100644
--- a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
+++ b/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
@@ -125,13 +125,10 @@
     public void addRemoveObserver() throws Exception {
         InvalidationTracker.Observer observer = new LatchObserver(1, "a");
         mTracker.addObserver(observer);
-        drainTasks();
         assertThat(mTracker.mObserverMap.size(), is(1));
         mTracker.removeObserver(new LatchObserver(1, "a"));
-        drainTasks();
         assertThat(mTracker.mObserverMap.size(), is(1));
         mTracker.removeObserver(observer);
-        drainTasks();
         assertThat(mTracker.mObserverMap.size(), is(0));
     }
 
@@ -241,9 +238,9 @@
 
     @Test
     public void closedDb() {
+        doReturn(false).when(mRoomDatabase).isOpen();
         doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase();
         mTracker.addObserver(new LatchObserver(1, "a", "b"));
-        mTracker.syncTriggers();
         mTracker.mRefreshRunnable.run();
     }
 
diff --git a/samples/ViewPager2Demos/src/main/AndroidManifest.xml b/samples/ViewPager2Demos/src/main/AndroidManifest.xml
index 57e52d3..9dcc335 100644
--- a/samples/ViewPager2Demos/src/main/AndroidManifest.xml
+++ b/samples/ViewPager2Demos/src/main/AndroidManifest.xml
@@ -31,6 +31,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".CardFragmentActivity" android:label="CardsFragmentDemo">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name=".BrowseActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardActivity.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardActivity.java
index 7484389..7198df3 100644
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardActivity.java
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardActivity.java
@@ -16,27 +16,28 @@
 
 package com.example.androidx.widget.viewpager2;
 
-import static java.util.Arrays.asList;
 import static java.util.Collections.unmodifiableList;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
 
 import com.example.androidx.widget.viewpager2.cards.Card;
-import com.example.androidx.widget.viewpager2.cards.CardDataAdapter;
+import com.example.androidx.widget.viewpager2.cards.CardView;
 
 import java.util.List;
 
 import androidx.widget.ViewPager2;
 
-/** @inheritDoc */
+/**
+ * Shows how to use {@link ViewPager2#setAdapter(RecyclerView.Adapter)}
+ *
+ * @see CardFragmentActivity
+ */
 public class CardActivity extends Activity {
-    private static final List<Card> sCards = unmodifiableList(asList(
-            new Card('♦', 'A'),
-            new Card('♣', 'K'),
-            new Card('♥', 'J'),
-            new Card('♠', '9'),
-            new Card('♦', '2')));
+    private static final List<Card> sCards = unmodifiableList(Card.createDeck52());
 
     @Override
     public void onCreate(Bundle bundle) {
@@ -44,6 +45,38 @@
         setContentView(R.layout.activity_card_layout);
 
         this.<ViewPager2>findViewById(R.id.view_pager).setAdapter(
-                new CardDataAdapter(getLayoutInflater(), sCards));
+                new RecyclerView.Adapter<CardViewHolder>() {
+                    @NonNull
+                    public CardViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                            int viewType) {
+                        return new CardViewHolder(new CardView(getLayoutInflater(), parent));
+                    }
+
+                    @Override
+                    public void onBindViewHolder(@NonNull CardViewHolder holder, int position) {
+                        holder.bind(sCards.get(position));
+                    }
+
+                    @Override
+                    public int getItemCount() {
+                        return sCards.size();
+                    }
+                });
+    }
+
+    /** @inheritDoc */
+    public static class CardViewHolder extends RecyclerView.ViewHolder {
+        private final CardView mCardView;
+
+        /** {@inheritDoc} */
+        public CardViewHolder(CardView cardView) {
+            super(cardView.getView());
+            mCardView = cardView;
+        }
+
+        /** @see CardView#bind(Card) */
+        public void bind(Card card) {
+            mCardView.bind(card);
+        }
     }
 }
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardFragmentActivity.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardFragmentActivity.java
new file mode 100644
index 0000000..f981e67
--- /dev/null
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/CardFragmentActivity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 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 com.example.androidx.widget.viewpager2;
+
+import static java.util.Collections.unmodifiableList;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.androidx.widget.viewpager2.cards.Card;
+import com.example.androidx.widget.viewpager2.cards.CardView;
+
+import java.util.List;
+
+import androidx.widget.ViewPager2;
+import androidx.widget.ViewPager2.FragmentProvider;
+
+/**
+ * Shows how to use {@link ViewPager2#setAdapter(FragmentManager, FragmentProvider, int)}
+ *
+ * @see CardActivity
+ */
+public class CardFragmentActivity extends FragmentActivity {
+    private static final List<Card> sCards = unmodifiableList(Card.createDeck52());
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.activity_card_layout);
+
+        this.<ViewPager2>findViewById(R.id.view_pager).setAdapter(getSupportFragmentManager(),
+                new FragmentProvider() {
+                    @Override
+                    public Fragment getItem(int position) {
+                        return CardFragment.create(sCards.get(position));
+                    }
+
+                    @Override
+                    public int getCount() {
+                        return sCards.size();
+                    }
+                },
+                ViewPager2.FragmentRetentionPolicy.SAVE_STATE);
+    }
+
+        /** {@inheritDoc} */
+    public static class CardFragment extends Fragment {
+        @Nullable
+        @Override
+        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                @Nullable Bundle savedInstanceState) {
+            CardView cardView = new CardView(getLayoutInflater(), container);
+            cardView.bind(Card.fromBundle(getArguments()));
+            return cardView.getView();
+        }
+
+        /** Creates a Fragment for a given {@link Card} */
+        public static CardFragment create(Card card) {
+            CardFragment fragment = new CardFragment();
+            fragment.setArguments(card.toBundle());
+            return fragment;
+        }
+    }
+}
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/Card.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/Card.java
index 02f3271..38a0615 100644
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/Card.java
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/Card.java
@@ -19,15 +19,21 @@
 import static java.util.Arrays.asList;
 import static java.util.Collections.unmodifiableSet;
 
+import android.os.Bundle;
+
+import java.util.ArrayList;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
  * Playing card
  */
 public class Card {
+    private static final String ARGS_BUNDLE = Card.class.getName() + ":Bundle";
+
     private static final Set<Character> SUITS = unmodifiableSet(new LinkedHashSet<>(
-            asList('♦', /* diamonds*/ '♣', /*, clubs*/ '♥', /* hearts*/ '♠' /*spades*/)));
+            asList('♣' /* clubs*/, '♦' /* diamonds*/, '♥' /* hearts*/, '♠' /*spades*/)));
     private static final Set<Character> VALUES = unmodifiableSet(new LinkedHashSet<>(
             asList('2', '3', '4', '5', '6', '7', '8', '9', 'â’‘', 'J', 'Q', 'K', 'A')));
 
@@ -39,18 +45,47 @@
         this.mValue = checkValidValue(value, VALUES);
     }
 
-    public char getSuit() {
+    char getSuit() {
         return mSuit;
     }
 
-    public String getCornerLabel() {
+    String getCornerLabel() {
         return mValue + "\n" + mSuit;
     }
 
+    /** Use in conjunction with {@link Card#fromBundle(Bundle)} */
+    public Bundle toBundle() {
+        Bundle args = new Bundle(1);
+        args.putCharArray(ARGS_BUNDLE, new char[]{mSuit, mValue});
+        return args;
+    }
+
+    /** Use in conjunction with {@link Card#toBundle()} */
+    public static Card fromBundle(Bundle bundle) {
+        char[] spec = bundle.getCharArray(ARGS_BUNDLE);
+        return new Card(spec[0], spec[1]);
+    }
+
     private static char checkValidValue(char value, Set<Character> allowed) {
         if (allowed.contains(value)) {
             return value;
         }
         throw new IllegalArgumentException("Illegal argument: " + value);
     }
+
+    /**
+     * Creates a deck of all allowed cards
+     */
+    public static List<Card> createDeck52() {
+        List<Card> result = new ArrayList<>(52);
+        for (Character suit : SUITS) {
+            for (Character value : VALUES) {
+                result.add(new Card(suit, value));
+            }
+        }
+        if (result.size() != 52) {
+            throw new IllegalStateException();
+        }
+        return result;
+    }
 }
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardDataAdapter.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardDataAdapter.java
deleted file mode 100644
index 7f69e1b..0000000
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardDataAdapter.java
+++ /dev/null
@@ -1,56 +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 com.example.androidx.widget.viewpager2.cards;
-
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import com.example.androidx.widget.viewpager2.R;
-
-import java.util.List;
-
-/** @inheritDoc */
-public class CardDataAdapter extends RecyclerView.Adapter<CardViewHolder> {
-    private final List<Card> mCards;
-    private final LayoutInflater mLayoutInflater;
-
-    public CardDataAdapter(LayoutInflater layoutInflater, List<Card> cards) {
-        mLayoutInflater = layoutInflater;
-        mCards = cards;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    @NonNull
-    public CardViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        return new CardViewHolder(
-                mLayoutInflater.inflate(R.layout.item_card_layout, parent, false));
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull CardViewHolder holder, int position) {
-        holder.apply(mCards.get(position));
-    }
-
-    @Override
-    public int getItemCount() {
-        return mCards.size();
-    }
-}
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardViewHolder.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardView.java
similarity index 63%
rename from samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardViewHolder.java
rename to samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardView.java
index 8fd0477..7c8533c 100644
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardViewHolder.java
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/widget/viewpager2/cards/CardView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2018 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.
@@ -16,29 +16,31 @@
 
 package com.example.androidx.widget.viewpager2.cards;
 
-import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.TextView;
 
 import com.example.androidx.widget.viewpager2.R;
 
-/** @inheritDoc */
-public class CardViewHolder extends RecyclerView.ViewHolder {
+/** Inflates and populates a {@link View} representing a {@link Card} */
+public class CardView {
+    private final View mView;
     private final TextView mTextSuite;
     private final TextView mTextCorner1;
     private final TextView mTextCorner2;
 
-    public CardViewHolder(View itemView) {
-        super(itemView);
-        mTextSuite = itemView.findViewById(R.id.label_center);
-        mTextCorner1 = itemView.findViewById(R.id.label_top);
-        mTextCorner2 = itemView.findViewById(R.id.label_bottom);
+    public CardView(LayoutInflater layoutInflater, ViewGroup container) {
+        mView = layoutInflater.inflate(R.layout.item_card_layout, container, false);
+        mTextSuite = mView.findViewById(R.id.label_center);
+        mTextCorner1 = mView.findViewById(R.id.label_top);
+        mTextCorner2 = mView.findViewById(R.id.label_bottom);
     }
 
     /**
      * Updates the view to represent the passed in card
      */
-    public void apply(Card card) {
+    public void bind(Card card) {
         mTextSuite.setText(Character.toString(card.getSuit()));
 
         String cornerLabel = card.getCornerLabel();
@@ -46,4 +48,8 @@
         mTextCorner2.setText(cornerLabel);
         mTextCorner2.setRotation(180);
     }
+
+    public View getView() {
+        return mView;
+    }
 }
diff --git a/samples/ViewPager2Demos/src/main/res/layout/item_card_layout.xml b/samples/ViewPager2Demos/src/main/res/layout/item_card_layout.xml
index 90a0404..1ae9295 100644
--- a/samples/ViewPager2Demos/src/main/res/layout/item_card_layout.xml
+++ b/samples/ViewPager2Demos/src/main/res/layout/item_card_layout.xml
@@ -26,6 +26,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="top|start"
+        android:gravity="center"
         android:textAppearance="@android:style/TextAppearance.Medium"/>
 
     <TextView
@@ -40,5 +41,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|end"
+        android:gravity="center"
         android:textAppearance="@android:style/TextAppearance.Medium"/>
 </FrameLayout>
diff --git a/transition/proguard-rules.pro b/transition/proguard-rules.pro
index 6cae5e6..dda2c4e 100644
--- a/transition/proguard-rules.pro
+++ b/transition/proguard-rules.pro
@@ -12,10 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# FragmentTransitionSupport is instantiated in support-fragment via reflection.
--keep public class android.support.transition.FragmentTransitionSupport {
-}
-
 # Keep a field in transition that is used to keep a reference to weakly-referenced object
 -keepclassmembers class android.support.transition.ChangeBounds$* extends android.animation.AnimatorListenerAdapter {
   android.support.transition.ChangeBounds$ViewBounds mViewBounds;
diff --git a/viewpager2/build.gradle b/viewpager2/build.gradle
index 7f75d09..543a8d4 100644
--- a/viewpager2/build.gradle
+++ b/viewpager2/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    api(project(":support-core-utils"))
+    api(project(":support-fragment"))
     api(project(":recyclerview-v7"))
 
     androidTestImplementation(TEST_RUNNER)
diff --git a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/TestActivity.java b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/TestActivity.java
index 351ad9a..bdc2ac9 100644
--- a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/TestActivity.java
+++ b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/TestActivity.java
@@ -16,12 +16,12 @@
 
 package androidx.widget.viewpager2.tests;
 
-import android.app.Activity;
 import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
 
 import androidx.widget.viewpager2.test.R;
 
-public class TestActivity extends Activity {
+public class TestActivity extends FragmentActivity {
     @Override
     public void onCreate(Bundle bundle) {
         super.onCreate(bundle);
diff --git a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
index 45b42aa..6f22bae 100644
--- a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
+++ b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
@@ -22,45 +22,61 @@
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
-import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
 import static android.view.View.OVER_SCROLL_NEVER;
 
 import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
 
 import android.content.Context;
 import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.test.InstrumentationRegistry;
-import android.support.test.espresso.IdlingRegistry;
 import android.support.test.espresso.ViewAction;
 import android.support.test.espresso.action.ViewActions;
-import android.support.test.espresso.idling.CountingIdlingResource;
 import android.support.test.filters.MediumTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.Fragment;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.Adapter;
 import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-import org.junit.After;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
 import androidx.widget.ViewPager2;
 import androidx.widget.viewpager2.test.R;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class ViewPager2Tests {
+    private static final Random RANDOM = new Random();
     private static final int[] sColors = {
             Color.parseColor("#BBA9FF00"),
             Color.parseColor("#BB00E87E"),
@@ -74,7 +90,9 @@
     public ExpectedException mExpectedException = ExpectedException.none();
 
     private ViewPager2 mViewPager;
-    private CountingIdlingResource mIdlingResource;
+
+    // allows to wait until swipe operation is finished (Smooth Scroller done)
+    private CountDownLatch mStableAfterSwipe;
 
     public ViewPager2Tests() {
         mActivityTestRule = new ActivityTestRule<>(TestActivity.class);
@@ -84,28 +102,342 @@
     public void setUp() {
         mViewPager = mActivityTestRule.getActivity().findViewById(R.id.view_pager);
 
-        mIdlingResource = new CountingIdlingResource(getClass().getSimpleName() + "IdlingResource");
         mViewPager.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
-                if (newState == SCROLL_STATE_IDLE && !mIdlingResource.isIdleNow()) {
-                    mIdlingResource.decrement();
-                } else if (newState == SCROLL_STATE_SETTLING && mIdlingResource.isIdleNow()) {
-                    mIdlingResource.increment();
+                // coming to idle from another state (dragging or setting) means we're stable now
+                if (newState == SCROLL_STATE_IDLE) {
+                    mStableAfterSwipe.countDown();
                 }
             }
         });
-        IdlingRegistry.getInstance().register(mIdlingResource);
+
+        final long seed = RANDOM.nextLong();
+        RANDOM.setSeed(seed);
+        Log.i(getClass().getName(), "Random seed: " + seed);
     }
 
-    @After
-    public void tearDown() {
-        IdlingRegistry.getInstance().unregister(mIdlingResource);
+    public static class PageFragment extends Fragment {
+        private static final String KEY_VALUE = "value";
+
+        public interface EventListener {
+            void onEvent(PageFragment fragment);
+
+            EventListener NO_OP = new EventListener() {
+                @Override
+                public void onEvent(PageFragment fragment) {
+                    // do nothing
+                }
+            };
+        }
+
+        private EventListener mOnAttachListener = EventListener.NO_OP;
+        private EventListener mOnDestroyListener = EventListener.NO_OP;
+
+        private int mPosition;
+        private int mValue;
+
+        public static PageFragment create(int position, int value) {
+            PageFragment result = new PageFragment();
+            Bundle args = new Bundle(1);
+            args.putInt(KEY_VALUE, value);
+            result.setArguments(args);
+            result.mPosition = position;
+            return result;
+        }
+
+        @Override
+        public void onAttach(Context context) {
+            super.onAttach(context);
+            mOnAttachListener.onEvent(this);
+        }
+
+        @NonNull
+        @Override
+        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                @Nullable Bundle savedInstanceState) {
+            return inflater.inflate(R.layout.item_test_layout, container, false);
+        }
+
+        @Override
+        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+            Bundle data = savedInstanceState != null ? savedInstanceState : getArguments();
+            setValue(data.getInt(KEY_VALUE));
+        }
+
+        @Override
+        public void onDestroy() {
+            super.onDestroy();
+            mOnDestroyListener.onEvent(this);
+        }
+
+        @Override
+        public void onSaveInstanceState(@NonNull Bundle outState) {
+            outState.putInt(KEY_VALUE, mValue);
+        }
+
+        public void setValue(int value) {
+            mValue = value;
+            TextView textView = getView().findViewById(R.id.text_view);
+            applyViewValue(textView, mValue);
+        }
+    }
+
+    private static void applyViewValue(TextView textView, int value) {
+        textView.setText(String.valueOf(value));
+        textView.setBackgroundColor(getColor(value));
+    }
+
+    private static int getColor(int value) {
+        return sColors[value % sColors.length];
     }
 
     @Test
-    public void rendersAndHandlesSwiping() throws Throwable {
-        final int pageCount = sColors.length;
+    public void fragmentAdapter_fullPass() throws Throwable {
+        testFragmentLifecycle(8, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0));
+    }
+
+    @Test
+    public void fragmentAdapter_random() throws Throwable {
+        final int totalPages = 10;
+        final int sequenceLength = 50;
+        testFragmentLifecycle_random(totalPages, sequenceLength, PageMutator.NO_OP);
+    }
+
+    @Test
+    public void fragmentAdapter_random_withMutations() throws Throwable {
+        final int totalPages = 10;
+        final int sequenceLength = 50;
+        testFragmentLifecycle_random(totalPages, sequenceLength, PageMutator.RANDOM);
+    }
+
+    private void testFragmentLifecycle_random(int totalPages, int sequenceLength,
+            PageMutator pageMutator) throws Throwable {
+        List<Integer> pageSequence = generateRandomPageSequence(totalPages, sequenceLength);
+
+        Log.i(getClass().getName(),
+                String.format("Testing with a sequence [%s]", TextUtils.join(", ", pageSequence)));
+
+        testFragmentLifecycle(totalPages, pageSequence, pageMutator);
+    }
+
+    @NonNull
+    private List<Integer> generateRandomPageSequence(int totalPages, int sequenceLength) {
+        List<Integer> pageSequence = new ArrayList<>(sequenceLength);
+
+        int pageIx = 0;
+        Double goRightProbability = null;
+        while (pageSequence.size() != sequenceLength) {
+            boolean goRight;
+            if (pageIx == 0) {
+                goRight = true;
+                goRightProbability = 0.7;
+            } else if (pageIx == totalPages - 1) { // last page
+                goRight = false;
+                goRightProbability = 0.3;
+            } else {
+                goRight = RANDOM.nextDouble() < goRightProbability;
+            }
+
+            pageSequence.add(goRight ? ++pageIx : --pageIx);
+        }
+
+        return pageSequence;
+    }
+
+    /**
+     * Test added when caught a bug: after the last swipe: actual=6, expected=4
+     * <p>
+     * Bug was caused by an invalid test assumption (new Fragment value can be inferred from number
+     * of instances created) - invalid in a case when we sometimes create Fragments off-screen and
+     * end up scrapping them.
+     **/
+    @Test
+    public void fragmentAdapter_regression1() throws Throwable {
+        testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 2, 1, 2, 3, 4));
+    }
+
+    /**
+     * Test added when caught a bug: after the last swipe: actual=4, expected=5
+     * <p>
+     * Bug was caused by mSavedStates.add(position, ...) instead of mSavedStates.set(position, ...)
+     **/
+    @Test
+    public void fragmentAdapter_regression2() throws Throwable {
+        testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 5));
+    }
+
+    /**
+     * Test added when caught a bug: after the last swipe: ArrayIndexOutOfBoundsException: length=5;
+     * index=-1 at androidx.widget.viewpager2.tests.ViewPager2Tests$PageFragment.onCreateView
+     * <p>
+     * Bug was caused by always saving states of unattached fragments as null (even if there was a
+     * valid previously saved state)
+     */
+    @Test
+    public void fragmentAdapter_regression3() throws Throwable {
+        testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 2, 1, 2, 3, 2, 1, 0));
+    }
+
+    /** Goes left on left edge / right on right edge */
+    @Test
+    public void fragmentAdapter_edges() throws Throwable {
+        testFragmentLifecycle(4, Arrays.asList(0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0));
+    }
+
+    private interface PageMutator {
+        void mutate(PageFragment fragment);
+
+        PageMutator NO_OP = new PageMutator() {
+            @Override
+            public void mutate(PageFragment fragment) {
+                // do nothing
+            }
+        };
+
+        /** At random modifies the page under Fragment */
+        PageMutator RANDOM = new PageMutator() {
+            @Override
+            public void mutate(PageFragment fragment) {
+                Random random = ViewPager2Tests.RANDOM;
+                if (random.nextDouble() < 0.125) {
+                    int delta = (1 + random.nextInt(5)) * sColors.length;
+                    fragment.setValue(fragment.mValue + delta);
+                }
+            }
+        };
+    }
+
+    /** @see this#testFragmentLifecycle(int, List, PageMutator) */
+    private void testFragmentLifecycle(final int totalPages, List<Integer> pageSequence)
+            throws Throwable {
+        testFragmentLifecycle(totalPages, pageSequence, PageMutator.NO_OP);
+    }
+
+    /**
+     * Verifies:
+     * <ul>
+     * <li>page content / background
+     * <li>maximum number of Fragments held in memory
+     * <li>Fragment state saving / restoring
+     * </ul>
+     */
+    private void testFragmentLifecycle(final int totalPages, List<Integer> pageSequence,
+            final PageMutator pageMutator) throws Throwable {
+        final AtomicInteger attachCount = new AtomicInteger(0);
+        final AtomicInteger destroyCount = new AtomicInteger(0);
+        final boolean[] wasEverAttached = new boolean[totalPages];
+        final PageFragment[] fragments = new PageFragment[totalPages];
+
+        final int[] expectedValues = new int[totalPages];
+        for (int i = 0; i < totalPages; i++) {
+            expectedValues[i] = i;
+        }
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mViewPager.setAdapter(mActivityTestRule.getActivity().getSupportFragmentManager(),
+                        new ViewPager2.FragmentProvider() {
+                            @Override
+                            public Fragment getItem(final int position) {
+                                // if the fragment was attached in the past, it means we have
+                                // provided it with the correct value already; give a dummy one
+                                // to prove state save / restore functionality works
+                                int value = wasEverAttached[position] ? -1 : position;
+                                PageFragment fragment = PageFragment.create(position, value);
+
+                                fragment.mOnAttachListener = new PageFragment.EventListener() {
+                                    @Override
+                                    public void onEvent(PageFragment fragment) {
+                                        attachCount.incrementAndGet();
+                                        wasEverAttached[fragment.mPosition] = true;
+                                    }
+                                };
+
+                                fragment.mOnDestroyListener = new PageFragment.EventListener() {
+                                    @Override
+                                    public void onEvent(PageFragment fragment) {
+                                        destroyCount.incrementAndGet();
+                                    }
+                                };
+
+                                fragments[position] = fragment;
+                                return fragment;
+                            }
+
+                            @Override
+                            public int getCount() {
+                                return totalPages;
+                            }
+                        }, ViewPager2.FragmentRetentionPolicy.SAVE_STATE);
+            }
+        });
+
+        final AtomicInteger currentPage = new AtomicInteger(0);
+        verifyView(expectedValues[currentPage.get()]);
+        for (int nextPage : pageSequence) {
+            swipe(currentPage.get(), nextPage, totalPages);
+            currentPage.set(nextPage);
+            verifyView(expectedValues[currentPage.get()]);
+
+            // TODO: validate Fragments that are instantiated, but not attached. No destruction
+            // steps are done to them - they're just left to the Garbage Collector. Maybe
+            // WeakReferences could help, but the GC behaviour is not predictable. Alternatively,
+            // we could only create Fragments onAttach, but there is a potential performance
+            // trade-off.
+            assertThat(attachCount.get() - destroyCount.get(), isBetween(1, 4));
+
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    final int page = currentPage.get();
+                    PageFragment fragment = fragments[page];
+                    pageMutator.mutate(fragment);
+                    expectedValues[page] = fragment.mValue;
+                }
+            });
+        }
+    }
+
+    private void swipe(int currentPageIx, int nextPageIx, int totalPages)
+            throws InterruptedException {
+        if (nextPageIx >= totalPages) {
+            throw new IllegalArgumentException("Invalid nextPageIx: >= totalPages.");
+        }
+
+        if (currentPageIx == nextPageIx) { // dedicated for testing edge behaviour
+            if (nextPageIx == 0) {
+                swipeRight(); // bounce off the left edge
+                return;
+            }
+            if (nextPageIx == totalPages - 1) { // bounce off the right edge
+                swipeLeft();
+                return;
+            }
+            throw new IllegalArgumentException(
+                    "Invalid sequence. Not on an edge, and currentPageIx/nextPageIx pages same.");
+        }
+
+        if (Math.abs(nextPageIx - currentPageIx) > 1) {
+            throw new IllegalArgumentException(
+                    "Specified nextPageIx not adjacent to the current page.");
+        }
+
+        if (nextPageIx > currentPageIx) {
+            swipeLeft();
+        } else {
+            swipeRight();
+        }
+    }
+
+    private Matcher<Integer> isBetween(int min, int max) {
+        return allOf(greaterThanOrEqualTo(min), lessThanOrEqualTo(max));
+    }
+
+    @Test
+    public void viewAdapter_rendersAndHandlesSwiping() throws Throwable {
+        final int totalPages = 8;
 
         if (Build.VERSION.SDK_INT < 16) { // TODO(b/71500143): remove temporary workaround
             RecyclerView mRecyclerView = (RecyclerView) mViewPager.getChildAt(0);
@@ -131,41 +463,66 @@
                             @Override
                             public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
                                 TextView view = (TextView) holder.itemView;
-                                view.setText(String.valueOf(position));
-                                view.setBackgroundColor(sColors[position]);
+                                applyViewValue(view, position);
                             }
 
                             @Override
                             public int getItemCount() {
-                                return pageCount;
+                                return totalPages;
                             }
                         });
             }
         });
 
-        final int pageIxFirst = 0;
-        final int pageIxLast = pageCount - 1;
-        final int swipeCount = pageCount + 1; // two swipes beyond edge to test 'edge behavior'
-        int pageNumber = pageIxFirst;
-        for (int i = 0; i < swipeCount; i++, pageNumber = Math.min(pageIxLast, ++pageNumber)) {
-            verifyView(pageNumber);
-            performSwipe(ViewActions.swipeLeft());
+        List<Integer> pageSequence = Arrays.asList(0, 0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 6, 5, 4, 3, 2,
+                1, 0, 0, 0);
+        verifyView(0);
+        int currentPage = 0;
+        for (int nextPage : pageSequence) {
+            swipe(currentPage, nextPage, totalPages);
+            currentPage = nextPage;
+            verifyView(currentPage);
         }
-        assertThat(pageNumber, equalTo(pageIxLast));
-        for (int i = 0; i < swipeCount; i++, pageNumber = Math.max(pageIxFirst, --pageNumber)) {
-            verifyView(pageNumber);
-            performSwipe(ViewActions.swipeRight());
-        }
-        assertThat(pageNumber, equalTo(pageIxFirst));
     }
 
     private void verifyView(int pageNumber) {
         onView(allOf(withId(R.id.text_view), isDisplayed())).check(
-                matches(withText(String.valueOf(pageNumber))));
+                matches(allOf(withText(String.valueOf(pageNumber)),
+                        new BackgroundColorMatcher(pageNumber))));
+    }
+
+    private static class BackgroundColorMatcher extends BaseMatcher<View> {
+        private final int mPageNumber;
+
+        BackgroundColorMatcher(int pageNumber) {
+            mPageNumber = pageNumber;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("should have background color: ").appendValue(
+                    getColor(mPageNumber));
+        }
+
+        @Override
+        public boolean matches(Object item) {
+            ColorDrawable background = (ColorDrawable) ((View) item).getBackground();
+            return background.getColor() == getColor(mPageNumber);
+        }
+    }
+
+    private void swipeLeft() throws InterruptedException {
+        performSwipe(ViewActions.swipeLeft());
+    }
+
+    private void swipeRight() throws InterruptedException {
+        performSwipe(ViewActions.swipeRight());
     }
 
     private void performSwipe(ViewAction swipeAction) throws InterruptedException {
+        mStableAfterSwipe = new CountDownLatch(1);
         onView(allOf(isDisplayed(), withId(R.id.text_view))).perform(swipeAction);
+        mStableAfterSwipe.await(1, TimeUnit.SECONDS);
     }
 
     @Test
@@ -211,4 +568,5 @@
 
     // TODO: verify correct padding behavior
     // TODO: add test for screen orientation change
+    // TODO: port some of the fragment adapter tests as view adapter tests
 }
diff --git a/viewpager2/src/main/java/androidx/widget/ViewPager2.java b/viewpager2/src/main/java/androidx/widget/ViewPager2.java
index 9ebdea1..19c8d3d 100644
--- a/viewpager2/src/main/java/androidx/widget/ViewPager2.java
+++ b/viewpager2/src/main/java/androidx/widget/ViewPager2.java
@@ -18,11 +18,19 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
 import android.content.Context;
 import android.graphics.Rect;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewCompat;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.PagerSnapHelper;
 import android.support.v7.widget.RecyclerView;
@@ -32,6 +40,11 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Work in progress: go/viewpager2
@@ -86,7 +99,7 @@
     }
 
     /**
-     * TODO(b/70663708): decide on an Adapter class (for now reusing RecyclerView.Adapter)
+     * TODO(b/70663708): decide on an Adapter class. Here supporting RecyclerView.Adapter.
      *
      * @see RecyclerView#setAdapter(Adapter)
      */
@@ -123,6 +136,174 @@
         });
     }
 
+    /**
+     * TODO(b/70663708): decide on an Adapter class. Here supporting {@link Fragment}s.
+     *
+     * @param fragmentRetentionPolicy allows for future parameterization of Fragment memory
+     *                                strategy, similar to what {@link FragmentPagerAdapter} and
+     *                                {@link FragmentStatePagerAdapter} provide.
+     */
+    public void setAdapter(FragmentManager fragmentManager, FragmentProvider fragmentProvider,
+            @FragmentRetentionPolicy int fragmentRetentionPolicy) {
+        if (fragmentRetentionPolicy != FragmentRetentionPolicy.SAVE_STATE) {
+            throw new IllegalArgumentException("Currently only SAVE_STATE policy is supported");
+        }
+
+        mRecyclerView.setAdapter(new FragmentStateAdapter(fragmentManager, fragmentProvider));
+    }
+
+    /**
+     * Similar in behavior to {@link FragmentStatePagerAdapter}
+     * <p>
+     * Lifecycle within {@link RecyclerView}:
+     * <ul>
+     * <li>{@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a
+     * re-usable container for a {@link Fragment} in later stages.
+     * <li>{@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the
+     * position. If we already have the fragment, or have previously saved its state, we use those.
+     * <li>{@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a
+     * container.
+     * <li>{@link RecyclerView.Adapter#onViewRecycled} and
+     * {@link RecyclerView.Adapter#onFailedToRecycleView} we remove, save state, destroy the
+     * {@link Fragment}.
+     * </ul>
+     */
+    private static class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> {
+        private final List<Fragment.SavedState> mSavedStates = new ArrayList<>();
+        // TODO: handle current item's menuVisibility userVisibleHint as FragmentStatePagerAdapter
+
+        private final FragmentManager mFragmentManager;
+        private final FragmentProvider mFragmentProvider;
+
+        private FragmentStateAdapter(FragmentManager fragmentManager,
+                FragmentProvider fragmentProvider) {
+            this.mFragmentManager = fragmentManager;
+            this.mFragmentProvider = fragmentProvider;
+        }
+
+        @NonNull
+        @Override
+        public FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            return FragmentViewHolder.create(parent);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull FragmentViewHolder holder, int position) {
+            if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
+                // this should never happen; if it does, it breaks our assumption that attaching
+                // a Fragment can reliably happen inside onViewAttachedToWindow
+                throw new IllegalStateException(
+                        String.format("View %s unexpectedly attached to a window.",
+                                holder.getContainer()));
+            }
+
+            holder.mFragment = getFragment(position);
+        }
+
+        private Fragment getFragment(int position) {
+            Fragment fragment = mFragmentProvider.getItem(position);
+            if (mSavedStates.size() > position) {
+                Fragment.SavedState savedState = mSavedStates.get(position);
+                if (savedState != null) {
+                    fragment.setInitialSavedState(savedState);
+                }
+            }
+            return fragment;
+        }
+
+        @Override
+        public void onViewAttachedToWindow(@NonNull FragmentViewHolder holder) {
+            if (holder.mFragment.isAdded()) {
+                return;
+            }
+            mFragmentManager.beginTransaction().add(holder.getContainer().getId(),
+                    holder.mFragment).commitNowAllowingStateLoss();
+        }
+
+        @Override
+        public int getItemCount() {
+            return mFragmentProvider.getCount();
+        }
+
+        @Override
+        public void onViewRecycled(@NonNull FragmentViewHolder holder) {
+            removeFragment(holder);
+        }
+
+        @Override
+        public boolean onFailedToRecycleView(@NonNull FragmentViewHolder holder) {
+            // This happens when a ViewHolder is in a transient state (e.g. during custom
+            // animation). We don't have sufficient information on how to clear up what lead to
+            // the transient state, so we are throwing away the ViewHolder to stay on the
+            // conservative side.
+            removeFragment(holder);
+            return false; // don't recycle the view
+        }
+
+        private void removeFragment(@NonNull FragmentViewHolder holder) {
+            if (holder.mFragment == null) {
+                return; // fresh ViewHolder, nothing to do
+            }
+
+            int position = holder.getAdapterPosition();
+
+            if (holder.mFragment.isAdded()) {
+                while (mSavedStates.size() <= position) {
+                    mSavedStates.add(null);
+                }
+                mSavedStates.set(position,
+                        mFragmentManager.saveFragmentInstanceState(holder.mFragment));
+            }
+
+            mFragmentManager.beginTransaction().remove(
+                    holder.mFragment).commitNowAllowingStateLoss();
+            holder.mFragment = null;
+        }
+    }
+
+    private static class FragmentViewHolder extends RecyclerView.ViewHolder {
+        private Fragment mFragment;
+
+        private FragmentViewHolder(FrameLayout container) {
+            super(container);
+        }
+
+        static FragmentViewHolder create(ViewGroup parent) {
+            FrameLayout container = new FrameLayout(parent.getContext());
+            container.setLayoutParams(
+                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT));
+            container.setId(ViewCompat.generateViewId());
+            return new FragmentViewHolder(container);
+        }
+
+        FrameLayout getContainer() {
+            return (FrameLayout) itemView;
+        }
+    }
+
+    /**
+     * Provides {@link Fragment}s for pages
+     */
+    public interface FragmentProvider {
+        /**
+         * Return the Fragment associated with a specified position.
+         */
+        Fragment getItem(int position);
+
+        /**
+         * Return the number of pages available.
+         */
+        int getCount();
+    }
+
+    @Retention(CLASS)
+    @IntDef({FragmentRetentionPolicy.SAVE_STATE})
+    public @interface FragmentRetentionPolicy {
+        /** Approach similar to {@link FragmentStatePagerAdapter} */
+        int SAVE_STATE = 0;
+    }
+
     @Override
     public void onViewAdded(View child) {
         // TODO(b/70666620): consider adding a support for Decor views