diff --git a/build.gradle.kts b/build.gradle.kts
index b9a22bae0..8de919c3e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -368,5 +368,10 @@ extensions.configure<ApiValidationExtension> {
   /**
    * Sub-projects that are excluded from API validation
    */
-  ignoredProjects = mutableSetOf("dispatch-internal-test", "dispatch-sample", "samples")
+  ignoredProjects = mutableSetOf(
+    "dispatch-internal-test",
+    "dispatch-internal-test-android",
+    "dispatch-sample",
+    "samples"
+  )
 }
diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt
index 3f95974fc..6609d369a 100644
--- a/buildSrc/src/main/kotlin/Modules.kt
+++ b/buildSrc/src/main/kotlin/Modules.kt
@@ -31,14 +31,15 @@ object Modules {
     ":dispatch-core:samples",
     ":dispatch-detekt",
     ":dispatch-internal-test",
+    ":dispatch-internal-test-android",
     ":dispatch-sample",
-    "dispatch-test",
-    "dispatch-test-junit4",
-    "dispatch-test-junit4:samples",
-    "dispatch-test-junit5",
-    "dispatch-test-junit5:samples",
-    "dispatch-test:samples",
-    "dispatch-test:samples"
+    ":dispatch-test",
+    ":dispatch-test-junit4",
+    ":dispatch-test-junit4:samples",
+    ":dispatch-test-junit5",
+    ":dispatch-test-junit5:samples",
+    ":dispatch-test:samples",
+    ":dispatch-test:samples"
   )
   val allInternalPaths = allPaths.filter { it.matches(internalRegex) }
   val allProductionPaths = allPaths.filter { !it.matches(internalRegex, sampleRegex) }
diff --git a/dispatch-android-lifecycle-extensions/samples/build.gradle.kts b/dispatch-android-lifecycle-extensions/samples/build.gradle.kts
index 06ca1fb38..35a2cbdaf 100644
--- a/dispatch-android-lifecycle-extensions/samples/build.gradle.kts
+++ b/dispatch-android-lifecycle-extensions/samples/build.gradle.kts
@@ -52,6 +52,7 @@ dependencies {
   implementation(project(":dispatch-android-lifecycle"))
   implementation(project(":dispatch-android-lifecycle-extensions"))
   implementation(project(":dispatch-core"))
+  implementation(project(":dispatch-internal-test-android"))
   implementation(project(":dispatch-test"))
   implementation(project(":dispatch-test-junit5"))
   testImplementation(Libs.JUnit.jUnit5)
diff --git a/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/Fragment.kt b/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/Fragment.kt
index d971bd474..3f73d824a 100644
--- a/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/Fragment.kt
+++ b/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/Fragment.kt
@@ -17,6 +17,7 @@
 package samples
 
 import androidx.lifecycle.*
+import dispatch.internal.test.android.*
 
 abstract class Fragment(
   initialState: Lifecycle.State = Lifecycle.State.INITIALIZED
diff --git a/dispatch-android-lifecycle/samples/build.gradle.kts b/dispatch-android-lifecycle/samples/build.gradle.kts
index 0f8cf4244..837179a8d 100644
--- a/dispatch-android-lifecycle/samples/build.gradle.kts
+++ b/dispatch-android-lifecycle/samples/build.gradle.kts
@@ -52,6 +52,7 @@ dependencies {
   implementation(project(":dispatch-android-lifecycle"))
   implementation(project(":dispatch-android-lifecycle-extensions"))
   implementation(project(":dispatch-core"))
+  implementation(project(":dispatch-internal-test-android"))
   implementation(project(":dispatch-test"))
   implementation(project(":dispatch-test-junit5"))
   testImplementation(Libs.JUnit.jUnit5)
diff --git a/dispatch-android-lifecycle/samples/src/test/java/samples/FakeLifecycleOwner.kt b/dispatch-android-lifecycle/samples/src/test/java/samples/FakeLifecycleOwner.kt
deleted file mode 100644
index f9cceec29..000000000
--- a/dispatch-android-lifecycle/samples/src/test/java/samples/FakeLifecycleOwner.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2020 Rick Busarow
- * 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 samples
-
-import androidx.lifecycle.*
-import kotlinx.coroutines.*
-
-@Suppress("EXPERIMENTAL_API_USAGE")
-open class FakeLifecycleOwner(
-  private val mainDispatcher: CoroutineDispatcher = fakeMainDispatcher(),
-  initialState: Lifecycle.State = Lifecycle.State.INITIALIZED
-) : LifecycleOwner {
-
-  private val registry: LifecycleRegistry by lazy { LifecycleRegistry(this) }
-
-  init {
-    when (initialState) {
-      Lifecycle.State.DESTROYED -> destroy()
-      Lifecycle.State.CREATED   -> create()
-      Lifecycle.State.STARTED   -> start()
-      Lifecycle.State.RESUMED   -> resume()
-      else                      -> Unit
-    }
-  }
-
-  override fun getLifecycle(): LifecycleRegistry = registry
-
-  fun create() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
-  }
-
-  fun start() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
-  }
-
-  fun resume() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
-  }
-
-  fun pause() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
-  }
-
-  fun stop() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
-  }
-
-  fun initialize() = runBlocking(mainDispatcher) {
-    lifecycle.currentState = Lifecycle.State.INITIALIZED
-  }
-
-  fun destroy() = runBlocking(mainDispatcher) {
-    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
-  }
-
-  fun getObserverCount(): Int = runBlocking(mainDispatcher) { registry.observerCount }
-}
-
-@Suppress("EXPERIMENTAL_API_USAGE")
-private fun fakeMainDispatcher() = newSingleThreadContext("FakeLifecycleOwner main")
diff --git a/dispatch-android-lifecycle/samples/src/test/java/samples/Fragment.kt b/dispatch-android-lifecycle/samples/src/test/java/samples/Fragment.kt
index d971bd474..3f73d824a 100644
--- a/dispatch-android-lifecycle/samples/src/test/java/samples/Fragment.kt
+++ b/dispatch-android-lifecycle/samples/src/test/java/samples/Fragment.kt
@@ -17,6 +17,7 @@
 package samples
 
 import androidx.lifecycle.*
