Magnet Scoping / ViewModel injection
open class BaseMagnetActivity : AppCompatActivity(), ScopeOwner { | |
private val scopeModel: ScopeModel by lazy { ScopeModel.setup(this) } | |
private var testScope: Scope? = null | |
override var scope: Scope | |
get() = testScope ?: scopeModel.scope | |
@VisibleForTesting | |
set(value) { | |
testScope = value | |
} | |
} |
const val APPLICATION = "application" | |
open class BaseMagnetApplication : Application(), ScopeOwner { | |
@Suppress("UnsafeCast") | |
override val scope: Scope by lazy { | |
Magnet.createRootScope().apply { | |
bind(this@BaseMagnetApplication as Application) | |
bind(this@BaseMagnetApplication as Context, APPLICATION) | |
} | |
} | |
} |
open class BaseMagnetFragment : Fragment(), ScopeOwner { | |
private val scopeModel: ScopeModel by lazy { ScopeModel.setup(this) } | |
private var testScope: Scope? = null | |
override var scope: Scope | |
get() = testScope ?: scopeModel.scope | |
@VisibleForTesting | |
set(value) { | |
testScope = value | |
} | |
} |
@Instance(type = ActivityAware::class, scoping = Scoping.DIRECT) | |
class ActivityAware : LifecycleOwnerAware<FragmentActivity>() | |
@Instance(type = FragmentAware::class, scoping = Scoping.DIRECT) | |
class FragmentAware : LifecycleOwnerAware<Fragment>() | |
open class LifecycleOwnerAware<T : LifecycleOwner> { | |
private val subject = BehaviorSubject.createDefault<Instance>(Instance.Absent) | |
@Suppress("UNCHECKED_CAST") | |
fun <R> with(work: T.() -> R): R = | |
when (val value = subject.value) { | |
null, | |
is Instance.Absent -> error("no instance present") | |
is Instance.Exists<*> -> (value.instance as T).run(work) | |
} | |
@Suppress("UNCHECKED_CAST") | |
fun observe(): Observable<T> = | |
subject | |
.filter { it is Instance.Exists<*> } | |
.map { (it as Instance.Exists<*>).instance as T } | |
.hide() | |
internal fun setInstance(instance: T) { | |
subject.onNext(Instance.Exists(instance)) | |
} | |
internal fun clearInstance() { | |
subject.onNext(Instance.Absent) | |
} | |
private sealed class Instance { | |
class Exists<T>(val instance: T) : Instance() | |
object Absent : Instance() | |
} | |
} |
class MyExampleFragment : BaseFragment() { | |
private lateinit var viewModel: MyViewModel | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
viewModel = scope.getSingle() | |
} | |
} |
class MyExampleFragmentTest { | |
@Mock | |
private lateinit var myViewModel: MyViewModel | |
private val stateSubject = PublishSubject.create<MyState>() | |
private val effectSubject = PublishSubject.create<MyEffect>() | |
private val testScope = Magnet.createRootScope() | |
@Before | |
fun setup() { | |
MockitoAnnotations.initMocks(this) | |
testScope.apply { | |
bind(MyViewModel::class.java, myViewModel) | |
} | |
myViewModel.stub { | |
on { observeState() } doReturn stateSubject | |
on { observeEffects() } doReturn effectSubject | |
} | |
} | |
@Test | |
fun shouldRenderScreen() { | |
launchFragment() | |
stateSubject.onNext(SOME_FIXTURE_STATE) | |
// verify now with Espresso | |
} | |
// needs JetPack's fragment-testing artifact | |
private fun launchFragment(): FragmentScenario<MyExampleFragment> = | |
launchFragmentInContainer { | |
MyExampleFragment().apply { | |
this.scope = testScope | |
} | |
} | |
companion object { | |
private val SOME_FIXTURE_STATE = MyState(...) | |
} | |
} |
sealed class MyState { ... } | |
sealed class MyEffect { ... } | |
@Instance( | |
type = MyViewModel::class, | |
factory = ViewModelFactory::class, | |
scoping = Scoping.UNSCOPED | |
) | |
class MyViewModel : ViewModel() { | |
fun observeState(): Observable<MyState> = ... | |
fun observeEffects(): Observable<MyEffect> = ... | |
} |
class ScopeModel(context: Context) : ViewModel(), DefaultLifecycleObserver, ScopeOwner { | |
override val scope: Scope by lazy { | |
context.createSubscope {} | |
} | |
override fun onCreate(owner: LifecycleOwner) { | |
scope.apply { | |
if (owner is FragmentActivity) { | |
getSingle(ActivityAware::class.java).setInstance(owner) | |
} | |
if (owner is Fragment) { | |
getSingle(FragmentAware::class.java).setInstance(owner) | |
} | |
} | |
} | |
override fun onDestroy(owner: LifecycleOwner) { | |
scope.apply { | |
if (owner is FragmentActivity) { | |
getSingle(ActivityAware::class.java).clearInstance() | |
} | |
if (owner is Fragment) { | |
getSingle(FragmentAware::class.java).clearInstance() | |
} | |
} | |
} | |
@Suppress("UNCHECKED_CAST") | |
companion object { | |
fun setup(activity: FragmentActivity): ScopeModel { | |
val model = ViewModelProviders.of(activity, object : ViewModelProvider.Factory { | |
override fun <T : ViewModel?> create(modelClass: Class<T>): T = | |
ScopeModel(activity.application) as T | |
}).get(ScopeModel::class.java) | |
activity.lifecycle.addObserver(model) | |
return model | |
} | |
fun setup(fragment: Fragment): ScopeModel { | |
val model = ViewModelProviders.of(fragment, object : ViewModelProvider.Factory { | |
override fun <T : ViewModel?> create(modelClass: Class<T>): T = | |
ScopeModel(fragment.requireActivity()) as T | |
}).get(ScopeModel::class.java) | |
fragment.lifecycle.addObserver(model) | |
return model | |
} | |
} | |
} |
interface ScopeOwner { | |
val scope: Scope | |
} | |
fun Context?.createSubscope(initializer: Scope.() -> Unit): Scope { | |
val parentScopeOwner = this as? ScopeOwner ?: error("$this is expected to implement ScopeOwner") | |
return parentScopeOwner.scope.createSubscope { initializer(this) } | |
} |
const val VIEW_MODEL_KEY = "view-model-key" | |
class ViewModelFactory<T : ViewModel> : Factory<T> { | |
override fun create( | |
scope: Scope, | |
type: Class<T>, | |
classifier: String, | |
scoping: Scoping, | |
instantiator: Factory.Instantiator<T> | |
): T { | |
val key = scope.getOptional(String::class.java, VIEW_MODEL_KEY) | |
if (key != null && scoping != Scoping.UNSCOPED) { | |
error("ViewModel '$type' with key '$key' must be declared with Scoping.UNSCOPED") | |
} | |
val androidViewModelFactory = AndroidViewModelFactory(scope, instantiator) | |
val provider = getProviderFor(scope, androidViewModelFactory) | |
return if (key == null) provider.get(type) else provider.get(key, type) | |
} | |
private fun getProviderFor(scope: Scope, viewModelProviderFactory: ViewModelProvider.Factory): ViewModelProvider { | |
// fragment's have precedence over activities in the given scope | |
val fragmentAware = scope.getOptional(FragmentAware::class.java) | |
if (fragmentAware != null) { | |
return fragmentAware.with { | |
ViewModelProviders.of(this, viewModelProviderFactory) | |
} | |
} | |
val activityAware = scope.getSingle(ActivityAware::class.java) | |
return activityAware.with { | |
ViewModelProviders.of(this, viewModelProviderFactory) | |
} | |
} | |
private class AndroidViewModelFactory<T>( | |
private var scope: Scope, | |
private var instantiator: Factory.Instantiator<T> | |
) : ViewModelProvider.Factory { | |
@Suppress("UNCHECKED_CAST") | |
override fun <T : ViewModel?> create(modelClass: Class<T>): T = | |
instantiator.instantiate(scope) as T | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment