androidandroid-jetpack-composeandroid-viewmodeldagger-hiltjetpack-compose-navigation

Share viewmodel from activity to compose function using hilt


My app uses hilt and I have some work with LoadManager inside my activity that read contacts using ContentResolver and when I finish work I get the cursor that I send to my viewModel in order to process the data and do some business logic which for that I declared the following on top of my activity :

@AndroidEntryPoint
class MainActivity : ComponentActivity(), LoaderManager.LoaderCallbacks<Cursor> {
    private val contactsViewModel: ContactsViewModel by viewModels()
 ...

such that I use it inside onLoadFinished :

    override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
  
                contactsViewModel.updateContactsListFromCursor(cursor, loader.id)
     }

Inside my viewModel I have the following code which updates the ui state of the list with the contacts to be displayed:

data class ContactsListUiState(
    val contacts: MutableList<Contact>,
    val searchFilter: String)

@HiltViewModel
class ContactsViewModel @Inject constructor() : ViewModel() {
    private val _contactsListUiState =
        MutableStateFlow(ContactsListUiState(mutableStateListOf(), ""))
    val contactsListUiState: StateFlow<ContactsListUiState> = _contactsListUiState.asStateFlow()

    private fun updateContactsList(filter: String) {
        viewModelScope.launch(Dispatchers.IO) {
            ...

            _contactsListUiState.update { currentState ->
                currentState.copy(contacts = list, searchFilter = filter)
            }
        }

Finally, I am supposed to display the contacts that a LazyColumn and I pass the viewModel to my composable function using hilt following the official documentation :

@Composable
fun ContactsListScreen(
       navController: NavController,
       modifier: Modifier = Modifier, viewModel: ContactsViewModel = hiltViewModel()
   ) {
       val uiState by viewModel.contactsListUiState.collectAsStateWithLifecycle()
       ...

But when i access uiState.contacts it is empty and my lists does not show anything and I also noticed that the contactsViewModel which I used in the activity is not the same viewModel instance that I got from hiltViewModel() inside the composable function which probably causes this problem..

Any suggestions how to share the sameViewModel between the activity and the composable functions assuming that I have to call the viewModel from the onLoadFinished function(which is not composable) where I get the cursor therefore I must have a viewModel reference inside the activity itself


Solution

  • Based on the docs.

    The function hiltViewModel() returns an existing ViewModel or creates a new one scoped to the current navigation graph present on the NavController back stack. The function can optionally take a NavBackStackEntry to scope the ViewModel to a parent back stack entry.

    It turns out the factories create a new instance of the ViewModel when they are part of a Navigation Graph. But since you already found out that to make it work you have to specify the ViewModelStoreOwner, so I took an approach based my recent answer from this post, and created a CompositionLocal of the current activity since its extending ComponentActivity being it as a ViewModelStoreOwner itself.

    Here's my short attempt that reproduces your issue with the possible fix.

    Activity

    @AndroidEntryPoint
    class HiltActivityViewModelActivity : ComponentActivity() {
    
        private val myViewModel: ActivityScopedViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContent {
                    CompositionLocalProvider(LocalActivity provides this@HiltActivityViewModelActivity) {
                        Log.e("ActivityScopedViewModel", "Hashcode: ${myViewModel.hashCode()} : Activity Scope")
                        HiltActivitySampleNavHost()
                }
            }
        }
    }
    

    ViewModel

    @HiltViewModel
    class ActivityScopedViewModel @Inject constructor(): ViewModel() {}
    

    Local Activity Composition

    val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
        error("LocalActivity is not present")
    }
    

    Simple Navigation Graph

    enum class HiltSampleNavHostRoute {
        DES_A, DES_B
    }
    
    @Composable
    fun HiltActivitySampleNavHost(
        modifier: Modifier = Modifier,
        navController: NavHostController = rememberNavController(),
        startDestination: String = HiltSampleNavHostRoute.DES_A.name
    ) {
        NavHost(
            modifier = modifier,
            navController = navController,
            startDestination = startDestination
        ) {
    
            composable(HiltSampleNavHostRoute.DES_A.name) {
                DestinationScreenA()
            }
    
            composable(HiltSampleNavHostRoute.DES_B.name) {
                DestinationScreenB()
            }
        }
    }
    

    Screens

    // here you can use the Local Activity as the ViewModelStoreOwner
    @Composable
    fun DestinationScreenA(
        myViewModelParam: ActivityScopedViewModel = hiltViewModel(LocalActivity.current)
        // myViewModelParam: ActivityScopedViewModel = viewModel(LocalActivity.current)
    ) {
        Log.e("ActivityScopedViewModel", "Hashcode: ${myViewModelParam.hashCode()} : Composable Scope")
    }
    
    @Composable
    fun DestinationScreenB(
        modifier: Modifier = Modifier
    ) {}
    

    Or better yet, like from this answer by Phil Dukhov, you can use LocalViewModelStoreOwner as the parameter when you invoke the builder.

    Same NavHost

    @Composable
    fun HiltActivitySampleNavHost(
        ...
    ) {
    
        val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        }
    
        NavHost(
            modifier = modifier,
            navController = navController,
            startDestination = startDestination
        ) {
    
            composable(HiltSampleNavHostRoute.DES_A.name) {
                DestinationScreenA(
                    myViewModelParam = viewModel(viewModelStoreOwner)
                )
            }
    
            ...
        }
    }
    
    
    

    Both logs from the activity and the composable in the nav graph shows the same hashcode

    E/ActivityScopedViewModel: Hashcode: 267094635 : Activity Scope
    E/ActivityScopedViewModel: Hashcode: 267094635 : Composable Scope
    

    Also have a look at Thracian's answer. It has a very detailed explanation about ComponentActivity, and based from it I think my first proposed solution would probably work in your case.