Share viewmodel from activity to compose function using hilt
S

1

6

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

Seafaring answered 27/11, 2022 at 21:37 Comment(10)
Check if this answer helps: #74270246Duro
It is not really what I am looking for, in my case the viewModel inside the activity is not the same instance inside the composable functionsSeafaring
Have you tried doing some Log printing inside the ViewModel's init{…} block and verifying its not being called more than once?Mong
It is for sure being called more than once, I logged both hashCodes of the viewModel inside the activity and the viewModel inside the composable functions and I saw that they are different.. So for sure the initialization of the viewModel with by viewModels() inside the activity provides a different instance than the hiltViewModel() inside the composable functionsSeafaring
Interesting.. I'm actually trying to re-produce your posted code, but its weird I'm having the same instance of the ViewModel, I'm trying to make a different instance of the ViewModel but I keep getting the same one.. Ill try to get back to this when I'm able to re-produce it..Mong
thats wierd.. isnt the hiltViewModel() supposed to be scoped to the "closest" LifeCycleOwner ? in this is is my activitySeafaring
I am trying to think of a work around for calling the viewModel somehow inside the activity, as for now I am calling it from the onLoadFinished callback which is not composable so I cannot use hiltViewModel() there..Seafaring
just want to ask if you can try, would you mind using the composable viewModel() factory instead of hiltViewModel()? just to check if it would make any difference, -androidx.lifecycle.viewmodel.compose.viewModelMong
Ok I found something, both viewModel() and hiltViewModel() seems to work only when I pass in the activity as the ViewModelStoreOwner like hiltViewModel(this)Seafaring
Let us continue this discussion in chat.Seafaring
M
8

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.

Mong answered 28/11, 2022 at 8:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.