+import dispatch.internal.test.android.*
 
 abstract class Fragment(
   initialState: Lifecycle.State = Lifecycle.State.INITIALIZED
diff --git a/dispatch-internal-test-android/build.gradle.kts b/dispatch-internal-test-android/build.gradle.kts
new file mode 100644
index 000000000..a1efd7642
--- /dev/null
+++ b/dispatch-internal-test-android/build.gradle.kts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 Rick Busarow
+ * 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.
+ */
+
+plugins {
+  id(Plugins.androidLibrary)
+  id(Plugins.kotlinAndroid)
+  id(Plugins.kotlinAndroidExtensions)
+}
+
+android {
+  compileSdkVersion(Versions.compileSdk)
+
+  defaultConfig {
+    minSdkVersion(Versions.minSdk)
+    targetSdkVersion(Versions.targetSdk)
+    versionName = Versions.versionName
+
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+  }
+
+  buildTypes {
+    getByName("release") {
+      isMinifyEnabled = false
+      proguardFiles(
+        getDefaultProguardFile("proguard-android-optimize.txt"),
+        "proguard-rules.pro"
+      )
+    }
+  }
+}
+dependencies {
+  implementation(Libs.AndroidX.Fragment.core)
+  implementation(Libs.AndroidX.Lifecycle.common)
+  implementation(Libs.AndroidX.Lifecycle.runtime)
+  implementation(Libs.Kotlin.reflect)
+  implementation(Libs.Kotlin.stdlib)
+  implementation(Libs.Kotlinx.Coroutines.android)
+  implementation(Libs.Kotlinx.Coroutines.core)
+}
diff --git a/dispatch-internal-test-android/src/main/AndroidManifest.xml b/dispatch-internal-test-android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..ef707610b
--- /dev/null
+++ b/dispatch-internal-test-android/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+  ~ Copyright (C) 2020 Rick Busarow
+  ~ 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="dispatch.internal.test.android" />
diff --git a/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeFragment.java b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeFragment.java
new file mode 100644
index 000000000..28847084c
--- /dev/null
+++ b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeFragment.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 Rick Busarow
+ * 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 dispatch.internal.test.android;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import org.jetbrains.annotations.Nullable;
+
+public class FakeFragment extends Fragment {
+
+    LifecycleOwner fragmentLifecycleOwner;
+    MutableLiveData<LifecycleOwner> fakeViewLifecycleOwnerLiveData = new MutableLiveData<>(null);
+    private LifecycleOwner fakeViewLifecycleOwner = null;
+
+    public FakeFragment(LifecycleOwner fragmentLifecycleOwner) {
+        this.fragmentLifecycleOwner = fragmentLifecycleOwner;
+    }
+
+    public void setFakeViewLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
+        fakeViewLifecycleOwner = lifecycleOwner;
+        fakeViewLifecycleOwnerLiveData.postValue(lifecycleOwner);
+    }
+
+    @NonNull
+    @Override
+    public LifecycleOwner getViewLifecycleOwner() {
+        return fakeViewLifecycleOwner;
+    }
+
+    @NonNull
+    @Override
+    public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {
+        return fakeViewLifecycleOwnerLiveData;
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return fragmentLifecycleOwner.getLifecycle();
+    }
+
+}
diff --git a/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/FakeLifecycleOwner.kt b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt
similarity index 71%
rename from dispatch-android-lifecycle-extensions/samples/src/test/java/samples/FakeLifecycleOwner.kt
rename to dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt
index f9cceec29..4d6d1003d 100644
--- a/dispatch-android-lifecycle-extensions/samples/src/test/java/samples/FakeLifecycleOwner.kt
+++ b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt
@@ -13,15 +13,15 @@
  * limitations under the License.
  */
 
