Merge "Infer args types" into flatfoot-navigation
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index 788da10..b6ec382 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -39,6 +39,9 @@
 include ":paging:runtime"
 project(':paging:runtime').projectDir = new File(supportRoot, "paging/runtime")
 
+include ":paging:rxjava2"
+project(':paging:rxjava2').projectDir = new File(supportRoot, "paging/rxjava2")
+
 include ':paging:integration-tests:testapp'
 project(':paging:integration-tests:testapp').projectDir = new File(supportRoot, "paging/integration-tests/testapp")
 
@@ -115,18 +118,33 @@
 include ':navigation:common'
 project(':navigation:common').projectDir = new File(supportRoot, "navigation/common")
 
+include ':navigation:common-ktx'
+project(':navigation:common-ktx').projectDir = new File(supportRoot, "navigation/common/ktx")
+
 include ':navigation:runtime'
 project(':navigation:runtime').projectDir = new File(supportRoot, "navigation/runtime")
 
+include ':navigation:runtime-ktx'
+project(':navigation:runtime-ktx').projectDir = new File(supportRoot, "navigation/runtime/ktx")
+
 include ':navigation:testing'
 project(':navigation:testing').projectDir = new File(supportRoot, "navigation/testing")
 
+include ':navigation:testing-ktx'
+project(':navigation:testing-ktx').projectDir = new File(supportRoot, "navigation/testing/ktx")
+
 include ':navigation:fragment'
 project(':navigation:fragment').projectDir = new File(supportRoot, "navigation/fragment")
 
+include ':navigation:fragment-ktx'
+project(':navigation:fragment-ktx').projectDir = new File(supportRoot, "navigation/fragment/ktx")
+
 include ':navigation:ui'
 project(':navigation:ui').projectDir = new File(supportRoot, "navigation/ui")
 
+include ':navigation:ui-ktx'
+project(':navigation:ui-ktx').projectDir = new File(supportRoot, "navigation/ui/ktx")
+
 include ':navigation:integration-tests:testapp'
 project(':navigation:integration-tests:testapp').projectDir = new File(supportRoot, "navigation/integration-tests/testapp")
 
diff --git a/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt b/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
index 72564cc..b43369a 100644
--- a/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
@@ -405,6 +405,7 @@
                 if (!offline) {
                     addStringOption("toroot", "/")
                     addBooleanOption("devsite", true)
+                    addBooleanOption("yamlV2", true)
                     addStringOption("dac_libraryroot", dacOptions.libraryroot)
                     addStringOption("dac_dataname", dacOptions.dataname)
                 }
diff --git a/buildSrc/src/main/kotlin/android/support/LibraryVersions.kt b/buildSrc/src/main/kotlin/android/support/LibraryVersions.kt
index abf1abf..d5b7fb0 100644
--- a/buildSrc/src/main/kotlin/android/support/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/android/support/LibraryVersions.kt
@@ -28,7 +28,7 @@
     /**
      * Version code for Room
      */
-    val ROOM = Version("1.1.0-beta1")
+    val ROOM = Version("1.1.0-beta2")
 
     /**
      * Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
@@ -48,7 +48,9 @@
     /**
      * Version code for Paging
      */
-    val PAGING = Version("1.0.0-alpha7")
+    val PAGING = Version("1.0.0-beta1")
+
+    val PAGING_RX = Version("1.0.0-alpha1")
 
     private val LIFECYCLES = Version("1.1.1")
 
diff --git a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
index 347c1e3..0f93744 100644
--- a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
@@ -58,7 +58,7 @@
 const val SUPPORT_RECYCLERVIEW_27 = "com.android.support:recyclerview-v7:$SUPPORT_VERSION_27"
 const val SUPPORT_APPCOMPAT_27 = "com.android.support:appcompat-v7:$SUPPORT_VERSION_27"
 
-private const val SUPPORT_VERSION = "27.0.0"
+private const val SUPPORT_VERSION = "27.1.0"
 const val SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$SUPPORT_VERSION"
 const val SUPPORT_APPCOMPAT = "com.android.support:appcompat-v7:$SUPPORT_VERSION"
 const val SUPPORT_CARDVIEW = "com.android.support:cardview-v7:$SUPPORT_VERSION"
diff --git a/navigation/common/ktx/build.gradle b/navigation/common/ktx/build.gradle
new file mode 100644
index 0000000..2c4556d
--- /dev/null
+++ b/navigation/common/ktx/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":navigation:common"))
+    api(KOTLIN_STDLIB)
+    testImplementation(project(":navigation:testing"))
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+    androidTestImplementation(project(":navigation:testing"))
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+    name = "Android Navigation Common Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.NAVIGATION
+    mavenGroup = LibraryGroups.NAVIGATION
+    inceptionYear = "2018"
+    description = "Android Navigation-Common-Ktx"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/common/ktx/src/androidTest/AndroidManifest.xml b/navigation/common/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..5b5eccf
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.navigation.ktx.test">
+    <application>
+    </application>
+</manifest>
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
new file mode 100644
index 0000000..dd52934
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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 androidx.navigation
+
+import android.os.Bundle
+import android.support.annotation.IdRes
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.TestNavigatorProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavDestinationTest {
+    private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+    @Test
+    fun navDestination() {
+        val destination = provider.navDestination(DESTINATION_ID) { }
+        assertEquals("NavDestination should have id set",
+                DESTINATION_ID, destination.id)
+    }
+
+    @Test
+    fun navDestinationLabel() {
+        val destination = provider.navDestination(DESTINATION_ID) {
+            label = LABEL
+        }
+        assertEquals("NavDestination should have label set",
+                LABEL, destination.label)
+    }
+
+    @Test
+    fun navDestinationDefaultArguments() {
+        val arguments = Bundle()
+        val destination = provider.navDestination(DESTINATION_ID) {
+            defaultArguments = arguments
+        }
+        assertEquals("NavDestination should have default arguments set",
+                arguments, destination.defaultArguments)
+    }
+
+    @Test
+    fun navDestinationAction() {
+        val destination = provider.navDestination(DESTINATION_ID) {
+            action(ACTION_ID) {
+                destinationId = DESTINATION_ID
+                navOptions {
+                    popUpTo = DESTINATION_ID
+                }
+            }
+        }
+        val action = destination.getAction(ACTION_ID)
+        assertNotNull("NavDestination should have action that was added", action)
+        assertEquals("NavAction should have NavOptions set",
+                DESTINATION_ID, action?.navOptions?.popUpTo)
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "TEST"
+private const val ACTION_ID = 1
+
+/**
+ * Instead of constructing a NavGraph from the NavigatorProvider, construct
+ * a NavDestination directly to allow for testing NavDestinationBuilder in
+ * isolation.
+ */
+fun NavigatorProvider.navDestination(
+        @IdRes id: Int,
+        block: NavDestinationBuilder<NavDestination>.() -> Unit
+): NavDestination = NavDestinationBuilder(this[TestNavigator::class], id).apply(block).build()
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
new file mode 100644
index 0000000..9ecffe9
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.annotation.IdRes
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.TestNavigatorProvider
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavGraphBuilderTest {
+    private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+    @Test
+    fun navigation() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            navDestination(DESTINATION_ID) {}
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+
+    @Test
+    fun navigationUnaryPlus() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            +NavDestination(provider[TestNavigator::class]).apply {
+                id = DESTINATION_ID
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+
+    @Test
+    fun navigationAddDestination() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            val destination = NavDestination(provider[TestNavigator::class]).apply {
+                id = DESTINATION_ID
+            }
+            addDestination(destination)
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun navigationMissingStartDestination() {
+        provider.navigation(startDestination = 0) {
+            navDestination(DESTINATION_ID) {}
+        }
+        fail("NavGraph should throw IllegalStateException if startDestination is zero")
+    }
+
+    @Test
+    fun navigationNested() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            navigation(DESTINATION_ID, startDestination = SECOND_DESTINATION_ID) {
+                navDestination(SECOND_DESTINATION_ID) {}
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val SECOND_DESTINATION_ID = 2
+
+/**
+ * Create a base NavDestination. Generally, only subtypes of NavDestination should be
+ * added to a NavGraph (hence why this is not in the common-ktx library)
+ */
+fun NavGraphBuilder.navDestination(
+        @IdRes id: Int,
+        block: NavDestinationBuilder<NavDestination>.() -> Unit
+) = destination(NavDestinationBuilder(provider[TestNavigator::class], id).apply(block))
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt
new file mode 100644
index 0000000..ebc11a7
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavGraphTest {
+    private val navGraphNavigator = NavGraphNavigator(InstrumentationRegistry.getTargetContext())
+    private val navigator = TestNavigator()
+
+    @Test
+    fun plusAssign() {
+        val graph = NavGraph(navGraphNavigator)
+        val destination = NavDestination(navigator).apply { id = DESTINATION_ID }
+        graph += destination
+        assertSame("plusAssign destination should be retrieved with get", destination,
+                graph[DESTINATION_ID])
+    }
+
+    @Test
+    fun minusAssign() {
+        val graph = NavGraph(navGraphNavigator)
+        val destination = NavDestination(navigator).apply { id = DESTINATION_ID }
+        graph += destination
+        assertSame("plusAssign destination should be retrieved with get", destination,
+                graph[DESTINATION_ID])
+        graph -= destination
+        assertFalse("Destination should be removed after minusAssign",
+                DESTINATION_ID in graph)
+    }
+
+    @Test
+    fun plusAssignGraph() {
+        val graph = NavGraph(navGraphNavigator)
+        val other = NavGraph(navGraphNavigator)
+        other += NavDestination(navigator).apply { id = DESTINATION_ID }
+        other += NavDestination(navigator).apply { id = SECOND_DESTINATION_ID }
+        graph += other
+        assertTrue("NavGraph should have destination1 from other",
+                DESTINATION_ID in graph)
+        assertFalse("other nav graph should not have destination1",
+                DESTINATION_ID in other)
+
+        assertTrue("NavGraph should have destination2 from other",
+                SECOND_DESTINATION_ID in graph)
+        assertFalse("other nav graph should not have destination2",
+                SECOND_DESTINATION_ID in other)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun getIllegalArgumentException() {
+        val graph = NavGraph(navGraphNavigator)
+        graph[DESTINATION_ID]
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val SECOND_DESTINATION_ID = 2
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt
new file mode 100644
index 0000000..c33afe8
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavOptionsTest {
+
+    @Test
+    fun launchSingleTop() {
+        val navOptions = navOptions {
+            launchSingleTop = true
+        }
+        assertTrue("NavOptions should have launchSingleTop set",
+                navOptions.shouldLaunchSingleTop())
+    }
+
+    @Test
+    fun launchDocument() {
+        val navOptions = navOptions {
+            launchDocument = true
+        }
+        assertTrue("NavOptions should have launchDocument set",
+                navOptions.shouldLaunchDocument())
+    }
+
+    @Test
+    fun clearTask() {
+        val navOptions = navOptions {
+            clearTask = true
+        }
+        assertTrue("NavOptions should have clearTask set",
+                navOptions.shouldClearTask())
+    }
+
+    @Test
+    fun popUpTo() {
+        val navOptions = navOptions {
+            popUpTo = DESTINATION_ID
+        }
+        assertEquals("NavOptions should have popUpTo destination id set",
+                DESTINATION_ID, navOptions.popUpTo)
+        assertFalse("NavOptions should have isPopUpToInclusive false by default",
+                navOptions.isPopUpToInclusive)
+    }
+
+    @Test
+    fun popUpToInclusive() {
+        val navOptions = navOptions {
+            popUpTo(DESTINATION_ID) {
+                inclusive = true
+            }
+        }
+        assertEquals("NavOptions should have popUpTo destination id set",
+                DESTINATION_ID, navOptions.popUpTo)
+        assertTrue("NavOptions should have isPopUpToInclusive set",
+                navOptions.isPopUpToInclusive)
+    }
+
+    @Test
+    fun anim() {
+        val navOptions = navOptions {
+            anim {
+                enter = ENTER_ANIM_ID
+                exit = EXIT_ANIM_ID
+                popEnter = POP_ENTER_ANIM_ID
+                popExit = POP_EXIT_ANIM_ID
+            }
+        }
+        assertEquals("NavOptions should have enter animation set",
+                ENTER_ANIM_ID, navOptions.enterAnim)
+        assertEquals("NavOptions should have exit animation set",
+                EXIT_ANIM_ID, navOptions.exitAnim)
+        assertEquals("NavOptions should have pop enter animation set",
+                POP_ENTER_ANIM_ID, navOptions.popEnterAnim)
+        assertEquals("NavOptions should have pop exit animation set",
+                POP_EXIT_ANIM_ID, navOptions.popExitAnim)
+    }
+}
+
+private const val DESTINATION_ID = 1
+
+private const val ENTER_ANIM_ID = 10
+private const val EXIT_ANIM_ID = 11
+private const val POP_ENTER_ANIM_ID = 12
+private const val POP_EXIT_ANIM_ID = 13
diff --git a/navigation/common/ktx/src/main/AndroidManifest.xml b/navigation/common/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..799fc93
--- /dev/null
+++ b/navigation/common/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+<manifest package="androidx.navigation.ktx"/>
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt
new file mode 100644
index 0000000..55cc97f
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.navigation
+
+import android.os.Bundle
+import android.support.annotation.IdRes
+
+@DslMarker
+annotation class NavDestinationDsl
+
+/**
+ * DSL for constructing a new [NavDestination]
+ */
+@NavDestinationDsl
+open class NavDestinationBuilder<out D : NavDestination>(
+        protected val navigator: Navigator<out D>,
+        @IdRes val id: Int
+) {
+    /**
+     * The descriptive label of the destination
+     */
+    var label: CharSequence? = null
+
+    /**
+     * The default arguments that should be passed to the destination
+     */
+    var defaultArguments: Bundle? = null
+
+    private var deepLinks = mutableListOf<String>()
+
+    /**
+     * Add a deep link to this destination.
+     *
+     * In addition to a direct Uri match, the following features are supported:
+     *
+     * *    Uris without a scheme are assumed as http and https. For example,
+     *      `www.example.com` will match `http://www.example.com` and
+     *      `https://www.example.com`.
+     * *    Placeholders in the form of `{placeholder_name}` matches 1 or more
+     *      characters. The String value of the placeholder will be available in the arguments
+     *      [Bundle] with a key of the same name. For example,
+     *      `http://www.example.com/users/{id}` will match
+     *      `http://www.example.com/users/4`.
+     * *    The `.*` wildcard can be used to match 0 or more characters.
+     *
+     * @param uriPattern The uri pattern to add as a deep link
+     */
+    fun deepLink(uriPattern: String) {
+        deepLinks.add(uriPattern)
+    }
+
+    private var actions = mutableMapOf<Int, NavAction>()
+
+    /**
+     * Adds a new [NavAction] to the destination
+     */
+    fun action(actionId: Int, block: NavActionBuilder.() -> Unit) {
+        actions[actionId] = NavActionBuilder().apply(block).build()
+    }
+
+    /**
+     * Build the NavDestination by calling [Navigator.createDestination].
+     */
+    open fun build(): D {
+        return navigator.createDestination().also { destination ->
+            destination.id = id
+            destination.label = label
+            destination.setDefaultArguments(defaultArguments)
+            deepLinks.forEach { deepLink ->
+                destination.addDeepLink(deepLink)
+            }
+            actions.forEach { actionId, action ->
+                destination.putAction(actionId, action)
+            }
+        }
+    }
+}
+
+/**
+ * DSL for building a [NavAction].
+ */
+@NavDestinationDsl
+class NavActionBuilder {
+    /**
+     * The ID of the destination that should be navigated to when this action is used
+     */
+    var destinationId: Int = 0
+
+    private var navOptions: NavOptions? = null
+
+    /**
+     * Sets the [NavOptions] for this action that should be used by default
+     */
+    fun navOptions(block: NavOptionsBuilder.() -> Unit) {
+        navOptions = NavOptionsBuilder().apply(block).build()
+    }
+
+    internal fun build() = NavAction(destinationId, navOptions)
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt
new file mode 100644
index 0000000..eaf2775
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Returns the destination with `id`.
+ *
+ * @throws IllegalArgumentException if no destination is found with that id.
+ */
+inline operator fun NavGraph.get(@IdRes id: Int): NavDestination =
+        findNode(id) ?: throw IllegalArgumentException("No destination for $id was found in $this")
+
+/** Returns `true` if a destination with `id` is found in this navigation graph. */
+operator fun NavGraph.contains(@IdRes id: Int): Boolean = findNode(id) != null
+
+/**
+ * Adds a destination to this NavGraph. The destination must have an
+ * [id][NavDestination.getId] set.
+ *
+ * The destination must not have a [parent][NavDestination.getParent] set. If
+ * the destination is already part of a [NavGraph], call
+ * [NavGraph.remove] before calling this method.</p>
+ *
+ * @param node destination to add
+ */
+inline operator fun NavGraph.plusAssign(node: NavDestination) {
+    addDestination(node)
+}
+
+/**
+ * Add all destinations from another collection to this one. As each destination has at most
+ * one parent, the destinations will be removed from the given NavGraph.
+ *
+ * @param other collection of destinations to add. All destinations will be removed from the
+ * parameter graph after being added to this graph.
+ */
+inline operator fun NavGraph.plusAssign(other: NavGraph) {
+    addAll(other)
+}
+
+/** Removes `node` from this navigation graph. */
+inline operator fun NavGraph.minusAssign(node: NavDestination) {
+    remove(node)
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt
new file mode 100644
index 0000000..97579ec
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavigatorProvider.navigation(
+        @IdRes id: Int = 0,
+        @IdRes startDestination: Int,
+        block: NavGraphBuilder.() -> Unit
+) = NavGraphBuilder(this, id, startDestination).apply(block).build()
+
+/**
+ * Construct a nested [NavGraph]
+ */
+inline fun NavGraphBuilder.navigation(
+        @IdRes id: Int,
+        @IdRes startDestination: Int,
+        block: NavGraphBuilder.() -> Unit
+) = destination(NavGraphBuilder(provider, id, startDestination).apply(block))
+
+/**
+ * DSL for constructing a new [NavGraph]
+ */
+@NavDestinationDsl
+class NavGraphBuilder(
+        val provider: NavigatorProvider,
+        @IdRes id: Int,
+        @IdRes private var startDestination: Int
+) : NavDestinationBuilder<NavGraph>(provider[NavGraphNavigator::class], id) {
+    private val destinations = mutableListOf<NavDestination>()
+
+    /**
+     * Build and add a new destination to the [NavGraphBuilder]
+     */
+    fun <D : NavDestination> destination(navDestination: NavDestinationBuilder<D>) {
+        destinations += navDestination.build()
+    }
+
+    /**
+     * Adds this destination to the [NavGraphBuilder]
+     */
+    operator fun NavDestination.unaryPlus() {
+        addDestination(this)
+    }
+
+    /**
+     * Add the destination to the [NavGraphBuilder]
+     */
+    fun addDestination(destination: NavDestination) {
+        destinations += destination
+    }
+
+    override fun build(): NavGraph = super.build().also { navGraph ->
+        navGraph.addDestinations(destinations)
+        if (startDestination == 0) {
+            throw IllegalStateException("You must set a startDestination")
+        }
+        navGraph.startDestination = startDestination
+    }
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt
new file mode 100644
index 0000000..5b4781d
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt
@@ -0,0 +1,160 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.annotation.AnimRes
+import android.support.annotation.AnimatorRes
+import android.support.annotation.IdRes
+
+@DslMarker
+annotation class NavOptionsDsl
+
+/**
+ * Construct a new [NavOptions]
+ */
+fun navOptions(block: NavOptionsBuilder.() -> Unit): NavOptions =
+        NavOptionsBuilder().apply(block).build()
+
+/**
+ * DSL for constructing a new [NavOptions]
+ */
+@NavOptionsDsl
+class NavOptionsBuilder {
+    private val builder = NavOptions.Builder()
+
+    /**
+     * Whether this navigation action should launch as single-top (i.e., there will be at most
+     * one copy of a given destination on the top of the back stack).
+     *
+     * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP]
+     * works with activites.
+     */
+    var launchSingleTop = false
+
+    /**
+     * Whether this navigation action should launch the destination in a new document.
+     *
+     * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT]
+     * works with activites.
+     */
+    var launchDocument = false
+
+    /**
+     * Whether this navigation action should clear the entire back stack
+     *
+     * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK]
+     * works with activites.
+     */
+    var clearTask = false
+
+    /**
+     * Pop up to a given destination before navigating. This pops all non-matching destinations
+     * from the back stack until this destination is found.
+     */
+    @IdRes
+    var popUpTo: Int = 0
+        set(value) {
+            field = value
+            inclusive = false
+        }
+    private var inclusive = false
+
+    /**
+     * Pop up to a given destination before navigating. This pops all non-matching destinations
+     * from the back stack until this destination is found.
+     */
+    fun popUpTo(@IdRes id: Int, block: PopUpToBuilder.() -> Unit) {
+        popUpTo = id
+        inclusive = PopUpToBuilder().apply(block).inclusive
+    }
+
+    /**
+     * Sets any custom Animation or Animator resources that should be used.
+     *
+     * Note: Animator resources are not supported for navigating to a new Activity
+     */
+    fun anim(block: AnimBuilder.() -> Unit) {
+        AnimBuilder().apply(block).run {
+            this@NavOptionsBuilder.builder.setEnterAnim(enter)
+                    .setExitAnim(exit)
+                    .setPopEnterAnim(popEnter)
+                    .setPopExitAnim(popExit)
+        }
+    }
+
+    internal fun build() = builder.apply {
+        setLaunchSingleTop(launchSingleTop)
+        setLaunchDocument(launchDocument)
+        setClearTask(clearTask)
+        setPopUpTo(popUpTo, inclusive)
+    }.build()
+}
+
+/**
+ * DSL for customizing [NavOptionsBuilder.popUpTo] operations.
+ */
+@NavOptionsDsl
+class PopUpToBuilder {
+    /**
+     * Whether the `popUpTo` destination should be popped from the back stack.
+     */
+    var inclusive: Boolean = false
+}
+
+/**
+ * DSL for setting custom Animation or Animator resources on a [NavOptionsBuilder]
+ */
+@NavOptionsDsl
+class AnimBuilder {
+    /**
+     * The custom Animation or Animator resource for the enter animation.
+     *
+     * Note: Animator resources are not supported for navigating to a new Activity
+     */
+    @AnimRes
+    @AnimatorRes
+    var enter = -1
+
+    /**
+     * The custom Animation or Animator resource for the exit animation.
+     *
+     * Note: Animator resources are not supported for navigating to a new Activity
+     */
+    @AnimRes
+    @AnimatorRes
+    var exit = -1
+
+    /**
+     * The custom Animation or Animator resource for the enter animation
+     * when popping off the back stack.
+     *
+     * Note: Animator resources are not supported for navigating to a new Activity
+     */
+    @AnimRes
+    @AnimatorRes
+    var popEnter = -1
+
+    /**
+     * The custom Animation or Animator resource for the exit animation
+     * when popping off the back stack.
+     *
+     * Note: Animator resources are not supported for navigating to a new Activity
+     */
+    @AnimRes
+    @AnimatorRes
+    var popExit = -1
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt
new file mode 100644
index 0000000..ec43d45
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import kotlin.reflect.KClass
+
+/**
+ * Retrieves a registered [Navigator] by name.
+ *
+ * @throws IllegalStateException if the Navigator has not been added
+ */
+inline operator fun <D : NavDestination, T : Navigator<D>> NavigatorProvider.get(name: String): T =
+        getNavigator(name)
+
+/**
+ * Retrieves a registered [Navigator] using the name provided by the
+ * [Navigator.Name annotation][Navigator.Name].
+ *
+ * @throws IllegalStateException if the Navigator has not been added
+ */
+inline operator fun <D : NavDestination, T : Navigator<D>> NavigatorProvider.get(
+        clazz: KClass<T>
+): T = getNavigator(clazz.java)
+
+/**
+ * Register a [Navigator] by name. If a navigator by this name is already
+ * registered, this new navigator will replace it.
+ *
+ * @return the previously added [Navigator] for the given name, if any
+ */
+inline operator fun <D : NavDestination> NavigatorProvider.set(
+        name: String,
+        navigator: Navigator<D>
+) = addNavigator(name, navigator)
+
+/**
+ * Register a navigator using the name provided by the
+ * [Navigator.Name annotation][Navigator.Name].
+ */
+inline operator fun <D : NavDestination> NavigatorProvider.plusAssign(navigator: Navigator<D>) {
+    addNavigator(navigator)
+}
diff --git a/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt b/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt
new file mode 100644
index 0000000..95d7324
--- /dev/null
+++ b/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import org.junit.Assert.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class NavigatorProviderTest {
+    private val provider = SimpleNavigatorProvider()
+
+    @Test
+    fun set() {
+        val navigator = TestNavigator()
+        provider[NAME] = navigator
+        val foundNavigator: Navigator<NavDestination> = provider[NAME]
+        assertSame("Set destination should be retrieved with get", navigator,
+                foundNavigator)
+    }
+
+    @Test
+    fun plusAssign() {
+        val navigator = TestNavigator()
+        provider += navigator
+        assertSame("Set destination should be retrieved with get", navigator,
+                provider[TestNavigator::class])
+    }
+}
+
+private const val NAME = "TEST"
diff --git a/navigation/fragment/ktx/build.gradle b/navigation/fragment/ktx/build.gradle
new file mode 100644
index 0000000..9c03b83
--- /dev/null
+++ b/navigation/fragment/ktx/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":navigation:fragment"))
+    // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+    api(project(":navigation:runtime-ktx"))
+    api(KOTLIN_STDLIB)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+    name = "Android Navigation Fragment Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.NAVIGATION
+    mavenGroup = LibraryGroups.NAVIGATION
+    inceptionYear = "2018"
+    description = "Android Navigation-Fragment-Ktx"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
+
diff --git a/navigation/fragment/ktx/src/androidTest/AndroidManifest.xml b/navigation/fragment/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..54df6d4
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.navigation.fragment.ktx.test">
+    <application>
+        <activity android:name="androidx.navigation.fragment.TestActivity" />
+    </application>
+</manifest>
diff --git a/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..5c2a9c1
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 androidx.navigation.fragment
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.support.test.runner.AndroidJUnit4
+import android.support.v4.app.Fragment
+import androidx.navigation.contains
+import androidx.navigation.createGraph
+import androidx.navigation.get
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TestNavigatorDestinationBuilderTest {
+    @get:Rule
+    val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+    private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+    @UiThreadTest
+    @Test fun fragment() {
+        val navHostFragment = NavHostFragment()
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, navHostFragment)
+                .commitNow()
+        val graph = navHostFragment.createGraph(startDestination = DESTINATION_ID) {
+            fragment<BuilderTestFragment>(DESTINATION_ID)
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Fragment class should be set to BuilderTestFragment",
+                BuilderTestFragment::class.java,
+                (graph[DESTINATION_ID] as FragmentNavigator.Destination).fragmentClass)
+    }
+
+    @UiThreadTest
+    @Test fun fragmentWithBody() {
+        val navHostFragment = NavHostFragment()
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, navHostFragment)
+                .commitNow()
+        val graph = navHostFragment.createGraph(startDestination = DESTINATION_ID) {
+            fragment<BuilderTestFragment>(DESTINATION_ID) {
+                label = LABEL
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Fragment class should be set to BuilderTestFragment",
+                BuilderTestFragment::class.java,
+                (graph[DESTINATION_ID] as FragmentNavigator.Destination).fragmentClass)
+        assertEquals("Fragment should have label set",
+                LABEL, graph[DESTINATION_ID].label)
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
+class BuilderTestFragment : Fragment()
diff --git a/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt
new file mode 100644
index 0000000..2540852
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.navigation.fragment
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentActivity
+import androidx.navigation.fragment.ktx.test.R
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class ActivityTest {
+    @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+    private val fragmentManager get() = activityRule.activity.supportFragmentManager
+    private val contentFragment get() = fragmentManager.findFragmentById(android.R.id.content)
+
+    @UiThreadTest
+    @Test fun navController() {
+        val navHostFragment = NavHostFragment.create(R.navigation.test_graph)
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, navHostFragment)
+                .commitNow()
+        assertTrue("Fragment should have NavController set",
+                contentFragment.navController == navHostFragment.navController)
+    }
+
+    @UiThreadTest
+    @Test fun navControllerNull() {
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, TestFragment())
+                .commitNow()
+        try {
+            contentFragment.navController
+            fail("navController should throw IllegalStateException if a NavController was not set")
+        } catch (e: IllegalStateException) {
+            // Expected
+        }
+    }
+
+    @UiThreadTest
+    @Test fun findNavController() {
+        val navHostFragment = NavHostFragment.create(R.navigation.test_graph)
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, navHostFragment)
+                .commitNow()
+
+        val foundNavController = contentFragment.findNavController()
+        assertNotNull("findNavController should return non-null if a NavController was set",
+                foundNavController)
+        assertTrue("Fragment should have NavController set",
+                foundNavController == navHostFragment.navController)
+    }
+
+    @UiThreadTest
+    @Test fun findNavControllerNull() {
+        fragmentManager.beginTransaction()
+                .add(android.R.id.content, TestFragment())
+                .commitNow()
+        assertNull("findNavController should return null if a NavController was never set",
+                contentFragment.findNavController())
+    }
+}
+
+class TestActivity : FragmentActivity()
+class TestFragment : Fragment()
diff --git a/navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml b/navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml
new file mode 100644
index 0000000..5f1268a
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<navigation
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:startDestination="@+id/start">
+    <fragment
+        android:id="@+id/start"
+        android:name="androidx.navigation.fragment.TestFragment"/>
+</navigation>
diff --git a/navigation/fragment/ktx/src/main/AndroidManifest.xml b/navigation/fragment/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bd32886
--- /dev/null
+++ b/navigation/fragment/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+
+<manifest package="androidx.navigation.fragment.ktx"/>
diff --git a/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt
new file mode 100644
index 0000000..41e6e43
--- /dev/null
+++ b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 androidx.navigation.fragment
+
+import android.support.v4.app.Fragment
+import androidx.navigation.NavController
+
+/**
+ * Find a [NavController] given a [Fragment]
+ */
+fun Fragment.findNavController(): NavController? =
+        NavHostFragment.findNavController(this)
+
+/**
+ * Gets the [NavController] given a [Fragment].
+ *
+ * Calling this on a Fragment that is not a [NavHostFragment] or within a [NavHostFragment]
+ * will result in an [IllegalStateException]
+ */
+val Fragment.navController: NavController
+        get() = NavHostFragment.getNavController(this)
diff --git a/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..a188bf0
--- /dev/null
+++ b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 androidx.navigation.fragment
+
+import android.support.annotation.IdRes
+import android.support.v4.app.Fragment
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+import kotlin.reflect.KClass
+
+/**
+ * Construct a new [FragmentNavigator.Destination]
+ */
+inline fun <reified F : Fragment> NavGraphBuilder.fragment(@IdRes id: Int) = fragment<F>(id) {}
+
+/**
+ * Construct a new [FragmentNavigator.Destination]
+ */
+inline fun <reified F : Fragment> NavGraphBuilder.fragment(
+        @IdRes id: Int,
+        block: FragmentNavigatorDestinationBuilder.() -> Unit
+) = destination(FragmentNavigatorDestinationBuilder(
+        provider[FragmentNavigator::class],
+        id,
+        F::class
+).apply(block))
+
+/**
+ * DSL for constructing a new [FragmentNavigator.Destination]
+ */
+@NavDestinationDsl
+class FragmentNavigatorDestinationBuilder(
+        navigator: FragmentNavigator,
+        @IdRes id: Int,
+        private val fragmentClass: KClass<out Fragment>
+) : NavDestinationBuilder<FragmentNavigator.Destination>(navigator, id) {
+
+    override fun build(): FragmentNavigator.Destination =
+            super.build().also { destination ->
+                destination.fragmentClass = fragmentClass.java
+            }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java
index 7be585f..456b47a 100644
--- a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java
@@ -17,6 +17,7 @@
 package androidx.navigation.fragment.test;
 
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v4.app.FragmentActivity;
 
@@ -36,7 +37,8 @@
         super.onCreate(savedInstanceState);
     }
 
+    @NonNull
     public NavController getNavController() {
-        return Navigation.findNavController(this, R.id.nav_host);
+        return Navigation.getNavController(this, R.id.nav_host);
     }
 }
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
index a1783a6..8f6dfe5 100644
--- a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
@@ -91,6 +91,7 @@
      * @param fragment the locally scoped Fragment for navigation
      * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
      */
+    @Nullable
     public static NavController findNavController(@Nullable Fragment fragment) {
         if (fragment == null) {
             return null;
@@ -110,7 +111,31 @@
         }
 
         // Try looking for one associated with the view instead, if applicable
-        return Navigation.findNavController(fragment.getView());
+        View view = fragment.getView();
+        return view != null ? Navigation.findNavController(view) : null;
+    }
+
+    /**
+     * Gets the {@link NavController} given a local {@link Fragment}.
+     *
+     * <p>This method will locate the {@link NavController} associated with this Fragment,
+     * looking first for a {@link NavHostFragment} along the given Fragment's parent chain.
+     * If a {@link NavController} is not found, this method will look for one along this
+     * Fragment's {@link Fragment#getView() view hierarchy} as specified by
+     * {@link Navigation#findNavController(View)}.</p>
+     *
+     * @param fragment the locally scoped Fragment for navigation
+     * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
+     * @throws IllegalStateException if the given Fragment does not correspond with a
+     * {@link NavHost} or is not within a NavHost.
+     */
+    public static NavController getNavController(@Nullable Fragment fragment) {
+        NavController navController = findNavController(fragment);
+        if (navController == null) {
+            throw new IllegalStateException("Fragment " + fragment
+                    + " does not have a NavController set");
+        }
+        return navController;
     }
 
     private NavController mNavController;
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java
index 985645c..1c8e768 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java
@@ -58,7 +58,7 @@
                 EditText editArgs = view.findViewById(R.id.edit_args);
                 Bundle args = new Bundle();
                 args.putString("myarg", editArgs.getText().toString());
-                PendingIntent deeplink = Navigation.findNavController(v).createDeepLink()
+                PendingIntent deeplink = Navigation.getNavController(v).createDeepLink()
                         .setDestination(R.id.android)
                         .setArguments(args)
                         .createPendingIntent();
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java
index 3a78c30..039af32 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java
@@ -98,14 +98,14 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         return NavigationUI.onNavDestinationSelected(
-                item, Navigation.findNavController(this, R.id.my_nav_host_fragment))
+                item, Navigation.getNavController(this, R.id.my_nav_host_fragment))
                 || super.onOptionsItemSelected(item);
     }
 
     @Override
     public boolean onSupportNavigateUp() {
         return NavigationUI.navigateUp(
-                mDrawerLayout, Navigation.findNavController(this, R.id.my_nav_host_fragment)
+                mDrawerLayout, Navigation.getNavController(this, R.id.my_nav_host_fragment)
         );
     }
 }
diff --git a/navigation/runtime/ktx/build.gradle b/navigation/runtime/ktx/build.gradle
new file mode 100644
index 0000000..f9e6eaa
--- /dev/null
+++ b/navigation/runtime/ktx/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":navigation:runtime"))
+    // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+    api(project(":navigation:common-ktx"))
+    api(KOTLIN_STDLIB)
+    androidTestImplementation(project(":navigation:testing-ktx"))
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+    name = "Android Navigation Runtime Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.NAVIGATION
+    mavenGroup = LibraryGroups.NAVIGATION
+    inceptionYear = "2018"
+    description = "Android Navigation-Runtime-Ktx"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/runtime/ktx/src/androidTest/AndroidManifest.xml b/navigation/runtime/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..a1a7c59
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.navigation.ktx.test">
+    <application>
+        <activity android:name="androidx.navigation.TestActivity"/>
+    </application>
+</manifest>
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..53704eb
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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 androidx.navigation
+
+import android.net.Uri
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import junit.framework.Assert.assertTrue
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ActivityNavigatorDestinationBuilderTest {
+    private val navController = NavController(InstrumentationRegistry.getTargetContext())
+
+    @Test
+    fun activity() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            activity(DESTINATION_ID) {
+                label = LABEL
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have label set",
+                LABEL,
+                graph[DESTINATION_ID].label)
+    }
+
+    @Test
+    fun activityClass() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            activity(DESTINATION_ID) {
+                activityClass = TestActivity::class
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have ComponentName set",
+                TestActivity::class.java.name,
+                (graph[DESTINATION_ID] as ActivityNavigator.Destination).component.className)
+    }
+
+    @Test
+    fun action() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            activity(DESTINATION_ID) {
+                action = ACTION
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have action set",
+                ACTION,
+                (graph[DESTINATION_ID] as ActivityNavigator.Destination).action)
+    }
+
+    @Test
+    fun data() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            activity(DESTINATION_ID) {
+                data = DATA
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have data set",
+                DATA,
+                (graph[DESTINATION_ID] as ActivityNavigator.Destination).data)
+    }
+
+    @Test
+    fun dataPattern() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            activity(DESTINATION_ID) {
+                dataPattern = DATA_PATTERN
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have data pattern set",
+                DATA_PATTERN,
+                (graph[DESTINATION_ID] as ActivityNavigator.Destination).dataPattern)
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
+private const val ACTION = "ACTION_TEST"
+private val DATA = Uri.parse("http://www.example.com")
+private const val DATA_PATTERN = "http://www.example.com/{id}"
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt
new file mode 100644
index 0000000..6468d9a
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 androidx.navigation
+
+import android.app.Activity
+import android.os.Bundle
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.view.View
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class ActivityTest {
+    @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+    private val view get() = activityRule.activity.findViewById<View>(VIEW_ID)
+
+    @Test fun navController() {
+        val navController = NavController(activityRule.activity)
+        view.navController = navController
+        assertTrue("View should have NavController set",
+                activityRule.activity.navController(VIEW_ID) == navController)
+    }
+
+    @Test fun navControllerNull() {
+        try {
+            activityRule.activity.navController(VIEW_ID)
+            fail("navController should throw IllegalStateException if a NavController was not set")
+        } catch (e: IllegalStateException) {
+            // Expected
+        }
+    }
+
+    @Test fun findNavController() {
+        val navController = NavController(activityRule.activity)
+        view.navController = navController
+
+        val foundNavController = activityRule.activity.findNavController(VIEW_ID)
+        assertNotNull("findNavController should return non-null if a NavController was set",
+                foundNavController)
+        assertTrue("View should have NavController set",
+                foundNavController == navController)
+    }
+
+    @Test fun findNavControllerNull() {
+        assertNull("findNavController should return null if a NavController was never set",
+                activityRule.activity.findNavController(VIEW_ID))
+    }
+}
+
+private const val VIEW_ID = 1
+
+class TestActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(View(this).apply {
+            id = VIEW_ID
+        })
+    }
+}
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt
new file mode 100644
index 0000000..bf6f332
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.test
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class NavControllerTest {
+    private val navController = NavController(InstrumentationRegistry.getTargetContext()).apply {
+        navigatorProvider += TestNavigator()
+    }
+
+    @Test
+    fun createGraph() {
+        val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+            test(DESTINATION_ID)
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+}
+
+private const val DESTINATION_ID = 1
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt
new file mode 100644
index 0000000..7a74ab2
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.test
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class NavHostTest {
+    private val navController = NavController(InstrumentationRegistry.getTargetContext()).apply {
+        navigatorProvider += TestNavigator()
+    }
+    private val navHost = NavHost { this@NavHostTest.navController }
+
+    @Test
+    fun createGraph() {
+        val graph = navHost.createGraph(startDestination = DESTINATION_ID) {
+            test(DESTINATION_ID)
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+}
+
+private const val DESTINATION_ID = 1
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt
new file mode 100644
index 0000000..f6d1978
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import android.view.View
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+    @Test fun navController() {
+        val view = View(InstrumentationRegistry.getTargetContext())
+        val navController = NavController(InstrumentationRegistry.getTargetContext())
+        view.navController = navController
+        assertTrue("View should have NavController set",
+                view.navController == navController)
+    }
+
+    @Test fun navControllerNull() {
+        val view = View(InstrumentationRegistry.getTargetContext())
+        try {
+            view.navController
+            fail("navController should throw IllegalStateException if a NavController was not set")
+        } catch (e: IllegalStateException) {
+            // Expected
+        }
+    }
+
+    @Test fun findNavController() {
+        val view = View(InstrumentationRegistry.getTargetContext())
+        val navController = NavController(InstrumentationRegistry.getTargetContext())
+        view.navController = navController
+
+        val foundNavController = view.findNavController()
+        assertNotNull("findNavController should return non-null if a NavController was set",
+                foundNavController)
+        assertTrue("View should have NavController set",
+                foundNavController == navController)
+    }
+
+    @Test fun findNavControllerNull() {
+        val view = View(InstrumentationRegistry.getTargetContext())
+        assertNull("findNavController should return null if a NavController was never set",
+                view.findNavController())
+    }
+}
diff --git a/navigation/runtime/ktx/src/main/AndroidManifest.xml b/navigation/runtime/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..799fc93
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+<manifest package="androidx.navigation.ktx"/>
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt
new file mode 100644
index 0000000..dff618a
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.navigation
+
+import android.app.Activity
+import android.support.annotation.IdRes
+
+/**
+ * Find a [NavController] given the id of a View and its containing
+ * [Activity].
+ */
+fun Activity.findNavController(@IdRes viewId: Int): NavController? =
+        Navigation.findNavController(this, viewId)
+
+/**
+ * Gets the [NavController] given the id of a View and its containing
+ * [Activity].
+ *
+ * Calling this on a View that is not a [NavHost] or within a [NavHost]
+ * will result in an [IllegalStateException]
+ */
+fun Activity.navController(@IdRes viewId: Int): NavController =
+        Navigation.getNavController(this, viewId)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..6de930d
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import android.app.Activity
+import android.content.ComponentName
+import android.net.Uri
+import android.support.annotation.IdRes
+import kotlin.reflect.KClass
+
+/**
+ * Construct a new [ActivityNavigator.Destination]
+ */
+inline fun NavGraphBuilder.activity(
+        @IdRes id: Int,
+        block: ActivityNavigatorDestinationBuilder.() -> Unit
+) = destination(ActivityNavigatorDestinationBuilder(
+        provider[ActivityNavigator::class],
+        id
+).apply(block))
+
+/**
+ * DSL for constructing a new [ActivityNavigator.Destination]
+ */
+@NavDestinationDsl
+class ActivityNavigatorDestinationBuilder(
+        navigator: ActivityNavigator,
+        @IdRes id: Int
+) : NavDestinationBuilder<ActivityNavigator.Destination>(navigator, id) {
+    private val context = navigator.context
+
+    var activityClass: KClass<out Activity>? = null
+
+    var action: String? = null
+
+    var data: Uri? = null
+
+    var dataPattern: String? = null
+
+    override fun build(): ActivityNavigator.Destination =
+            super.build().also { destination ->
+                activityClass?.let { clazz ->
+                    destination.setComponentName(ComponentName(context, clazz.java))
+                }
+                destination.action = action
+                destination.data = data
+                destination.dataPattern = dataPattern
+            }
+}
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt
new file mode 100644
index 0000000..76fb4dd
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavController.createGraph(
+        @IdRes id: Int = 0,
+        @IdRes startDestination: Int,
+        block: NavGraphBuilder.() -> Unit
+): NavGraph = navigatorProvider.navigation(id, startDestination, block)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt
new file mode 100644
index 0000000..1a3bc05
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 androidx.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavHost.createGraph(
+        @IdRes id: Int = 0,
+        @IdRes startDestination: Int,
+        block: NavGraphBuilder.() -> Unit
+): NavGraph = navController.createGraph(id, startDestination, block)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt
new file mode 100644
index 0000000..2f5cd6c
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.navigation
+
+import android.view.View
+
+/**
+ * Find a [NavController] associated with a [View].
+ */
+fun View.findNavController(): NavController? =
+        Navigation.findNavController(this)
+
+/**
+ * Property for the [NavController] associated with a [View].
+ *
+ * Calling view.navController on a View not within a [NavHost] will result in an
+ * [IllegalStateException]
+ */
+var View.navController: NavController
+    get() = Navigation.getNavController(this)
+    set(value) {
+        Navigation.setViewNavController(this, value)
+    }
diff --git a/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java b/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java
index 6e36f2a..f8f9092 100644
--- a/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java
+++ b/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java
@@ -58,6 +58,11 @@
     }
 
     @NonNull
+    Context getContext() {
+        return mContext;
+    }
+
+    @NonNull
     @Override
     public Destination createDestination() {
         return new Destination(this);
diff --git a/navigation/runtime/src/main/java/androidx/navigation/Navigation.java b/navigation/runtime/src/main/java/androidx/navigation/Navigation.java
index b1e4fd9..f1e3683 100644
--- a/navigation/runtime/src/main/java/androidx/navigation/Navigation.java
+++ b/navigation/runtime/src/main/java/androidx/navigation/Navigation.java
@@ -55,6 +55,29 @@
     }
 
     /**
+     * Gets the {@link NavController} given the id of a View and its containing
+     * {@link Activity}. This is a convenience wrapper around {@link #getNavController(View)}.
+     *
+     * <p>This method will locate the {@link NavController} associated with this view.
+     * This is automatically populated for the id of a {@link NavHost} and its children.</p>
+     *
+     * @param activity The Activity hosting the view
+     * @param viewId The id of the view to search from
+     * @return the {@link NavController} associated with the view referenced by id
+     * @throws IllegalStateException if the given viewId does not correspond with a
+     * {@link NavHost} or is not within a NavHost.
+     */
+    @NonNull
+    public static NavController getNavController(@NonNull Activity activity, @IdRes int viewId) {
+        NavController navController = findNavController(activity, viewId);
+        if (navController == null) {
+            throw new IllegalStateException("Activity " + activity
+                    + " does not have a NavController set on " + viewId);
+        }
+        return navController;
+    }
+
+    /**
      * Find a {@link NavController} given a local {@link View}.
      *
      * <p>This method will locate the {@link NavController} associated with this view.
@@ -65,10 +88,8 @@
      * @param view the view to search from
      * @return the locally scoped {@link NavController} to the given view
      */
-    public static NavController findNavController(@Nullable View view) {
-        if (view == null) {
-            return null;
-        }
+    @Nullable
+    public static NavController findNavController(@NonNull View view) {
         while (view != null) {
             NavController controller = getViewNavController(view);
             if (controller != null) {
@@ -81,6 +102,28 @@
     }
 
     /**
+     * Gets the {@link NavController} given a local {@link View}.
+     *
+     * <p>This method will locate the {@link NavController} associated with this view.
+     * This is automatically populated for views that are managed by a {@link NavHost}
+     * and is intended for use by various {@link android.view.View.OnClickListener listener}
+     * interfaces.</p>
+     *
+     * @param view the view to search from
+     * @return the locally scoped {@link NavController} to the given view
+     * @throws IllegalStateException if the given view does not correspond with a
+     * {@link NavHost} or is not within a NavHost.
+     */
+    @NonNull
+    public static NavController getNavController(@NonNull View view) {
+        NavController navController = findNavController(view);
+        if (navController == null) {
+            throw new IllegalStateException("View " + view + " does not have a NavController set");
+        }
+        return navController;
+    }
+
+    /**
      * Create an {@link android.view.View.OnClickListener} for navigating
      * to a destination. This supports both navigating via an
      * {@link NavDestination#getAction(int) action} and directly navigating to a destination.
@@ -110,27 +153,20 @@
         return new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                final NavController controller = findNavController(view);
-                if (controller != null) {
-                    controller.navigate(resId, args);
-                } else {
-                    throw new IllegalStateException(
-                            "OnClickListener#onClick could not find NavController for view "
-                            + view);
-                }
+                getNavController(view).navigate(resId, args);
             }
         };
     }
 
     /**
      * Associates a NavController with the given View, allowing developers to use
-     * {@link #findNavController(View)} and {@link #findNavController(Activity, int)} with that
+     * {@link #getNavController(View)} and {@link #getNavController(Activity, int)} with that
      * View or any of its children to retrieve the NavController.
      * <p>
      * This is generally called for you by the hosting {@link NavHost}.
      * @param view View that should be associated with the given NavController
      * @param controller The controller you wish to later retrieve via
-     *                   {@link #findNavController(View)}
+     *                   {@link #getNavController(View)}
      */
     public static void setViewNavController(@NonNull View view,
             @Nullable NavController controller) {
diff --git a/navigation/testing/ktx/build.gradle b/navigation/testing/ktx/build.gradle
new file mode 100644
index 0000000..9a96e77
--- /dev/null
+++ b/navigation/testing/ktx/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":navigation:testing"))
+    // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+    api(project(":navigation:common-ktx"))
+    api(KOTLIN_STDLIB)
+
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+    name = "Android Navigation Testing Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.NAVIGATION
+    mavenGroup = LibraryGroups.NAVIGATION
+    inceptionYear = "2018"
+    description = "Android Navigation-Testing-Ktx"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/testing/ktx/src/androidTest/AndroidManifest.xml b/navigation/testing/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..d963bb6
--- /dev/null
+++ b/navigation/testing/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.navigation.testing.ktx.test">
+
+</manifest>
diff --git a/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt b/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..20bba5a
--- /dev/null
+++ b/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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 androidx.navigation.testing
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.contains
+import androidx.navigation.get
+import androidx.navigation.navigation
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TestNavigatorDestinationBuilderTest {
+    private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+    @Test
+    fun test() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            test(DESTINATION_ID)
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+    }
+
+    @Test
+    fun testWithBody() {
+        val graph = provider.navigation(startDestination = DESTINATION_ID) {
+            test(DESTINATION_ID) {
+                label = LABEL
+            }
+        }
+        assertTrue("Destination should be added to the graph",
+                DESTINATION_ID in graph)
+        assertEquals("Destination should have label set",
+                LABEL, graph[DESTINATION_ID].label)
+    }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
diff --git a/navigation/testing/ktx/src/main/AndroidManifest.xml b/navigation/testing/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..35a5d64
--- /dev/null
+++ b/navigation/testing/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+<manifest package="androidx.navigation.testing.ktx"/>
diff --git a/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt
new file mode 100644
index 0000000..fc46653
--- /dev/null
+++ b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 androidx.navigation.testing
+
+import android.os.Bundle
+
+/**
+ * Get the [TestNavigator] back stack as a [List] of [destination and argument pairs][Pair].
+ */
+val TestNavigator.backStack: List<Pair<TestNavigator.Destination, Bundle?>>
+    get() = mBackStack.map { Pair(it.first!!, it.second) }
diff --git a/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..ce8ae41
--- /dev/null
+++ b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation.testing
+
+import android.support.annotation.IdRes
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+
+/**
+ * Construct a new [TestNavigator.Destination]
+ */
+inline fun NavGraphBuilder.test(@IdRes id: Int) = test(id) {}
+
+/**
+ * Construct a new [TestNavigator.Destination]
+ */
+inline fun NavGraphBuilder.test(
+        @IdRes id: Int,
+        block: TestNavigatorDestinationBuilder.() -> Unit
+) = destination(TestNavigatorDestinationBuilder(provider[TestNavigator::class], id).apply(block))
+
+/**
+ * DSL for constructing a new [TestNavigator.Destination]
+ */
+@NavDestinationDsl
+class TestNavigatorDestinationBuilder(
+        navigator: TestNavigator,
+        @IdRes id: Int
+) : NavDestinationBuilder<TestNavigator.Destination>(navigator, id)
diff --git a/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt b/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt
new file mode 100644
index 0000000..80cdb4f
--- /dev/null
+++ b/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 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 androidx.navigation.testing
+
+import android.os.Bundle
+import android.support.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class TestNavigatorTest {
+
+    @Test
+    fun backStack() {
+        val testNavigator = TestNavigator()
+        val destination = testNavigator.createDestination()
+        val args = Bundle()
+        testNavigator.navigate(destination, args, null)
+        assertEquals("TestNavigator back stack size is 1 after navigate",
+                1,
+                testNavigator.backStack.size)
+        val (foundDestination, foundArgs) = testNavigator.backStack.last()
+        assertEquals("last() returns last destination navigated to",
+                destination, foundDestination)
+        assertEquals("last() returns arguments Bundle",
+                args, foundArgs)
+    }
+}
diff --git a/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java
index 4ba5fd9..68a4994 100644
--- a/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java
+++ b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java
@@ -20,12 +20,12 @@
 import android.support.annotation.NonNull;
 import android.support.v4.util.Pair;
 
-import java.util.ArrayDeque;
-
 import androidx.navigation.NavDestination;
 import androidx.navigation.NavOptions;
 import androidx.navigation.Navigator;
 
+import java.util.ArrayDeque;
+
 /**
  * A simple Navigator that doesn't actually navigate anywhere, but does dispatch correctly
  */
@@ -66,7 +66,10 @@
         return popped;
     }
 
-    static class Destination extends NavDestination {
+    /**
+     * A simple Test destination
+     */
+    public static class Destination extends NavDestination {
         /**
          * NavDestinations should be created via {@link Navigator#createDestination}.
          */
diff --git a/navigation/ui/ktx/build.gradle b/navigation/ui/ktx/build.gradle
new file mode 100644
index 0000000..93d4966
--- /dev/null
+++ b/navigation/ui/ktx/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        debug {
+            testCoverageEnabled = false // Breaks Kotlin compiler.
+        }
+    }
+}
+
+dependencies {
+    api(project(":navigation:ui"))
+    // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+    api(project(":navigation:runtime-ktx"))
+    api(KOTLIN_STDLIB)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+    name = "Android Navigation UI Kotlin Extensions"
+    publish = true
+    mavenVersion = LibraryVersions.NAVIGATION
+    mavenGroup = LibraryGroups.NAVIGATION
+    inceptionYear = "2018"
+    description = "Android Navigation-UI-Ktx"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/ui/ktx/src/main/AndroidManifest.xml b/navigation/ui/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..120cc3f
--- /dev/null
+++ b/navigation/ui/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 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.
+  -->
+<manifest package="androidx.navigation.ui.ktx"/>
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt
new file mode 100644
index 0000000..6e530c6
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt
@@ -0,0 +1,44 @@
+/*
+ * 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 androidx.navigation.ui
+
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.AppCompatActivity
+import androidx.navigation.NavController
+
+/**
+ * Sets up the ActionBar returned by [AppCompatActivity.getSupportActionBar] for use
+ * with a [NavController].
+ *
+ * By calling this method, the title in the action bar will automatically be updated when
+ * the destination changes (assuming there is a valid
+ * [label][androidx.navigation.NavDestination.getLabel]).
+ *
+ * The action bar will also display the Up button when you are on a non-root destination and
+ * the drawer icon when on the root destination, automatically animating between them.
+ * Call [DrawerLayout.navigateUp] to handle the Up button.
+ *
+ * @param navController The NavController whose navigation actions will be reflected
+ *                      in the title of the action bar.
+ * @param drawerLayout The DrawerLayout that should be toggled from the home button
+ */
+fun AppCompatActivity.setupActionBarWithNavController(
+        navController: NavController,
+        drawerLayout: DrawerLayout? = null
+) {
+    NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
+}
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt
new file mode 100644
index 0000000..a23285b
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.navigation.ui
+
+import android.support.design.widget.BottomNavigationView
+import androidx.navigation.NavController
+
+/**
+ * Sets up a [BottomNavigationView] for use with a [NavController]. This will call
+ * [android.view.MenuItem.onNavDestinationSelected] when a menu item is selected.
+ *
+ * The selected item in the NavigationView will automatically be updated when the destination
+ * changes.
+ */
+fun BottomNavigationView.setupWithNavController(navController: NavController) {
+    NavigationUI.setupWithNavController(this, navController)
+}
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt
new file mode 100644
index 0000000..c87acdc
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 androidx.navigation.ui
+
+import android.support.v4.widget.DrawerLayout
+import androidx.navigation.NavController
+
+/**
+ * Handles the Up button by delegating its behavior to the given [NavController].
+ *
+ * This is equivalent to calling [NavController.navigateUp] if the [DrawerLayout] is null.
+ *
+ * @return True if the [NavController] was able to navigate up.
+ */
+fun DrawerLayout?.navigateUp(navController: NavController): Boolean =
+        NavigationUI.navigateUp(this, navController)
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt
new file mode 100644
index 0000000..a989ee3
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 androidx.navigation.ui
+
+import android.view.MenuItem
+import androidx.navigation.NavController
+
+/**
+ * Attempt to navigate to the [NavDestination] associated with this [MenuItem].
+ *
+ * Importantly, it assumes the [menu item id][getItemId] matches a valid
+ * [action id][androidx.navigation.NavDestination.getAction] or
+ * [destination id][androidx.navigation.NavDestination.getId] to be navigated to.
+ *
+ * @return True if the [NavController] was able to navigate to the destination.
+ */
+fun MenuItem.onNavDestinationSelected(navController: NavController): Boolean =
+        NavigationUI.onNavDestinationSelected(this, navController)
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt
new file mode 100644
index 0000000..71e1db5
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.navigation.ui
+
+import android.support.design.widget.NavigationView
+import androidx.navigation.NavController
+
+/**
+ * Sets up a [NavigationView] for use with a [NavController]. This will call
+ * [android.view.MenuItem.onNavDestinationSelected] when a menu item is selected.
+ *
+ * The selected item in the NavigationView will automatically be updated when the destination
+ * changes.
+ */
+fun NavigationView.setupWithNavController(navController: NavController) {
+    NavigationUI.setupWithNavController(this, navController)
+}
diff --git a/paging/common/build.gradle b/paging/common/build.gradle
index dbe6b06..cc3b185 100644
--- a/paging/common/build.gradle
+++ b/paging/common/build.gradle
@@ -41,4 +41,4 @@
     inceptionYear = "2017"
     description = "Android Paging-Common"
     url = SupportLibraryExtension.ARCHITECTURE_URL
-}
\ No newline at end of file
+}
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 1fe6725..e5cd46c 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -459,13 +459,34 @@
     @SuppressWarnings("deprecation")
     static class ContiguousWithoutPlaceholdersWrapper<Value>
             extends ContiguousDataSource<Integer, Value> {
-
         @NonNull
-        final PositionalDataSource<Value> mPositionalDataSource;
+        final PositionalDataSource<Value> mSource;
 
         ContiguousWithoutPlaceholdersWrapper(
-                @NonNull PositionalDataSource<Value> positionalDataSource) {
-            mPositionalDataSource = positionalDataSource;
+                @NonNull PositionalDataSource<Value> source) {
+            mSource = source;
+        }
+
+        @Override
+        public void addInvalidatedCallback(
+                @NonNull InvalidatedCallback onInvalidatedCallback) {
+            mSource.addInvalidatedCallback(onInvalidatedCallback);
+        }
+
+        @Override
+        public void removeInvalidatedCallback(
+                @NonNull InvalidatedCallback onInvalidatedCallback) {
+            mSource.removeInvalidatedCallback(onInvalidatedCallback);
+        }
+
+        @Override
+        public void invalidate() {
+            mSource.invalidate();
+        }
+
+        @Override
+        public boolean isInvalid() {
+            return mSource.isInvalid();
         }
 
         @NonNull
@@ -493,7 +514,7 @@
             // Note enablePlaceholders will be false here, but we don't have a way to communicate
             // this to PositionalDataSource. This is fine, because only the list and its position
             // offset will be consumed by the LoadInitialCallback.
-            mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
+            mSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
                     pageSize, mainThreadExecutor, receiver);
         }
 
@@ -502,7 +523,7 @@
                 @NonNull Executor mainThreadExecutor,
                 @NonNull PageResult.Receiver<Value> receiver) {
             int startIndex = currentEndIndex + 1;
-            mPositionalDataSource.dispatchLoadRange(
+            mSource.dispatchLoadRange(
                     PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
         }
 
@@ -514,12 +535,12 @@
             int startIndex = currentBeginIndex - 1;
             if (startIndex < 0) {
                 // trigger empty list load
-                mPositionalDataSource.dispatchLoadRange(
+                mSource.dispatchLoadRange(
                         PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
             } else {
                 int loadSize = Math.min(pageSize, startIndex + 1);
                 startIndex = startIndex - loadSize + 1;
-                mPositionalDataSource.dispatchLoadRange(
+                mSource.dispatchLoadRange(
                         PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
             }
         }
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
index 93520ca..6d112a8 100644
--- a/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
@@ -25,13 +25,6 @@
 class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
     private final ItemKeyedDataSource<K, A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
 
     private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
 
@@ -39,11 +32,26 @@
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     private List<B> convertWithStashedKeys(List<A> source) {
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java
index e6fa274..003e154 100644
--- a/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java
@@ -25,23 +25,31 @@
 class WrapperPageKeyedDataSource<K, A, B> extends PageKeyedDataSource<K, B> {
     private final PageKeyedDataSource<K, A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
 
     WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     @Override
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java
index 0626f87..1478c87 100644
--- a/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java
@@ -25,23 +25,30 @@
     private final PositionalDataSource<A> mSource;
     private final Function<List<A>, List<B>> mListFunction;
 
-    private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
-        @Override
-        public void onInvalidated() {
-            invalidate();
-            removeCallback();
-        }
-    };
-
     WrapperPositionalDataSource(PositionalDataSource<A> source,
             Function<List<A>, List<B>> listFunction) {
         mSource = source;
         mListFunction = listFunction;
-        mSource.addInvalidatedCallback(mInvalidatedCallback);
     }
 
-    private void removeCallback() {
-        mSource.removeInvalidatedCallback(mInvalidatedCallback);
+    @Override
+    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.addInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+        mSource.removeInvalidatedCallback(onInvalidatedCallback);
+    }
+
+    @Override
+    public void invalidate() {
+        mSource.invalidate();
+    }
+
+    @Override
+    public boolean isInvalid() {
+        return mSource.isInvalid();
     }
 
     @Override
diff --git a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
index 060094a..6550c50 100644
--- a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
@@ -337,17 +337,20 @@
 
     private abstract class WrapperDataSource<K, A, B>(private val source: ItemKeyedDataSource<K, A>)
             : ItemKeyedDataSource<K, B>() {
-        private val invalidatedCallback = DataSource.InvalidatedCallback {
-            invalidate()
-            removeCallback()
+        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.addInvalidatedCallback(onInvalidatedCallback)
         }
 
-        init {
-            source.addInvalidatedCallback(invalidatedCallback)
+        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.removeInvalidatedCallback(onInvalidatedCallback)
         }
 
-        private fun removeCallback() {
-            removeInvalidatedCallback(invalidatedCallback)
+        override fun invalidate() {
+            source.invalidate()
+        }
+
+        override fun isInvalid(): Boolean {
+            return source.isInvalid
         }
 
         override fun loadInitial(params: LoadInitialParams<K>, callback: LoadInitialCallback<B>) {
@@ -399,7 +402,7 @@
         // verify that it's possible to wrap an ItemKeyedDataSource, and add info to its data
 
         val orig = ItemDataSource(items = ITEMS_BY_NAME_ID)
-        val wrapper = DecoratedWrapperDataSource(orig)
+        val wrapper = createWrapper(orig)
 
         // load initial
         @Suppress("UNCHECKED_CAST")
@@ -425,6 +428,10 @@
         wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
         verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(10, 20).map { DecoratedItem(it) })
         verifyNoMoreInteractions(loadCallback)
+
+        // verify invalidation
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
     }
 
     @Test
@@ -442,6 +449,24 @@
         it.map { DecoratedItem(it) }
     }
 
+    @Test
+    fun testInvalidateToWrapper() {
+        val orig = ItemDataSource()
+        val wrapper = orig.map { DecoratedItem(it) }
+
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
+    }
+
+    @Test
+    fun testInvalidateFromWrapper() {
+        val orig = ItemDataSource()
+        val wrapper = orig.map { DecoratedItem(it) }
+
+        wrapper.invalidate()
+        assertTrue(orig.isInvalid)
+    }
+
     companion object {
         private val ITEM_COMPARATOR = compareBy<Item>({ it.name }).thenByDescending({ it.id })
         private val KEY_COMPARATOR = compareBy<Key>({ it.name }).thenByDescending({ it.id })
diff --git a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
index f7ea4b6..7c8667c 100644
--- a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
@@ -17,6 +17,7 @@
 package android.arch.paging
 
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -160,17 +161,20 @@
 
     private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
             : PageKeyedDataSource<K, B>() {
-        private val invalidatedCallback = DataSource.InvalidatedCallback {
-            invalidate()
-            removeCallback()
+        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.addInvalidatedCallback(onInvalidatedCallback)
         }
 
-        init {
-            source.addInvalidatedCallback(invalidatedCallback)
+        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.removeInvalidatedCallback(onInvalidatedCallback)
         }
 
-        private fun removeCallback() {
-            removeInvalidatedCallback(invalidatedCallback)
+        override fun invalidate() {
+            source.invalidate()
+        }
+
+        override fun isInvalid(): Boolean {
+            return source.isInvalid
         }
 
         override fun loadInitial(params: LoadInitialParams<K>,
@@ -247,6 +251,10 @@
         verify(loadCallback).onResult(expectedInitial.data.map { it.toString() },
                 expectedInitial.prev)
         verifyNoMoreInteractions(loadCallback)
+
+        // verify invalidation
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
     }
 
     @Test
@@ -258,11 +266,30 @@
     fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
         it.mapByPage { it.map { it.toString() } }
     }
+
     @Test
     fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
         it.map { it.toString() }
     }
 
+    @Test
+    fun testInvalidateToWrapper() {
+        val orig = ItemDataSource()
+        val wrapper = orig.map { it.toString() }
+
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
+    }
+
+    @Test
+    fun testInvalidateFromWrapper() {
+        val orig = ItemDataSource()
+        val wrapper = orig.map { it.toString() }
+
+        wrapper.invalidate()
+        assertTrue(orig.isInvalid)
+    }
+
     companion object {
         // first load is 2nd page to ensure we test prepend as well as append behavior
         private val INIT_KEY: String = "key 2"
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
index eab778c..2b3c770 100644
--- a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
@@ -17,6 +17,7 @@
 package android.arch.paging
 
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -219,17 +220,20 @@
 
     private abstract class WrapperDataSource<in A, B>(private val source: PositionalDataSource<A>)
             : PositionalDataSource<B>() {
-        private val invalidatedCallback = DataSource.InvalidatedCallback {
-            invalidate()
-            removeCallback()
+        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.addInvalidatedCallback(onInvalidatedCallback)
         }
 
-        init {
-            source.addInvalidatedCallback(invalidatedCallback)
+        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            source.removeInvalidatedCallback(onInvalidatedCallback)
         }
 
-        private fun removeCallback() {
-            removeInvalidatedCallback(invalidatedCallback)
+        override fun invalidate() {
+            source.invalidate()
+        }
+
+        override fun isInvalid(): Boolean {
+            return source.isInvalid
         }
 
         override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<B>) {
@@ -292,6 +296,10 @@
         orig.invalidate()
         verify(invalCallback).onInvalidated()
         verifyNoMoreInteractions(invalCallback)
+
+        // verify invalidation
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
     }
 
     @Test
@@ -308,4 +316,40 @@
     fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
         it.map { it.toString() }
     }
+
+    @Test
+    fun testInvalidateToWrapper() {
+        val orig = ListDataSource(listOf(0, 1, 2))
+        val wrapper = orig.map { it.toString() }
+
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
+    }
+
+    @Test
+    fun testInvalidateFromWrapper() {
+        val orig = ListDataSource(listOf(0, 1, 2))
+        val wrapper = orig.map { it.toString() }
+
+        wrapper.invalidate()
+        assertTrue(orig.isInvalid)
+    }
+
+    @Test
+    fun testInvalidateToWrapper_contiguous() {
+        val orig = ListDataSource(listOf(0, 1, 2))
+        val wrapper = orig.wrapAsContiguousWithoutPlaceholders()
+
+        orig.invalidate()
+        assertTrue(wrapper.isInvalid)
+    }
+
+    @Test
+    fun testInvalidateFromWrapper_contiguous() {
+        val orig = ListDataSource(listOf(0, 1, 2))
+        val wrapper = orig.wrapAsContiguousWithoutPlaceholders()
+
+        wrapper.invalidate()
+        assertTrue(orig.isInvalid)
+    }
 }
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
index b82e1f9..7fd6621 100644
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
+++ b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
@@ -30,7 +30,7 @@
  * {@link PagedList.Config}.
  * <p>
  * The required parameters are in the constructor, so you can simply construct and build, or
- * optionally enable extra features (such as initial load key, or BoundaryCallback.
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
  *
  * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
  *             you're using PositionalDataSource.
diff --git a/paging/rxjava2/build.gradle b/paging/rxjava2/build.gradle
new file mode 100644
index 0000000..f96bc70
--- /dev/null
+++ b/paging/rxjava2/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryGroups
+import android.support.LibraryVersions
+import android.support.SupportLibraryExtension
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+    id("kotlin-android")
+}
+
+dependencies {
+    api(project(":arch:runtime"))
+    api(project(":paging:common"))
+
+    api(RX_JAVA)
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(ESPRESSO_CORE)
+    androidTestImplementation(KOTLIN_STDLIB)
+}
+
+supportLibrary {
+    name = "Android Paging RXJava2"
+    publish = true
+    mavenVersion = LibraryVersions.PAGING_RX
+    mavenGroup = LibraryGroups.PAGING
+    inceptionYear = "2018"
+    description = "Android Paging RXJava2"
+    url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/paging/rxjava2/src/androidTest/java/android/arch/paging/RxPagedListBuilderTest.kt b/paging/rxjava2/src/androidTest/java/android/arch/paging/RxPagedListBuilderTest.kt
new file mode 100644
index 0000000..33598fc
--- /dev/null
+++ b/paging/rxjava2/src/androidTest/java/android/arch/paging/RxPagedListBuilderTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 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.paging
+
+import android.support.test.filters.SmallTest
+import io.reactivex.Observable
+import io.reactivex.observers.TestObserver
+import io.reactivex.schedulers.TestScheduler
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RxPagedListBuilderTest {
+
+    /**
+     * Creates a data source that will sequentially supply the passed lists
+     */
+    private fun testDataSourceSequence(data: List<List<String>>): DataSource.Factory<Int, String> {
+        return object : DataSource.Factory<Int, String>() {
+            var localData = data
+            override fun create(): DataSource<Int, String> {
+                val currentList = localData.first()
+                localData = localData.drop(1)
+                return ListDataSource<String>(currentList)
+            }
+        }
+    }
+
+    @Test
+    fun basic() {
+        val factory = testDataSourceSequence(listOf(listOf("a", "b"), listOf("c", "d")))
+        val scheduler = TestScheduler()
+        val observable = RxPagedListBuilder(factory, 10)
+                .setFetchScheduler(scheduler)
+                .setNotifyScheduler(scheduler)
+                .buildObservable()
+        val observer = TestObserver<PagedList<String>>()
+
+        observable.subscribe(observer)
+
+        // initial state
+        observer.assertNotComplete()
+        observer.assertValueCount(0)
+
+        // load first item
+        scheduler.triggerActions()
+        observer.assertValueCount(1)
+        assertEquals(listOf("a", "b"), observer.values().first())
+
+        // invalidate triggers second load
+        observer.values().first().dataSource.invalidate()
+        scheduler.triggerActions()
+        observer.assertValueCount(2)
+        assertEquals(listOf("c", "d"), observer.values().last())
+    }
+
+    @Test
+    fun checkSchedulers() {
+        val factory = testDataSourceSequence(listOf(listOf("a", "b"), listOf("c", "d")))
+        val notifyScheduler = TestScheduler()
+        val fetchScheduler = TestScheduler()
+        val observable: Observable<PagedList<String>> = RxPagedListBuilder(
+                factory, 10)
+                .setFetchScheduler(fetchScheduler)
+                .setNotifyScheduler(notifyScheduler)
+                .buildObservable()
+        val observer = TestObserver<PagedList<String>>()
+        observable.subscribe(observer)
+
+        // notify has nothing to do
+        notifyScheduler.triggerActions()
+        observer.assertValueCount(0)
+
+        // fetch creates list, but observer doesn't see
+        fetchScheduler.triggerActions()
+        observer.assertValueCount(0)
+
+        // now notify reveals item
+        notifyScheduler.triggerActions()
+        observer.assertValueCount(1)
+    }
+}
diff --git a/paging/rxjava2/src/main/AndroidManifest.xml b/paging/rxjava2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6329afe
--- /dev/null
+++ b/paging/rxjava2/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.arch.paging.rxjava2">
+</manifest>
diff --git a/paging/rxjava2/src/main/java/android/arch/paging/RxPagedListBuilder.java b/paging/rxjava2/src/main/java/android/arch/paging/RxPagedListBuilder.java
new file mode 100644
index 0000000..aa7fd33
--- /dev/null
+++ b/paging/rxjava2/src/main/java/android/arch/paging/RxPagedListBuilder.java
@@ -0,0 +1,330 @@
+/*
+ * 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.paging;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.Scheduler;
+import io.reactivex.functions.Cancellable;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * Builder for {@code Observable<PagedList>} or {@code Flowable<PagedList>}, given a
+ * {@link DataSource.Factory} and a {@link PagedList.Config}.
+ * <p>
+ * The required parameters are in the constructor, so you can simply construct and build, or
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
+ * <p>
+ * The returned observable/flowable will already be subscribed on the
+ * {@link #setFetchScheduler(Scheduler)}, and will perform all loading on that scheduler. It will
+ * already be observed on {@link #setNotifyScheduler(Scheduler)}, and will dispatch new PagedLists,
+ * as well as their updates to that scheduler.
+ *
+ * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
+ *             you're using PositionalDataSource.
+ * @param <Value> Item type being presented.
+ */
+public final class RxPagedListBuilder<Key, Value> {
+    private Key mInitialLoadKey;
+    private PagedList.Config mConfig;
+    private DataSource.Factory<Key, Value> mDataSourceFactory;
+    private PagedList.BoundaryCallback mBoundaryCallback;
+    private Executor mNotifyExecutor;
+    private Executor mFetchExecutor;
+    private Scheduler mFetchScheduler;
+    private Scheduler mNotifyScheduler;
+
+    /**
+     * Creates a RxPagedListBuilder with required parameters.
+     *
+     * @param dataSourceFactory DataSource factory providing DataSource generations.
+     * @param config Paging configuration.
+     */
+    public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+            @NonNull PagedList.Config config) {
+        //noinspection ConstantConditions
+        if (config == null) {
+            throw new IllegalArgumentException("PagedList.Config must be provided");
+        }
+        //noinspection ConstantConditions
+        if (dataSourceFactory == null) {
+            throw new IllegalArgumentException("DataSource.Factory must be provided");
+        }
+        mDataSourceFactory = dataSourceFactory;
+        mConfig = config;
+    }
+
+    /**
+     * Creates a RxPagedListBuilder with required parameters.
+     * <p>
+     * This method is a convenience for:
+     * <pre>
+     * RxPagedListBuilder(dataSourceFactory,
+     *         new PagedList.Config.Builder().setPageSize(pageSize).build())
+     * </pre>
+     *
+     * @param dataSourceFactory DataSource.Factory providing DataSource generations.
+     * @param pageSize Size of pages to load.
+     */
+    @SuppressWarnings("unused")
+    public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+            int pageSize) {
+        this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
+    }
+
+    /**
+     * First loading key passed to the first PagedList/DataSource.
+     * <p>
+     * When a new PagedList/DataSource pair is created after the first, it acquires a load key from
+     * the previous generation so that data is loaded around the position already being observed.
+     *
+     * @param key Initial load key passed to the first PagedList/DataSource.
+     * @return this
+     */
+    @SuppressWarnings("unused")
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
+        mInitialLoadKey = key;
+        return this;
+    }
+
+    /**
+     * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
+     * additional data from network when paging from local storage.
+     * <p>
+     * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
+     * method is not called, or {@code null} is passed, you will not be notified when each
+     * DataSource runs out of data to provide to its PagedList.
+     * <p>
+     * If you are paging from a DataSource.Factory backed by local storage, you can set a
+     * BoundaryCallback to know when there is no more information to page from local storage.
+     * This is useful to page from the network when local storage is a cache of network data.
+     * <p>
+     * Note that when using a BoundaryCallback with a {@code Observable<PagedList>}, method calls
+     * on the callback may be dispatched multiple times - one for each PagedList/DataSource
+     * pair. If loading network data from a BoundaryCallback, you should prevent multiple
+     * dispatches of the same method from triggering multiple simultaneous network loads.
+     *
+     * @param boundaryCallback The boundary callback for listening to PagedList load state.
+     * @return this
+     */
+    @SuppressWarnings("unused")
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setBoundaryCallback(
+            @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
+        mBoundaryCallback = boundaryCallback;
+        return this;
+    }
+
+    /**
+     * Sets scheduler which will be used for observing new PagedLists, as well as loading updates
+     * within the PagedLists.
+     * <p>
+     * The built observable will be {@link Observable#observeOn(Scheduler) observed on} this
+     * scheduler, so that the thread receiving PagedLists will also receive the internal updates to
+     * the PagedList.
+     *
+     * @param scheduler Scheduler for background DataSource loading.
+     * @return this
+     */
+    public RxPagedListBuilder<Key, Value> setNotifyScheduler(
+            final @NonNull Scheduler scheduler) {
+        mNotifyScheduler = scheduler;
+        final Scheduler.Worker worker = scheduler.createWorker();
+        mNotifyExecutor = new Executor() {
+            @Override
+            public void execute(@NonNull Runnable command) {
+                // We use a worker here since the page load notifications
+                // should not be dispatched in parallel
+                worker.schedule(command);
+            }
+        };
+        return this;
+    }
+
+    /**
+     * Sets scheduler which will be used for background fetching of PagedLists, as well as on-demand
+     * fetching of pages inside.
+     *
+     * @param scheduler Scheduler for background DataSource loading.
+     * @return this
+     */
+    @SuppressWarnings({"unused", "WeakerAccess"})
+    @NonNull
+    public RxPagedListBuilder<Key, Value> setFetchScheduler(
+            final @NonNull Scheduler scheduler) {
+        mFetchExecutor = new Executor() {
+            @Override
+            public void execute(@NonNull Runnable command) {
+                // We use scheduleDirect since the page loads that use
+                // executor are intentionally parallel.
+                scheduler.scheduleDirect(command);
+            }
+        };
+        mFetchScheduler = scheduler;
+        return this;
+    }
+
+    /**
+     * Constructs a {@code Observable<PagedList>}.
+     * <p>
+     * The returned Observable will already be observed on the
+     * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+     * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+     *
+     * @return The Observable of PagedLists
+     */
+    @NonNull
+    public Observable<PagedList<Value>> buildObservable() {
+        if (mNotifyExecutor == null) {
+            mNotifyExecutor = ArchTaskExecutor.getMainThreadExecutor();
+            mNotifyScheduler = Schedulers.from(mNotifyExecutor);
+        }
+        if (mFetchExecutor == null) {
+            mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor();
+            mFetchScheduler = Schedulers.from(mFetchExecutor);
+        }
+        return Observable.create(new PagingObservableOnSubscribe<>(
+                mInitialLoadKey,
+                mConfig,
+                mBoundaryCallback,
+                mDataSourceFactory,
+                mNotifyExecutor,
+                mFetchExecutor))
+                        .observeOn(mNotifyScheduler)
+                        .subscribeOn(mFetchScheduler);
+    }
+
+    /**
+     * Constructs a {@code Flowable<PagedList>}.
+     *
+     * The returned Observable will already be observed on the
+     * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+     * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+     *
+     * @param backpressureStrategy BackpressureStrategy for the Flowable to use.
+     * @return The Flowable of PagedLists
+     */
+    @NonNull
+    public Flowable<PagedList<Value>> buildFlowable(BackpressureStrategy backpressureStrategy) {
+        return buildObservable()
+                .toFlowable(backpressureStrategy);
+    }
+
+    static class PagingObservableOnSubscribe<Key, Value>
+            implements ObservableOnSubscribe<PagedList<Value>>, DataSource.InvalidatedCallback,
+            Cancellable,
+            Runnable {
+
+        @Nullable
+        private final Key mInitialLoadKey;
+        @NonNull
+        private final PagedList.Config mConfig;
+        @Nullable
+        private final PagedList.BoundaryCallback mBoundaryCallback;
+        @NonNull
+        private final DataSource.Factory<Key, Value> mDataSourceFactory;
+        @NonNull
+        private final Executor mNotifyExecutor;
+        @NonNull
+        private final Executor mFetchExecutor;
+
+        @Nullable
+        private PagedList<Value> mList;
+        @Nullable
+        private DataSource<Key, Value> mDataSource;
+
+        private ObservableEmitter<PagedList<Value>> mEmitter;
+
+        private PagingObservableOnSubscribe(@Nullable Key initialLoadKey,
+                @NonNull PagedList.Config config,
+                @Nullable PagedList.BoundaryCallback boundaryCallback,
+                @NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+                @NonNull Executor notifyExecutor,
+                @NonNull Executor fetchExecutor) {
+            mInitialLoadKey = initialLoadKey;
+            mConfig = config;
+            mBoundaryCallback = boundaryCallback;
+            mDataSourceFactory = dataSourceFactory;
+            mNotifyExecutor = notifyExecutor;
+            mFetchExecutor = fetchExecutor;
+        }
+
+        @Override
+        public void subscribe(ObservableEmitter<PagedList<Value>> emitter)
+                throws Exception {
+            mEmitter = emitter;
+            mEmitter.setCancellable(this);
+
+            // known that subscribe is already on fetchScheduler
+            mEmitter.onNext(createPagedList());
+        }
+
+        @Override
+        public void cancel() throws Exception {
+            if (mDataSource != null) {
+                mDataSource.removeInvalidatedCallback(this);
+            }
+        }
+
+        @Override
+        public void run() {
+            // fetch data, run on fetchExecutor
+            mEmitter.onNext(createPagedList());
+        }
+
+        @Override
+        public void onInvalidated() {
+            if (!mEmitter.isDisposed()) {
+                mFetchExecutor.execute(this);
+            }
+        }
+
+        private PagedList<Value> createPagedList() {
+            @Nullable Key initializeKey = mInitialLoadKey;
+            if (mList != null) {
+                //noinspection unchecked
+                initializeKey = (Key) mList.getLastKey();
+            }
+
+            do {
+                if (mDataSource != null) {
+                    mDataSource.removeInvalidatedCallback(this);
+                }
+                mDataSource = mDataSourceFactory.create();
+                mDataSource.addInvalidatedCallback(this);
+
+                mList = new PagedList.Builder<>(mDataSource, mConfig)
+                        .setNotifyExecutor(mNotifyExecutor)
+                        .setFetchExecutor(mFetchExecutor)
+                        .setBoundaryCallback(mBoundaryCallback)
+                        .setInitialKey(initializeKey)
+                        .build();
+            } while (mList.isDetached());
+            return mList;
+        }
+    }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt
index 0d99743..e64b50d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt
@@ -59,6 +59,7 @@
     private fun createClearAllTables(): MethodSpec {
         val scope = CodeGenScope(this)
         return MethodSpec.methodBuilder("clearAllTables").apply {
+            addStatement("super.assertNotMainThread()")
             val dbVar = scope.getTmpVar("_db")
             addStatement("final $T $L = super.getOpenHelper().getWritableDatabase()",
                     SupportDbTypeNames.DB, dbVar)
@@ -98,7 +99,10 @@
                     endControlFlow()
                 }
                 addStatement("$L.query($S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
-                addStatement("$L.execSQL($S)", dbVar, "VACUUM")
+                beginControlFlow("if (!$L.inTransaction())", dbVar).apply {
+                    addStatement("$L.execSQL($S)", dbVar, "VACUUM")
+                }
+                endControlFlow()
             }
             endControlFlow()
         }.build()
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index e03a0e1..9d9dbf3 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -93,6 +93,7 @@
 
     @Override
     public void clearAllTables() {
+        super.assertNotMainThread();
         final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
         try {
             super.beginTransaction();
@@ -101,7 +102,9 @@
         } finally {
             super.endTransaction();
             _db.query("PRAGMA wal_checkpoint(FULL)").close();
-            _db.execSQL("VACUUM");
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM");
+            }
         }
     }
 
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 45ec9c6..47564dd 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -66,6 +66,7 @@
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
 
     testImplementation(JUNIT)
+    compile project(path: ':paging:rxjava2')
 }
 
 tasks['check'].dependsOn(tasks['connectedCheck'])
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java
index 0eff707..5b5b9e7 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java
@@ -16,11 +16,13 @@
 
 package android.arch.persistence.room.integration.testapp.test;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
 
 import android.arch.persistence.room.Dao;
 import android.arch.persistence.room.Database;
@@ -132,6 +134,29 @@
 
     @Test
     @SmallTest
+    public void inTransaction() {
+        mDao.insertParent(new Parent(1, "A"));
+        assertThat(mDao.countParent(), is(1));
+        // Running clearAllTables in a transaction is not recommended, but we should not crash.
+        mDatabase.runInTransaction(() -> mDatabase.clearAllTables());
+        assertThat(mDao.countParent(), is(0));
+    }
+
+    @Test
+    @SmallTest
+    public void inMainThread() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            try {
+                mDatabase.clearAllTables();
+                fail("Was expecting an exception");
+            } catch (IllegalStateException e) {
+                assertThat(e.getMessage(), containsString("main thread"));
+            }
+        });
+    }
+
+    @Test
+    @SmallTest
     public void foreignKey() {
         mDao.insertParent(new Parent(1, "A"));
         mDao.insertChild(new Child(1, "a", 1));
@@ -186,12 +211,12 @@
         db.dao().insertParent(new Parent(1, uuid));
         assertThat(queryEncoding(db), is(equalTo("UTF-8")));
         db.close();
-        assertThat(containsString(file, uuid), is(true));
+        assertThat(fileContainsString(file, uuid), is(true));
         db = Room.databaseBuilder(context, ClearAllTablesDatabase.class, dbName)
                 .setJournalMode(journalMode).build();
         db.clearAllTables();
         db.close();
-        assertThat(containsString(file, uuid), is(false));
+        assertThat(fileContainsString(file, uuid), is(false));
     }
 
     private String queryEncoding(RoomDatabase db) {
@@ -207,7 +232,7 @@
         }
     }
 
