Cannot create an instance of viewmodel while using Jetpack Compose navigation - android

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.

Related

How to inject ViewModel into Activity using Hilt?

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.

Why can't I launch viewModel() two times when I use Hilt as DI in an Android Studio project?

I use Hilt as DI in an Android Studio project, and viewModel() will create an instance of SoundViewModel automatically.
Code A works well.
I think viewModel() will create an Singleton of SoundViewModel.
I think mViewMode_A will be assigned to mViewMode_B automatically without creating a new instance in Code B.
I think both mViewMode_A and mViewMode_B will point the same instance in Code B.
But I don't know why I get Result B when I run Code B, could you tell me?
Result B
java.lang.RuntimeException: Cannot create an instance of class info.dodata.soundmeter.presentation.viewmodel.SoundViewModel
Code A
#Composable
fun NavGraph(
mViewModel_A: SoundViewModel = viewModel()
) {
ScreenHome(mViewMode_B = mViewMode1_A)
}
#Composable
fun ScreenHome(
mViewModel_B: SoundViewModel
) {
...
}
#HiltViewModel
class SoundViewModel #Inject constructor(
#ApplicationContext private val appContext: Context,
...
): ViewModel() {
...
}
Code B
#Composable
fun NavGraph(
mViewMode_A: SoundViewModel = viewModel()
) {
ScreenHome()
}
#Composable
fun ScreenHome(
mViewMode_B: SoundViewModel = viewModel() // I think mViewMode_A will be assigned to mViewMode_B automatically without creating a new instnace.
) {
...
}
//The same
You need to pass the key in ViewModel initialization
I'm not good at Composable but this will resolve your problem
For creating a new instance of ViewModel we need to set Key property
ViewModelProvider(requireActivity()).get(<UniqueKey>, SoundViewModel::class.java)
key – The key to use to identify the ViewModel.
val mViewMode_A = viewModel<SoundViewModel>(key = "NavGraph")
val mViewMode_B = viewModel<SoundViewModel>(key = "ScreenHome")
For Composable this link may help you separateViewmodel

How to pass arguments to ViewModel from Fragment and inject dependencies at the same time?

I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}

Inject viewModel to #Composable

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

Testing Navigation component: "does not have a NavController"

I'm implementing Espresso tests. I'm using a Fragment with a NavGraph scoped ViewModel. The problem is when I try to test the Fragment I got an IllegalStateException because the Fragment does not have a NavController set. How can I fix this problem?
class MyFragment : Fragment(), Injectable {
private val viewModel by navGraphViewModels<MyViewModel>(R.id.scoped_graph){
viewModelFactory
}
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
//Other stuff
}
Test class:
class FragmentTest {
class TestMyFragment: MyFragment(){
val navMock = mock<NavController>()
override fun getNavController(): NavController {
return navMock
}
}
#Mock
private lateinit var viewModel: MyViewModel
private lateinit var scenario: FragmentScenario<TestMyFragment>
#Before
fun prepareTest(){
MockitoAnnotations.initMocks(this)
scenario = launchFragmentInContainer<TestMyFragment>(themeResId = R.style.Theme_AppCompat){
TestMyFragment().apply {
viewModelFactory = ViewModelUtil.createFor(viewModel)
}
}
// My test
}
Exception I got:
java.lang.IllegalStateException: View android.widget.ScrollView does not have a NavController setjava.lang.IllegalStateException
As can be seen in docs, here's the suggested approach:
// Create a mock NavController
val mockNavController = mock(NavController::class.java)
scenario = launchFragmentInContainer<TestMyFragment>(themeResId = R.style.Theme_AppCompat) {
TestMyFragment().also { fragment ->
// In addition to returning a new instance of our Fragment,
// get a callback whenever the fragment’s view is created
// or destroyed so that we can set the mock NavController
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
// The fragment’s view has just been created
Navigation.setViewNavController(fragment.requireView(), mockNavController)
}
}
}
}
Thereafter you can perform verification on mocked mockNavController as such:
verify(mockNavController).navigate(SearchFragmentDirections.showRepo("foo", "bar"))
See architecture components sample for reference.
There exists another approach which is mentioned in docs as well:
// Create a graphical FragmentScenario for the TitleScreen
val titleScenario = launchFragmentInContainer<TitleScreen>()
// Set the NavController property on the fragment
titleScenario.onFragment { fragment ->
Navigation.setViewNavController(fragment.requireView(), mockNavController)
}
This approach won't work in case there happens an interaction with NavController up until onViewCreated() (included). Using this approach onFragment() would set mock NavController too late in the lifecycle, causing the findNavController() call to fail. As a unified approach which will work for all cases I'd suggest using first approach.
You are missing setting the NavController:
testFragmentScenario.onFragment {
Navigation.setViewNavController(it.requireView(), mockNavController)
}

Categories

Resources