-package samples
+package dispatch.internal.test.android
 
 import androidx.lifecycle.*
 import kotlinx.coroutines.*
 
 @Suppress("EXPERIMENTAL_API_USAGE")
 open class FakeLifecycleOwner(
-  private val mainDispatcher: CoroutineDispatcher = fakeMainDispatcher(),
-  initialState: Lifecycle.State = Lifecycle.State.INITIALIZED
+  initialState: Lifecycle.State = Lifecycle.State.INITIALIZED,
+  private val mainDispatcher: CoroutineDispatcher = fakeMainDispatcher()
 ) : LifecycleOwner {
 
   private val registry: LifecycleRegistry by lazy { LifecycleRegistry(this) }
@@ -38,6 +38,24 @@ open class FakeLifecycleOwner(
 
   override fun getLifecycle(): LifecycleRegistry = registry
 
+  fun stepDown() = when (lifecycle.currentState) {
+    Lifecycle.State.DESTROYED   -> throw IllegalArgumentException("already destroyed")
+    Lifecycle.State.INITIALIZED -> throw IllegalArgumentException(
+      "cannot transition straight from initialized to destroyed"
+    )
+    Lifecycle.State.CREATED     -> destroy()
+    Lifecycle.State.STARTED     -> stop()
+    Lifecycle.State.RESUMED     -> pause()
+  }
+
+  fun stepUp() = when (lifecycle.currentState) {
+    Lifecycle.State.DESTROYED   -> throw IllegalArgumentException("already destroyed")
+    Lifecycle.State.INITIALIZED -> create()
+    Lifecycle.State.CREATED     -> start()
+    Lifecycle.State.STARTED     -> resume()
+    Lifecycle.State.RESUMED     -> throw IllegalArgumentException("already resumed")
+  }
+
   fun create() = runBlocking(mainDispatcher) {
     lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
   }
diff --git a/dispatch-internal-test-android/src/test/resources/junit-platform.properties b/dispatch-internal-test-android/src/test/resources/junit-platform.properties
new file mode 100644
index 000000000..0e863bea1
--- /dev/null
+++ b/dispatch-internal-test-android/src/test/resources/junit-platform.properties
@@ -0,0 +1,15 @@
+#
+# Copyright (C) 2020 Rick Busarow
+# 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.
+#
+junit.jupiter.testinstance.lifecycle.default=per_class
diff --git a/settings.gradle.kts b/settings.gradle.kts
index df110ef43..9a88ccbd4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -32,4 +32,5 @@ include(":dispatch-core:samples")
 include(":dispatch-detekt")
 include(":dispatch-detekt:samples")
 include(":dispatch-internal-test")
+include(":dispatch-internal-test-android")
 include(":dispatch-sample")