Constructor injection for fragments and view models using Dagger2 with minimal boilerplate.
Replace $version
below with the version number in the badge above.
dependencies {
implementation "ph.codeia.shiv:shiv-runtime:$version"
// for java:
annotationProcessor "ph.codeia.shiv:shiv-compiler:$version"
// for kotlin:
kapt "ph.codeia.shiv:shiv-compiler:$version"
}
- Dagger in your build dependencies
- no existing binding to
FragmentFactory
andViewModelProvider.Factory
in the graph - constructor injection only
- using
androidx.*
packages - all view models are owned by the activity and thus shared by every fragment
The following code examples are simplified in order to highlight the important parts
of the process. Please see the demo module to see how the components typically look
like in actual projects. All generated code are Java 6/7-compatible, so Kotlin or
(target|source)Compatibility "1.8"
is not required. I would still recommend at
least using Java 8 though.
-
Define the classes to be injected. This step is done first in order to trigger the module code generation. You can fill in the dependencies later. Hit
Ctrl-F9
to build the project.class LoginFragment @Inject constructor() : Fragment(R.layout.fragment_login) class LoginModel @Inject constructor() : ViewModel()
-
Define the components. Since fragments are shorter-lived than view models, all injected fragments must be bound in a subcomponent of wherever the view models are bound. This is so that you can't inject view-related objects into a view model's constructor and cause a memory leak.
Install the generated
shiv.SharedViewModelProviders
module to the view model component. In the factory or builder interface of the component, add a@BindsInstance
-annotatedViewModelStoreOwner
parameter/builder method. Expose the fragment subcomponent or its factory/builder here.@Component(modules = [shiv.SharedViewModelProviders::class]) interface ModelComponent { val viewComponent: ViewComponent @Component.Factory interface Factory { fun create(@BindsInstance owner: ViewModelStoreOwner): ModelComponent } }
The
shiv.
part is needed in kotlin. In java, you can import the generated module and just use the class name. To do the same in kotlin, you must addkapt { correctErrorTypes = true }
in your gradle script. -
Install the bundled
ShivModule
and the generatedshiv.FragmentBindings
modules into the subcomponent. Expose the typeFragmentFactory
from the subcomponent. Rebuild the project withCtrl-F9
to generate the Dagger implementations.@Subcomponent(modules = [ShivModule::class, shiv.FragmentBindings::class]) interface ViewComponent { val fragmentFactory: FragmentFactory }
-
Override the activity's
#onCreate
method to use the fragment factory built by Dagger. Make sure to do this before callingsuper.onCreate(...)
.class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { supportFragmentManager.fragmentFactory = DaggerModelComponent.factory() .create(this) .viewComponent .fragmentFactory super.onCreate(savedInstanceState) } }
-
Go back and fill in the rest of the fragment and view model classes. Install additional modules as required. Qualify view model dependencies with
@Shared
. If you forget to use@Shared
in the fragment's constructor parameter, you would get an orphan view model that would not survive configuration changes.class LoginFragment @Inject constructor( @Shared private val model: LoginModel ) : Fragment(R.layout.fragment_login) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // ... val submitButton = view.findViewById(R.id.submit_button) submitButton.setOnClickListener { model.login() } model.state().observe(viewLifecycleOwner) { // ... } } } class LoginModel @Inject constructor( private val auth: AuthService ) : ViewModel() { private val state = MutableLiveData<LoginState>().also { it.value = LoginState() } fun state(): LiveData<LoginState> = state fun login() { // ... } }
DO NOT request
Fragment
classes fromViewModel
s because even though it might compile, you won't get the same instance attached to the activity. If you somehow managed to inject a live fragment to a view model, that is a bad situation that you must rectify because you have just leaked the activity context and all the views hanging on it. -
After this, you can write new fragments and view models and they will automatically be part of the object graph. Just don't forget the
@Inject
and@Shared
annotations.
AndroidViewModel
s are view models that have a constructor dependency on an
Application
. The view model can then build other objects that need an application
context (e.g. anything that needs file IO). If you really need an application, you
can bind it to the Dagger graph via @BindsInstance
in a (sub)component factory/builder
and it will be provided to your view model constructor. You probably don't need
an Application
though but a service that depends on Context
. For clarity, it's
best to depend directly on that service instead.
SavedStateHandle
allows view models constructed by SavedStateViewModelFactory
to read and write to a Bundle
that persists not only across configuration changes
but also process death and recreation. When a SavedStateHandle
is requested in
an injectable constructor, the codegen creates an extra provider method in
shiv.SharedViewModelProviders
that returns a SavedStateHandle
that is properly
tied to the recreation cycle of the fragment or activity. This relies on the fact
that FragmentActivity
and Fragment
implement HasDefaultViewModelProviderFactory
that returns a SavedStateViewModelFactory
that is used to create and attach a
view model instance that serves only to hold a SavedStateHandler
. It will not
work on any other ViewModelStoreOwner
(are there other kinds of VM store owners?).
TL;DR: it just works. You can freely add a SavedStateHandle
dependency in your
view model constructor.
Sometimes you want a view model that is scoped to a particular fragment and not to
the activity. You might want the data to survive configuration changes, but you
also want the data to go away when the fragment is detached so that when the fragment
is re-attached, you get back a clean slate. In this case, you shouldn't inject
a @Shared
view model to a fragment, but instead depend on a ViewModelProvider.Factory
and build your own view models using ViewModelProvider
or the Jetpack viewModels
extension.
When the interface ViewModelProvider.Factory
is requested anywhere in the graph,
the shiv
processor generates a module called shiv.ViewModelBindings
that should
be added to your Dagger graph. The bundled ShivModule
module itself binds an
implementation of the ViewModelProvider.Factory
that relies on this generated
module to populate a map multibinding of view model providers.
@Subcomponent(modules = [ShivModule::class, shiv.FragmentBindings::class, shiv.ViewModelBindings::class])
interface ViewComponent {
// ...
}
class SomeFragment @Inject constructor(
private val vmFactory: ViewModelProvider.Factory
) : Fragment(R.layout.some_layout) {
// using jetpack lifecycle-viewmodel-ktx
private val model: SomeViewModel by viewModels { vmFactory }
// ...
}
Note that SomeViewModel
above must be reachable by Dagger, so an @Inject
ed
constructor is required even if it's empty.
Don't install the ShivModule
module. Instead, use the concrete types InjectingFragmentFactory
and InjectingViewModelFactory
anywhere you use FragmentFactory
and
ViewModelProvider.Factory
respectively.
Not really related to fragments or view models, but this library also provides a generator for injectable factories for classes with constructor arguments that vary widely and is likely only known at the call site. This accomplishes the same goals as assisted injection but in a very simplistic and less flexible manner.
Suppose you have a class like this and you want Dagger to build it for you:
class LoginView {
private final FragmentLoginBindings bindings;
private final LoginPresenter presenter;
LoginView(View root, LoginPresenter presenter) {
bindings = FragmentLoginBindings.bind(root);
this.presenter = presenter;
}
}
The View
object that the constructor needs is obtained very late and it's not
very practical to create a subgraph at the call site just for this class. What
you'd usually do is write a factory with the late-bound objects in the operative
method and the rest constructor-injected. This could then be easily built by
Dagger:
class LoginView {
//...
static class Factory {
private final LoginPresenter presenter;
@Inject
Factory(LoginPresenter presenter) {
this.presenter = presenter;
}
LoginView create(View root) {
return new LoginView(root, presenter);
}
}
}
Then you could have your LoginFragment
request a LoginView.Factory
in the
constructor and call loginViewFactory.create(view)
in the #onViewCreated(View, Bundle?)
method.
class LoginFragment {
private final LoginView.Factory viewFactory;
@Inject
LoginFragment(LoginView.Factory viewFactory){
super(R.layout.fragment_login);
this.viewFactory = viewFactory;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
LoginView loginView = viewFactory.create(view);
// ...
}
}
This pattern is useful but painful to do by hand, especially in Java. It can be
automated by annotating the late-bound constructor parameters with @LateBound
.
class LoginView {
// ...
LoginView(@LateBound View root, LoginPresenter presenter) {
// ...
}
}
This triggers the generation of a class named PartialLoginView
in the same package
that is implemented a lot like the LoginView.Factory
example above. Your fragment
could then request a PartialLoginView
then call its #bind(View)
method in the
fragment hook. It's the same code as before, just with different names:
class LoginFragment {
private final PartialLoginView partialView;
@Inject
LoginFragment(PartialLoginView partialView) {
this.partialView = partialView;
}
@Override
public void onViewCreate(View view, @Nullable Bundle savedInstanceState) {
LoginView loginView = partialView.bind(view);
// ...
}
}
You could have any number of @LateBound
parameters in any position. The operative
method name is hard-coded as bind
and its parameters are all the
@LateBound
-annotated parameters in the order they appear in the constructor.
The example outlined in the section above is simplified but is not as simple as it
can get. If you are sure you wouldn't break the "no Context
dependency" rule in
view models and don't care about scopes at all, you could put everything in a
single Dagger component:
@Component(modules = [shiv.SharedViewModelProviders::class, shiv.FragmentBindings::class])
interface AppComponent {
val fragmentFactory: InjectingFragmentFactory
@Component.Factory
interface Factory {
fun create(@BindsInstance owner: ViewModelStoreOwner): AppComponent
}
}
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
DaggerAppComponent.factory()
.create(this)
.fragmentFactory
.let { supportFragmentManager.fragmentFactory = it }
super.onCreate(savedInstanceState)
}
}
class FooFragment @Inject constructor(
@Shared private val model: FooViewModel
) : Fragment(R.layout.fragment_foo) {
// ...
}
class FooViewModel @Inject constructor(
private val savedState: SavedStateHandle
) : ViewModel() {
// ...
}
MIT License
Copyright (c) 2020 Mon Zafra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.