-    private boolean containsString(File file, String s) throws IOException {
+    private boolean fileContainsString(File file, String s) throws IOException {
         final byte[] content = new byte[(int) file.length()];
         final FileInputStream stream = new FileInputStream(file);
         //noinspection TryFinallyCanBeTryWithResources
diff --git a/room/integration-tests/testapp/src/main/AndroidManifest.xml b/room/integration-tests/testapp/src/main/AndroidManifest.xml
index 02c1975..0bb827f 100644
--- a/room/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/room/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -37,5 +37,14 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".RoomPagedListRxActivity"
+            android:label="PagedList Observable"
+            android:theme="@style/Theme.AppCompat">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
     </application>
-  </manifest>
+</manifest>
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
index d8cd8d4..762e0a2 100644
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
@@ -23,6 +23,7 @@
 import android.arch.paging.DataSource;
 import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
+import android.arch.paging.RxPagedListBuilder;
 import android.arch.persistence.room.Room;
 import android.arch.persistence.room.integration.testapp.database.Customer;
 import android.arch.persistence.room.integration.testapp.database.LastNameAscCustomerDataSource;
@@ -31,6 +32,9 @@
 
 import java.util.UUID;
 
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+
 /**
  * Sample database-backed view model of Customers
  */
@@ -47,20 +51,17 @@
         mDatabase = Room.databaseBuilder(this.getApplication(),
                 SampleDatabase.class, "customerDatabase").build();
 
-        ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
-            @Override
-            public void run() {
-                // fill with some simple data
-                int customerCount = mDatabase.getCustomerDao().countCustomers();
-                if (customerCount == 0) {
-                    Customer[] initialCustomers = new Customer[10];
-                    for (int i = 0; i < 10; i++) {
-                        initialCustomers[i] = createCustomer();
-                    }
-                    mDatabase.getCustomerDao().insertAll(initialCustomers);
+        ArchTaskExecutor.getInstance().executeOnDiskIO(() -> {
+            // fill with some simple data
+            int customerCount = mDatabase.getCustomerDao().countCustomers();
+            if (customerCount == 0) {
+                Customer[] initialCustomers = new Customer[10];
+                for (int i = 0; i < 10; i++) {
+                    initialCustomers[i] = createCustomer();
                 }
-
+                mDatabase.getCustomerDao().insertAll(initialCustomers);
             }
+
         });
     }
 
@@ -73,12 +74,13 @@
     }
 
     void insertCustomer() {
-        ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
-            @Override
-            public void run() {
-                mDatabase.getCustomerDao().insert(createCustomer());
-            }
-        });
+        ArchTaskExecutor.getInstance().executeOnDiskIO(
+                () -> mDatabase.getCustomerDao().insert(createCustomer()));
+    }
+
+    void clearAllCustomers() {
+        ArchTaskExecutor.getInstance().executeOnDiskIO(
+                () -> mDatabase.getCustomerDao().removeAll());
     }
 
     private static <K> LiveData<PagedList<Customer>> getLivePagedList(
@@ -92,6 +94,20 @@
                 .build();
     }
 
