Skip to content

Instantly share code, notes, and snippets.

@trevjonez
Last active June 2, 2020 19:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trevjonez/0535c396b7b79a8fa85827f79f543c52 to your computer and use it in GitHub Desktop.
Save trevjonez/0535c396b7b79a8fa85827f79f543c52 to your computer and use it in GitHub Desktop.
VM injection simplified example
class ActualUsecaseActivity: SomeActivityBaseClass(), ViewModelInjected<UsecaseViewModel> {
/* Live template to generate this property */
@Inject
lateinit var viewModelHolder: ViewModelHolder<UsecaseViewModel>
override fun onLifecycleStuffs() {
viewModel.statefulReactiveThing.subscribe { theState ->
viewProperty.text = theState.text
[...]
} //addTo disposable, lifecycle aware helpers, implicit coroutine scope etc...
}
/* Live template to generate this module */
@Module
object ProvidesModule {
@Provides
fun viewModelHolder(
vmStoreOwner: ActualUsecaseActivity, //navGraph, activity, fragment, or custom impl
adaptedFactory: ProviderAdaptedFactory<UsecaseViewModel>
) = ViewModelHolder.moduleMethod(vsStoreOwner, adaptedFactory)
}
}
//Converts the dagger generated provider to a ViewModelProvider.Factory
//Could be changed to use a multi bound map of providers, though in practice this 1:1 pattern has worked great.
class ProviderAdaptedFactory<VM : ViewModel>
@Inject constructor(private val modelProvider: Provider<VM>) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
modelProvider.get() as T
}
//Implicitly scoped to the viewModelStoreOwner used in your provides method
class UsecaseViewModel @Inject constructor([...]): ViewModel() {
[...]
}
interface ViewModelHolder<VM : ViewModel> {
/**
* If backed by the default ViewModelProviders this get method impl MUST ALWAYS BE LAZY
* in order to ensure you get the correct VM instance.
*
* In Detail:
* ViewModelStore's for activity and fragment depend on fragment manager state,
* fragment manager state isn't restored until after the point injection must happen.
*/
@get:MainThread
val viewModel: VM
companion object {
inline fun <reified VM : ViewModel> moduleMethod(
storeOwner: ViewModelStoreOwner,
adaptedFactory: ProviderAdaptedFactory<VM>
) = object : ViewModelHolder<VM> {
@get:MainThread
override val viewModel: VM by lazy(LazyThreadSafetyMode.NONE) {
ViewModelProvider(storeOwner, adaptedFactory)
.get<VM>(VM::class.java)
}
}
}
}
//Optional way to consume this pattern so viewModel is implicitly available
interface ViewModelInjected<VM: ViewModel> {
/* should always be @Inject lateinit var */
val viewModelHolder: ViewModelHolder<VM>
val viewModel: VM
get() = viewModelHolder.viewModel
}

I believe that since an activity or fragment subcomponent scope life is bounded to the instance of said activity or fragment, perhaps we should question the correctness of providing the view models via those components. They are, by design, intended to outlive the scope of a single activity or fragment instance if needed which exceeds the scope of the component providing them.

Due to this shoehorning of scopes it might introduce the possibility of memory leaks. If the view model were to in some way hold a reference to the component that initially injected it, it would then become a leak source of the activity or fragment which it was intended to outlive.

Perhaps a more correct approach would be a top level finite state machine that would become a domain model aware implementation of a ViewModelStore which could then be used. State persistance and restoration as well as runtime specific data (intent/bundle args) at that point could become an issue that might make constructor injection impossible and require late init type functionality.

At that point, if alternative approaches were considered I believe it would be best to remove the usage of arch components view model and potentially even dagger-android for a custom mechanism. Major drawbacks being: increased learning overhead when adding new team members, as well as a high probability of less test coverage, increased boilerplate for handling injection in android (at least until minsdk=28 or dagger-hilt?).

The approach shown in this gist does a decent job of pushing all the necessary configuration and dependency delivery into dagger which is its intended goal, the down sides being the issue of the scope shoehorning which is the key driver of the complexity in injecting view models that are not owned by a dagger component directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment