I have this on my NavHost:
composable(
ScreenRoutes.AstronautDetailsScreen.route,
arguments = listOf(navArgument("astronautId") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let {
AstronautDetailScreen(astronautDetailsViewModel)
}
}
I want the viewModel to receive the astronautId so it can communicate with the useCase and send a GET request with this dinamic id.
In the viewModel I've got this:
#HiltViewModel
class AstronautDetailsViewModel #Inject constructor(
astronautDetailsUseCase: AstronautDetailsUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val argument = checkNotNull(savedStateHandle.get<Int>("astronautId"))
init {
viewModelScope.launch {
_astronautDetails.value = astronautDetailsUseCase(argument)
}
}
The screen that I go to is this:
#Composable
fun AstronautDetailScreen(astronautDetailsViewModel: AstronautDetailsViewModel) {
val astronautDetails by astronautDetailsViewModel.astronautDetails.observeAsState(AstronautDetails())
But it always says that argument is null and crashes.
Can someone tell me please what could I'm be doing incorrectly?
In your NavHost, you are passing viewModel object to AstronautDetailScreen instead of injecting it by hiltViewModel().
You can change your composable function to inject the viewModel by hilt. In that way, your arguments will be available in SavedStateHandle of your view model class. You can change your composable function like below:
AstronautDetailScreen(
astronautDetailsViewModel: AstronautDetailsViewModel = hiltViewModel()
) {
// Your rest of the code here
}
Or, if you want to pass your view model instance from NavHost instead of default value of the composable function, Then you can modify your NavHost part like below:
composable(
ScreenRoutes.AstronautDetailsScreen.route,
arguments = listOf(navArgument("astronautId") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let {
val astronautDetailsViewModel: AstronautDetailsViewModel = hiltViewModel()
AstronautDetailScreen(astronautDetailsViewModel)
}
}
Related
I have a ViewModel that I'm already injecting into a Composable. Now I want to inject the same instance of that ViewModel into my Activity. For example:
In AccountScreen.kt
#Composable
fun AccountScreen(accountViewModel: AccountViewModel = hiltViewModel()) {
...
}
and my Activity class:
class MainActivity : ComponentActivity() {
#Inject
lateinit var accountViewModel: AccountViewModel
}
should have the same instance of AccountViewModel.
I know using #Inject in the Activity as in the example above doesn't work. Hilt's documentation suggests using ViewModelProvider or by viewModels() instead, both of which give me a new instance of AccountViewModel, but I need the same instance as what's in the AccountScreen Composable.
I'm assuming AccountScreen is part of a NavGraph, since you mentioned you need same instance of the view model, you can consider specifying the ViewModelStoreOwner when you inject your ViewModel in your AccountScreen, so MainActivity and AccountScreen will share same instance of it.
#Composable
fun MyNavHost(
...
) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(<Destination>) {
AccountScreen(accountViewModel: AccountViewModel = hiltViewModel(viewModelStoreOwner)) {
...
}
}
...
}
}
I ended up solving this by getting the parent Activity's ViewModel in my child Composable (AccountScreen in this case) like so:
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
Within my MainActivity I'm getting the ViewModel the standard way
private val accountViewModel: AccountViewModel by viewModels()
Thanks to #z.g.y for providing a helpful suggestion that led me to this solution.
I would like to share a viewmodel between many composables. Just like how we share a viewmodel between fragments within an Activity.
But when I try this
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
navigation(startDestination = "username", route = "login") {
// FIXME: I get an error here
val viewModel: LoginViewModel = viewModel()
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
}
I get an error
#Composable invocations can only happen from the context of a #Composable function
Need
The viewmodel should only be active in the NavGraph Scope.
When I go to a different route and come back I should initialize a new viewmodel (this is why I'm calling it in the NavGraph)
Almost similar solution
Answer by Philip Dukhov for the question How to share a viewmodel between two or more Jetpack composables inside a Compose NavGraph?
But in this approach the viewmodel stays in the scope of the activity that launched it and so is never garbage collected.
Solution 1
(copied from the docs)
The Navigation back stack stores a NavBackStackEntry not only for each individual destination, but also for each parent navigation graph that contains the individual destination. This allows you to retrieve a NavBackStackEntry that is scoped to a navigation graph. A navigation graph-scoped NavBackStackEntry provides a way to create a ViewModel that's scoped to a navigation graph, enabling you to share UI-related data between the graph's destinations. Any ViewModel objects created in this way live until the associated NavHost and its ViewModelStore are cleared or until the navigation graph is popped from the back stack.
This means we can use the NavBackStackEntry to get the scope of the navigation graph we are in and use that as the ViewModelStoreOwner to get the viewmodel for that scope.
Add this in every composable to get the BackStackEntry for login and then use that as the ViewModelStoreOwner to get the viewmodel.
val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
So the final code changes to
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
navigation(startDestination = "username", route = "login") {
composable("username") {
val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
...
}
composable("password") {
val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
...
}
composable("registration") {
val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
...
}
}
}
}
Solution 2
Copied from ianhanniballake answer
This can also be achieved using an extension
Get the current scope and get or create the viewmodel for that scope
#Composable
fun <reified VM : ViewModel> NavBackStackEntry.parentViewModel(
navController: NavController
): VM {
// First, get the parent of the current destination
// This always exists since every destination in your graph has a parent
val parentId = destination.parent!!.id
// Now get the NavBackStackEntry associated with the parent
val parentBackStackEntry = navController.getBackStackEntry(parentId)
// And since we can't use viewModel(), we use ViewModelProvider directly
// to get the ViewModel instance, using the lifecycle-viewmodel-ktx extension
return ViewModelProvider(parentBackStackEntry).get()
}
Then simply use this extension inside your navigation graph
navigate(secondNestedRoute, startDestination = nestedStartRoute) {
composable(route) {
val loginViewModel: LoginViewModel = it.parentViewModel(navController)
}
}
I have viewModel for my ProfileScreen.
#Composable
fun ProfileScreen() {
val viewModel: ProfileViewModel = viewModel()
...
}
Every time when I call ProfileScreen, new viewModel is created. How can I created only one viewModel instance for my ProfileScreen. I tried to inject viewModel following https://insert-koin.io/docs/reference/koin-android/compose/ but when I try
val viewModel: ProfileViewModel = viewModel()
Android Studio throws error.
Or use remember() for save instance ViewModel between recompose calls
#Composable
fun ProfileScreen() {
val viewModel = remember { ProfileViewModel() }
...
}
Also, rememberSaveable allows saving state(aka data class) between recreating of activity
Your viewModel gets destroyed whenever you destroy the composable, it can survive re-compositions but as soon as your composable gets destroyed it will be destroyed.
What you can do is create the viewModel in a scope that lives longer than the ProfileScreen composable and then pass the viewModel as parameter to it.
Something like this should work.
#Composable
fun MainScreen() {
val vModel : ProfileViewModel = viewModel()
....
ProfileScreen(vModel)
}
If u want to use Koin to inject your view model to composable you should follow what is described in the docs.
getViewModel() - fetch instance
By calling that method Koin will search for that view model and will provide with an instance.
Here is an example of injecting view model in my app.
fun ManualControlScreen(
onDrawerClick: () -> Unit,
viewModel: ManualControlViewModel = getViewModel<ManualControlViewModel>()
) {
// Your composable UI
}
Try this:
#Composable
fun HomeScreen(viewModel: PokemonViewModel = koinViewModel()) {
}
build.gradle(:app)
def koin_version = '3.3.2'
implementation "io.insert-koin:koin-core:$koin_version"
implementation "io.insert-koin:koin-android:$koin_version"
implementation 'io.insert-koin:koin-androidx-compose:3.4.1'
SOURCE
I am using navigation component for jetpack compose in my app like this:
#Composable
fun FoodiumNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Main.route,
) {
composable(Screen.Main.route) {
MainScreen(navController)
}
...
}
}
And I am getting viewmodel in my MainScreen composable like this:
#Composable
fun MainScreen(navController: NavController) {
val mainViewModel: MainViewModel = viewModel()
...
}
which is giving me a runtime exception as Cannot create an instance of class com.package.main.MainViewModel.
Here, I am stating that this only happens while using navigation component, i.e. everything was working fine and mainViewModel was successfully instantiated before using navigation component in my app.
The MainViewModel is like this:
#ExperimentalCoroutinesApi
#HiltViewModel
class MainViewModel #Inject constructor(private val postRepository: PostRepository) :
ViewModel() {
private val _postsLiveDataState = MutableLiveData<UiState<List<Post>>>()
val postLiveState: LiveData<UiState<List<Post>>> = _postsLiveDataState
init {
getPostsState()
}
private fun getPostsState() {
viewModelScope.launch {
postRepository.getAllPosts()
.onStart { _postsLiveDataState.value = UiState(loading = true) }
.map { resource -> UiState.fromResource(resource) }
.collect { state -> _postsLiveDataState.value = state }
}
}
}
If your #HiltViewModel is scoped to the navigation graph use hiltNavGraphViewModel() instead of viewModel() to initialize. For more reference android documentaion
Update
hiltNavGraphViewModel() is now deprecated, use hiltViewModel() instead
Thanks to Narek Hayrapetyan for the reminder
hiltNavGraphViewModel is deprecated, should be used hiltViewModel() instead
also add dependency androidx.hilt:hilt-navigation-compose:1.0.0-alpha03
You should add this
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
then you can use this code for create instance of your viewmodel
val viewModel: YourViewModelClass= hiltViewModel()
You can use viewModel() as well, but check that owning Activity or Fragment has been annotated with #AndroidEntryPoint.
This video (MVVM & Nested Fragments/Views: ViewModel Contracts - By Marcos Paulo Damesceno, Bret Erickson
droidcon San Francisco 2019
) shows a way to deal with communication between activities/fragments using ViewModel.
I am implementing it for learning purpose but I got stuck.
// 18:35 of the video
private const val VM_KEY = "view_model_contract_key"
fun <T> Fragment.viewModelContracts() = lazy {
val clazz: Class<ViewModel> = arguments?.getSerializable(VM_KEY) as Class<ViewModel>
val viewModelProvider = ViewModelProvider(requireActivity())
return#lazy viewModelProvider.get(clazz) as T
}
The ViewModelStoreOwner passed as parameter is an Activity, but if I have a Fragment inside another Fragment where both of them share the same ViewModel, the ViewModel returned by viewModelContracts() will be a different object as the one created by the Parent Fragment.
interface ChildViewModelContract {
// ...
}
class SomeViewModel : ViewModel(), ChildViewModelContract {
// ...
}
class ParentFragment: Fragment {
private val viewModel: SomeViewModel by viewModels()
// ...
}
class ChildFragment: Fragment {
private val viewModelContract: ChildViewModelContract by viewModelContracts()
// ...
}
The ideal solution would be to check in fun <T> Fragment.viewModelContracts() if the ViewModelProvider of the parent fragment has the ViewModel stored in it, and if not, use the ViewModelProvider of the Activity. But I'm not knowing how to do this.
fun <T> Fragment.viewModelContracts() = lazy {
val clazz: Class<ViewModel> = arguments?.getSerializable(VM_KEY) as Class<ViewModel>
val parentFragment = parentFragment
if (parentFragment != null) {
val viewModelProvider = ViewModelProvider(parentFragment)
// is there any way to do something like this?
if (viewModelProvider.isViewModelStored(clazz)) {
return#lazy viewModelProvider.get(clazz) as T
}
}
val viewModelProvider = ViewModelProvider(requireActivity())
return#lazy viewModelProvider.get(clazz) as T
}