+    private static <K> Flowable<PagedList<Customer>> getPagedListFlowable(
+            DataSource.Factory<K, Customer> dataSourceFactory) {
+        PagedList.Config config = new PagedList.Config.Builder()
+                .setPageSize(10)
+                .setEnablePlaceholders(false)
+                .build();
+        return new RxPagedListBuilder<>(dataSourceFactory, config)
+                .buildFlowable(BackpressureStrategy.LATEST);
+    }
+
+    Flowable<PagedList<Customer>> getPagedListFlowable() {
+        return getPagedListFlowable(mDatabase.getCustomerDao().loadPagedAgeOrder());
+    }
+
     LiveData<PagedList<Customer>> getLivePagedList(int position) {
         if (mLiveCustomerList == null) {
             mLiveCustomerList =
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListRxActivity.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListRxActivity.java
new file mode 100644
index 0000000..df2adf2
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListRxActivity.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import android.arch.lifecycle.ViewModelProviders;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.RecyclerView;
+import android.widget.Button;
+
+import io.reactivex.disposables.CompositeDisposable;
+
+/**
+ * Sample {@code Flowable<PagedList>} activity which uses Room.
+ */
+public class RoomPagedListRxActivity extends AppCompatActivity {
+
+    private PagedListCustomerAdapter mAdapter;
+
+    private final CompositeDisposable mDisposable = new CompositeDisposable();
+    private CustomerViewModel mViewModel;
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_recycler_view);
+        mViewModel = ViewModelProviders.of(this)
+                .get(CustomerViewModel.class);
+
+        RecyclerView recyclerView = findViewById(R.id.recyclerview);
+        mAdapter = new PagedListCustomerAdapter();
+        recyclerView.setAdapter(mAdapter);
+
+        final Button addButton = findViewById(R.id.addButton);
+        addButton.setOnClickListener(v -> mViewModel.insertCustomer());
+
+        final Button clearButton = findViewById(R.id.clearButton);
+        clearButton.setOnClickListener(v -> mViewModel.clearAllCustomers());
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        mDisposable.add(mViewModel.getPagedListFlowable()
+                .subscribe(list -> mAdapter.submitList(list)));
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mDisposable.clear();
+    }
+}
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 db45dc4..98d2bab 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
@@ -44,6 +44,12 @@
     void insertAll(Customer[] customers);
 
     /**
+     * Delete all customers
+     */
+    @Query("DELETE FROM customer")
+    void removeAll();
+
+    /**
      * @return DataSource.Factory of customers, ordered by last name. Use
      * {@link android.arch.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
      */
diff --git a/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml b/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
index 34e4491..49208f6 100644
--- a/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
+++ b/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
@@ -35,6 +35,14 @@
         android:paddingTop="@dimen/activity_vertical_margin"
     />
     <Button
+        android:id="@+id/clearButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
+        android:text="@string/clear"/>
+    <Button
         android:id="@+id/addButton"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/room/integration-tests/testapp/src/main/res/values/strings.xml b/room/integration-tests/testapp/src/main/res/values/strings.xml
index a18723e..2bba589 100644
--- a/room/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/room/integration-tests/testapp/src/main/res/values/strings.xml
@@ -15,6 +15,7 @@
 -->
 
 <resources>
+    <string name="clear">Clear</string>
     <string name="insert">Insert</string>
     <string name="loading">loading</string>
 </resources>