Reputation: 327
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
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