Yarin Shitrit
Yarin Shitrit

Reputation: 327

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

Upvotes: 6

Views: 4976

Answers (1)

z.g.y
z.g.y

Reputation: 6197

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.

Upvotes: 8

Related Questions