Assisted Inject for AAC ViewModels with vanilla Dagger2
Into the Part #7 of the Dagger series, we saw how complicated ViewModel
injection can be on Android, but that can be easily fixed with @AssistedInject
.
For this to work we need @AssistedInject
on a constructor, an @AssistedFactory
for the ViewModel
factory and @Assisted
for each parameter we’ll be providing as an outside dependency.
Note that this is easily solved with @ViewModelInject
from Hilt but in a pure Dagger world this is how it’ll work.
By assistedViewModel
Each ViewModel
can have a SavedStateHandle
for your needs, in order to get an access to that dependency we need to implement AbstractSavedStateViewModelFactory
while leveraging the lazy creation of the delegate that’s already provided to us by viewModels<>
.
Upon looking closely
1
2
3
4
5
6
object : AbstractSavedStateViewModelFactory(TODO()) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
)
after implementing the factory we have the handle, all we need to do is give this factory to the Fragment.viewModels
delegate as it awaits a factoryProducer
1
2
3
4
5
6
7
8
9
10
11
12
inline fun <reified T : ViewModel> Fragment.assistedViewModel(
crossinline viewModelProducer: (SavedStateHandle) -> T
) = viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) =
viewModelProducer(handle) as T
}
}
we can have a nice delegate functionality by assistedViewModel
in our Fragment
s now.
Assisted ViewModel
Let’s take an example of a ViewModel
that looks like the following, pretty simple, ExampleNetworkSearchService
is provided from our SingletonComponent
.
1
2
3
4
5
6
7
8
class SearchViewModel @AssistedInject constructor(
private val searchService: ExampleNetworkSearchService,
@Assisted savedStateHandle: SavedStateHandle
) : ViewModel() {
private val args = SearchFragmentArgs.fromSavedStateHandle(savedStateHandle)
private val query = args.query
private val sort = args.sort
}
Assisted ViewModel factory
In order to facilitate the usage of a factory, an assisted factory has the job to create the dependency it provides, in this case a SearchViewModel
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SearchViewModel @AssistedInject constructor(
private val searchService: ExampleNetworkSearchService,
@Assisted savedStateHandle: SavedStateHandle
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(
savedStateHandle: SavedStateHandle
): SearchViewModel
}
private val args = SearchFragmentArgs.fromSavedStateHandle(savedStateHandle)
private val query = args.query
private val sort = args.sort
}
Assisted inside a Fragment
and inside the SearchFragment
we can leverage
1
2
3
4
5
6
7
8
9
10
11
12
13
class SearchFragment : Fragment(R.layout.fragment_search) {
override fun onAttach(context: Context) {
fragmentComponent.inject(this)
super.onAttach(context)
}
private val binding by viewBinding(FragmentSearchBinding::bind)
@Inject
lateinit var searchViewModelFactory: SearchViewModel.Factory
private val searchViewModel by assistedViewModel { savedStateHandle -> searchViewModelFactory.create(savedStateHandle) }
}
The way this works is that you’re providing the @AssistedInject
annotation dependency that you already have in the Dagger graph in our case an ExampleNetworkSearchService
because Dagger knows about this and doesn’t need an assistance, but what Dagger graph doesn’t have is access to SavedStateHandle
and we assist Dagger with a dependency that’s just created (creation time assistance).
The downside to this is having one factory per one ViewModel
, a lot of boilerplate to be written, also if you inject assisted dependencies that outlive the ViewModel
’s lifecycle that means you’ll have memory leaks that you haven’t handled (dependencies like Fragment’s or Activitiy’s own stuff, even worse a View).
This is just to show the easy it can be without having to rely on MultiMap
bindings from Dagger.
However Hilt makes all of this go away and should be the preferred solution for writing injectable ViewModel
s in your code unless you have a ton of Dagger code that can’t be migrated.
Happy coding.