5

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

z.g.y
  • 5,512
  • 4
  • 10
  • 36
Yarin Shitrit
  • 297
  • 4
  • 16
  • Check if this answer helps: https://stackoverflow.com/questions/74269746/jetpack-compose-using-same-state-value-for-all-screens/74277703#74277703 – Hamed Nov 27 '22 at 21:46
  • 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 functions – Yarin Shitrit Nov 27 '22 at 22:31
  • Have you tried doing some Log printing inside the `ViewModel's` `init{…}` block and verifying its not being called more than once? – z.g.y Nov 28 '22 at 08:05
  • 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 functions – Yarin Shitrit Nov 28 '22 at 08:16
  • 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.. – z.g.y Nov 28 '22 at 08:17
  • thats wierd.. isnt the `hiltViewModel()` supposed to be scoped to the "closest" `LifeCycleOwner` ? in this is is my activity – Yarin Shitrit Nov 28 '22 at 08:19
  • 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.. – Yarin Shitrit Nov 28 '22 at 08:22
  • 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.viewModel` – z.g.y Nov 28 '22 at 08:22
  • 1
    Ok I found something, both `viewModel()` and `hiltViewModel()` seems to work only when I pass in the activity as the `ViewModelStoreOwner` like `hiltViewModel(this)` – Yarin Shitrit Nov 28 '22 at 08:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249943/discussion-between-yarin-shitrit-and-z-y). – Yarin Shitrit Nov 28 '22 at 08:36

1 Answers1

3

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.

z.g.y
  • 5,512
  • 4
  • 10
  • 36