'How to do Assisted Injection with Navigation Compose?
I've a composable called ParentScreen
and a ViewModel
named ParentViewModel
. Inside the ParentViewModel
, I am collecting a value from my repo.
class MyRepo @Inject constructor() {
fun getParentData() = System.currentTimeMillis().toString() // some dummy value
}
@HiltViewModel
class ParentViewModel @Inject constructor(
myRepo: MyRepo
) : ViewModel() {
private val _parentData = MutableStateFlow("")
val parentData = _parentData.asStateFlow()
init {
val realData = myRepo.getParentData()
_parentData.value = realData
}
}
@Composable
fun ParentScreen(
parentViewModel: ParentViewModel = hiltViewModel()
) {
val parentData by parentViewModel.parentData.collectAsState()
ChildWidget(parentData = parentData)
}
Inside the ParentScreen
composable, I have a ChildWidget
composable and it has its own ViewModel
named ChildViewModel
.
@HiltViewModel
class ChildViewModel @AssistedInject constructor(
@Assisted val parentData: String
) : ViewModel() {
@AssistedFactory
interface ChildViewModelFactory {
fun create(parentData: String): ChildViewModel
}
init {
Timber.d("Child says data is $parentData ")
}
}
@Composable
fun ChildWidget(
parentData: String,
childViewModel: ChildViewModel = hiltViewModel() // How do I supply assisted injection factory here?
) {
// Code omitted
}
Now, I want to get parentData
inside ChildViewModel
's constructor.
Questions
- How do I supply
ChildViewModelFactory
to Navigation Compose'shiltViewModel
method? - If that's not possible, what would be the most suitable method to inject an object from the parent composable to the child composable's
ViewModel
? How about creating alateinit
property andinit
method like below?
@HiltViewModel
class ChildViewModel @Inject constructor(
) : ViewModel() {
lateinit var parentData: Long
fun init(parentData: Long){
if(this::parentData.isInitialized) return
this.parentData = parentData
}
}
Solution 1:[1]
You can do this using EntryPointAccessors
(from Hilt) and a ViewModelProvider.Factory
from View Model library.
In my sample app, BookFormScreen
is using BookFormViewModel
and the view model needs to load a book based on a bookId
passed by the previous screen. This is what I did:
class BookFormViewModel @AssistedInject constructor(
...
@Assisted private val bookId: String?,
) : ViewModel() {
...
@AssistedFactory
interface Factory {
fun create(bookId: String?): BookFormViewModel
}
companion object {
@Suppress("UNCHECKED_CAST")
fun provideFactory(
assistedFactory: Factory, // this is the Factory interface
// declared above
bookId: String?
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(bookId) as T
}
}
}
}
Notice that I'm not using @HiltViewModel
. The provideFactory
will be use to supply a factory to create this view model.
Now, you need to define a composable function to provide the view model using this factory.
@Composable
fun bookFormViewModel(bookId: String?): BookFormViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
ViewModelFactoryProvider::class.java
).bookFormViewModelFactory()
return viewModel(factory = BookFormViewModel.provideFactory(factory, bookId))
}
If you're using the navigation library, you can add the ViewModelStoreOwner
parameter in this function and use it in viewModel()
function call. For this parameter, you can pass the NavBackStackEntry
object, with this, the view model will be scoped to that particular back stack entry.
Finally, you can use your view model in your composable.
val bookFormViewModel: BookFormViewModel = bookFormViewModel(bookId)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | nglauber |