Created
July 19, 2019 15:27
-
-
Save rharter/5939bb73207aeb6f24c8522ecf4c9d72 to your computer and use it in GitHub Desktop.
An ActivityScenario that allows you to use Dagger Android's automatic, lifecycle based injection without making your Application class `open`, or overriding it in tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.pixite.pigment.testing | |
import android.app.Activity | |
import android.app.Instrumentation | |
import android.content.Context | |
import android.content.Intent | |
import androidx.fragment.app.Fragment | |
import androidx.fragment.app.FragmentActivity | |
import androidx.fragment.app.FragmentManager | |
import androidx.lifecycle.Lifecycle | |
import androidx.test.core.app.ActivityScenario | |
import androidx.test.runner.lifecycle.ActivityLifecycleCallback | |
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry | |
import androidx.test.runner.lifecycle.Stage | |
import java.io.Closeable | |
/** | |
* Creates an [InjectableActivityScenario] that uses the supplied injections. | |
* | |
* ``` | |
* injectableActivityScenario<MyActivity> { | |
* injectActivity { // this: MyActivity | |
* dependency1 = myMockDependency1 | |
* dependency2 = myMockDependency2 | |
* } | |
* injectFragment<MyFragment> { // this: MyFragment | |
* dependency1 = myMockDependency1 | |
* } | |
* } | |
* ``` | |
*/ | |
inline fun <reified T : Activity> injectableActivityScenario(injector: InjectableActivityScenario<T>.() -> Unit) = | |
InjectableActivityScenario(T::class.java).apply { | |
this.injector() | |
} | |
/** | |
* Adds an injector to the target [InjectableActivityScenario] for the given [Activity]. | |
*/ | |
inline fun <reified A : Activity> InjectableActivityScenario<*>.injectActivity(noinline injector: A.() -> Unit) { | |
injectActivity(A::class.java, injector) | |
} | |
/** | |
* Adds an injector to the target [InjectableActivityScenario] for the given [Fragment]. | |
*/ | |
inline fun <reified F : Fragment> InjectableActivityScenario<*>.injectFragment(noinline injector: F.() -> Unit) { | |
injectFragment(F::class.java, injector) | |
} | |
/** | |
* InjectableActivityScenario is an extension and wrapper of [ActivityScenario] which allows you to easily inject | |
* any [Activity]s and [Fragment]s that need to be loaded from the launch Activity. | |
* | |
* ``` | |
* val activityScenario = InjectableActivityScenario(MyActivity::class.java) | |
* activityScenario.injectActivity { // this: MyActivity | |
* dependency1 = myMockDependency1 | |
* dependency2 = myMockDependency2 | |
* } | |
* activityScenario.injectFragment(MyFragment::class.java) { // this: MyFragment | |
* dependency1 = myMockDependency1 | |
* } | |
* ``` | |
*/ | |
class InjectableActivityScenario<T : Activity>(private val activityClass: Class<T>) : AutoCloseable, Closeable { | |
private var delegate: ActivityScenario<T>? = null | |
private val activityInjectors = mutableListOf<ActivityInjector<out Activity>>() | |
private val fragmentInjectors = mutableListOf<FragmentInjector<out Fragment>>() | |
fun launch(startIntent: Intent? = null): InjectableActivityScenario<T> { | |
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(activityLifecycleObserver) | |
delegate = if (startIntent != null) { | |
ActivityScenario.launch(startIntent) | |
} else { | |
ActivityScenario.launch(activityClass) | |
} | |
return this | |
} | |
override fun close() { | |
delegate?.close() | |
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(activityLifecycleObserver) | |
} | |
fun moveToState(newState: Lifecycle.State): InjectableActivityScenario<T> { | |
val d = | |
delegate ?: throw IllegalStateException("Cannot move to state $newState since the activity hasn't been launched.") | |
d.moveToState(newState) | |
return this | |
} | |
fun recreate(): InjectableActivityScenario<T> { | |
val d = delegate ?: throw IllegalStateException("Cannot recreate the activity since it hasn't been launched.") | |
d.recreate() | |
return this | |
} | |
fun onActivity(action: (T) -> Unit): InjectableActivityScenario<T> { | |
val d = delegate ?: throw IllegalStateException("Cannot run onActivity since the activity hasn't been launched.") | |
d.onActivity(action) | |
return this | |
} | |
fun runOnMainThread(action: (T) -> Unit) { | |
val d = delegate ?: throw IllegalStateException("Cannot run onActivity since the activity hasn't been launched.") | |
d.onActivity(action) | |
} | |
fun getResult(): Instrumentation.ActivityResult = | |
delegate?.result ?: throw IllegalStateException("Cannot get result since activity hasn't been launched.") | |
/** | |
* Injects the target Activity using the supplied [injector]. | |
* | |
* ``` | |
* activityTestRule.addActivityInjector { | |
* // this is the target Activity | |
* dependency = fakeDependency | |
* } | |
* ``` | |
*/ | |
fun injectActivity(injector: T.() -> Unit) { | |
activityInjectors.add(ActivityInjector(activityClass, injector)) | |
} | |
fun <A : Activity> injectActivity(activityClass: Class<A>, injector: A.() -> Unit) { | |
activityInjectors.add(ActivityInjector(activityClass, injector)) | |
} | |
fun <F : Fragment> injectFragment(fragmentClass: Class<F>, injector: F.() -> Unit) { | |
fragmentInjectors.add(FragmentInjector(fragmentClass, injector)) | |
} | |
fun <F : Fragment> injectFragment(fragment: F, injector: F.() -> Unit) { | |
fragmentInjectors.add( | |
FragmentInjector( | |
fragment::class.java, | |
injector | |
) | |
) | |
} | |
private class ActivityInjector<A : Activity>( | |
private val activityClass: Class<A>, | |
private val injector: A.() -> Unit | |
) { | |
fun inject(activity: Activity?): Boolean { | |
if (activityClass.isInstance(activity)) { | |
activityClass.cast(activity)!!.injector() | |
return true | |
} | |
return false | |
} | |
} | |
private class FragmentInjector<F : Fragment>( | |
private val fragmentClass: Class<F>, | |
private val injection: F.() -> Unit | |
) { | |
fun inject(fragment: Fragment?): Boolean { | |
if (fragmentClass.isInstance(fragment)) { | |
fragmentClass.cast(fragment)!!.injection() | |
return true | |
} | |
return false | |
} | |
} | |
private val fragmentCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { | |
override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) { | |
fragmentInjectors.forEach { | |
if (it.inject(f)) return | |
} | |
} | |
} | |
private val activityLifecycleObserver = object : ActivityLifecycleCallback { | |
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) { | |
when (stage) { | |
Stage.PRE_ON_CREATE -> { | |
if (activity is FragmentActivity) { | |
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentCallbacks, true) | |
} | |
activityInjectors.forEach { if (it.inject(activity)) return } | |
} | |
Stage.DESTROYED -> { | |
if (activity is FragmentActivity) { | |
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentCallbacks) | |
} | |
} | |
else -> { | |
// no op | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyActivityTest { | |
@get:Rule val instantExecutorRule = InstantTaskExecutorRule() | |
private lateinit var activityScenario: InjectableActivityScenario<MyActivity> | |
private lateinit var sharedViewModel: MySharedViewModel | |
private lateinit var homeViewModel: HomeFragmentViewModel | |
private lateinit var detailViewModel: DetailViewModel | |
private lateinit var fakeNavigator: FakeNavigator | |
@Before fun setup() { | |
sharedViewModel = mock { | |
// ... | |
} | |
homeViewModel = mock { | |
// ... | |
} | |
detailViewModel = mock { | |
// ... | |
} | |
fakeNavigator = FakeNavigator() | |
activityScenario = injectableActivityScenario<MyActivity> { | |
injectActivity { | |
viewModelFactory = viewModelFactoryFor(sharedViewModel) | |
navigator = fakeNavigator | |
} | |
injectFragment(HomeViewModel::class.java) { | |
viewModelFactory = viewModelFactoryFor(homeViewModel) | |
} | |
injectFragment(DetailViewModel::class.java) { | |
viewModelFactory = viewModelFactoryFor(detailViewModel) | |
} | |
}.launch() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment