Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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's hiltViewModel 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 a lateinit property and init 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
    }
}
like image 787
theapache64 Avatar asked Mar 14 '26 23:03

theapache64


1 Answers

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.

Then, define the ViewModelFactoryProvider for the entry point:

@EntryPoint
@InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {

    fun bookDetailsViewModelFactory(): BookDetailsViewModel.Factory

    fun bookFormViewModelFactory(): BookFormViewModel.Factory
}

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)
like image 176
nglauber Avatar answered Mar 17 '26 14:03

nglauber



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!