Maybe I'm blind but I can't find anything about injecting a dependency that needs parameters in side a composable using dagger hilt.
Lets say my ViewModel looks something like this:
class MyViewModel #AssistedInject constructor(#Assisted myValue: Int) : ViewModel() {
...
}
and I've got a factory interface like this:
#AssistedFactory
interface MyViewModelAssistedFactory {
fun create(myValue: Int): MyViewModel
}
how can I inject that dependency with a certain value as parameter?
All answers I found where like:
#Inject
var myViewModelFactory: MyViewModelAssistedFactory;
and
val initValue = 4
fun onCreate(){
val viewModel = myViewModelFactory.create(initValue)
}
but that doesn't work inside a composable fun.
Not sure if still relevant but if you use the navigation component you can just call hiltViewModel() in the navgraph builder.
Example:
https://github.com/pablichjenkov/ComposeStudy/blob/04298ca8393d3eea0f5b7883fb223161ef79a962/app/src/main/java/com/pablichj/study/compose/home/HomeNavigation.kt#L24
If not using Jetpack Navigation then the solution is a bit more complex. You will need to create a State tree in your App where they implement LifecycleOwner and ViewModelStoreOwner, in order to be able to install the ViewModel appropriately. The good news is that there is work out there already doing so, check this:
https://github.com/Syer10/voyager
Related
This is the situation:
I'm using Compose, Hilt, Navigation and ViewModel. I'm trying to get an instance of my ViewModel within a Composable Screen via Hilt:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
homeViewModel: HomeViewModel = viewModel()
) {
...
}
#HiltViewModel
class HomeViewModel #Inject constructor(
private val updateCaptureUseCase: UpdateCaptureUseCase
) : ViewModel() {
...
}
class UpdateCaptureUseCase #Inject constructor(private val captureRepository: CaptureRepository) {
...
}
I get an instance of CaptureRepository by defining it inside a Module:
#Module
#InstallIn(ViewModelComponent::class)
abstract class CaptureModule {
#Binds
abstract fun bindCaptureLocalDataSource(
captureLocalDataSourceImpl: CaptureLocalDataSourceImpl
): CaptureLocalDataSource
#Binds
abstract fun bindCaptureRepository(
captureRepositoryImpl: CaptureRepositoryImpl
): CaptureRepository
}
The problem is that CaptureModule appears in Android Studio as if it had no usages.
I can build and run the app with no problems, but when it is supposed to show HomeScreen it crashes. What stresses me out and makes it hard to figure out a solution is that there are no errors in the Run tab nor the Logcat.
If I remove updateCaptureUseCase from the constructor of HomeViewModel, then the app works correctly and is able to reach HomeScreen without errors. Since updateCaptureUseCase depends on CaptureRepository and it is being defined in CaptureModule, but this Module shows no usages, I suspect the error comes from Hilt and ViewModel
I think when ViewModel gets initialized hilt checks the dependency graph/tree, and since it has a parameter that also needs a dependency which is the CaptureRepository , hilt also looks for it, but because your'e using #Bind, afaik, those dependencies should also define #Inject annotation.
I was able to reproduce your issue and manage to fix it by, specifying inject to your repository impl
class CaptureRepositoryImpl #Inject constructor(): CaptureRepository
another work around is having your DI module a companion object and define how hilt will provide the dependency without the need to specify #Inject in your repository impl.
#Module
#InstallIn(ViewModelComponent::class)
abstract class CaptureModule {
...
companion object {
#Provides
fun provideHomePresenter(): CaptureRepository {
return CaptureRepositoryImpl()
}
}
}
After many hours I found out a solution: I had to use #AndroidEntryPoint annotation in my Activity.
The problem is that since I'm fairly new with Compose, Hilt and Navigation I had no idea what structure I should use: I wanted to use a single Activity and instead of using Fragments for navigation I desired to use Composables.
Android Docs for Navigation provide examples about the structure I wanted; I had set up everything the same, but the only thing that was missing was that annotation. I though it was not needed since I didn't require to inject dependencies directly into the Activity, but in the end this was the root of the bug, a difficult one because the app crashed without showing a single error
just it's me trying to implement MVVM architecture with Kotlin. I need to use ViewModelProvider to init my instance and I'm getting this error...
Any advice?
You have a no of options to to instantiate viewmodels.
Now You can now use the ViewModelProvider constructor directly.
private val assignmentViewModel= ViewModelProvider(this).get(AssignmentViewModel::class.java)
If you use androidx.activity:activity-ktx:$Version in the library you can do this directly
private val assignmentViewModel: AssignmentViewModel by viewModels()
If you're using a viewModelFactory
private val assignmentViewModel= ViewModelProvider(this, viewModelFactory).get(AssignmentViewModel ::class.java)
and private val assignmentViewModel: AssignmentViewModel by viewModels { viewModelFactory } respectively
If you're using Kotlin the easiest way to incorporate a ViewModel in your code is via a property delegate:
Activity/Fragment:
val assignmentViewModel: AssignmentViewModel by viewModels()
Fragment:
val assignmentViewModel: AssignmentViewModel by activityViewModels()
The latter allows you to share data between fragments, since activityViewModels scopes it to the Activity.
See ViewModel Overview for more information.
I have a ViewModel that takes a string as an argument
class ComplimentIdeasViewModel(ideaCategory : String) : ViewModel() {
//some code here
}
What is the best way to initiate this ViewModel inside a composable fun without using a ViewModel factory and Hilt? A simple statement seems to achieve this inside a composable fun
#Composable
fun SampleComposableFun() {
val compIdeasViewModel = remember { ComplimentIdeasViewModel("someCategory") }
}
There is no warning in Android studio when I try to do this, but this seems too easy to be true, I am able to do this without Dependency Injection and with a ViewModelFactory class. Am I missing something here?
I've tried how you have written yours out and I had issues with screen rotation resetting the view model. I suspect you may too.
I was able to fix it by utilizing the the factory parameter on viewModel() for this, which worked well for me. See this answer on a similar question with example on how to use it: jetpack compose pass parameter to viewModel
This will not provide you the correct instance if viewmodel. See if you store some state in the viewmodel, then using the factory to initialise it is necessary to ensure that you get the same and latest copy of the viewmodel currently present. There is no error since the syntactic implementation is correct. I do not know of any way to do this because most of the times, you don't need to. Why don't you initialise it in the top-level container, like the activity? Then pass it down wherever necessary.
Create a CompositionLocal for your ViewModel.
val YourViewModel = compositionLocalOf { YourViewModel(...) }
Then initialise it (You'd likely use the ViewModelProvider.Factory here). And then provide that to your app.
CompositionLocalProvider(
YourViewModel provides yourInitialisedViewModel,
) {
YourApp()
}
Then reference it in the composable.
#Composable
fun SampleComposableFun(
compIdeasViewModel = YourViewModel.current
) {
...
}
Note, the docs say that ViewModels are not a good fit for CompositionLocals because they will make your composable harder to test, make your composables tied to this app and make it harder to use #Preview.
Some get pretty angry about this. However, if you manage to mock out the ViewModel, so you can test the app and use #Preview and your composables are tied to the app and not generic, then I see no problem.
You can mock a ViewModel fairly simply, providing its dependencies are included as parameters (which is good practice anyway).
open class MockedViewModel : MyViewModel(
app = Application(),
someOtherDependeny = MockedDependecy(),
)
The more dependencies your ViewModel has the more mocking you'll need to do. But I've not found it prohibitive and including the ViewModel as a default parameter has massively sped up development.
I used ViewModelProvider(this).get(myDataIdentifier, MyViewModel::class.java) to get the same viewmodel for each identifier.
Now I want to use Koin for dependency injection but I can't figure out how to get this working.
I can inject data via val viewModel by viewModel() but where am I able to make sure to get the same instance, identified by myDataIdentifier? I can't wrap my head around qualifier, parameter,....
Sorry, maybe this is a dumb question and i just overlooked something.
Try with named components, name your viewmodel (this may now need to be a singleton?)
val myModule = module {
viewModel(named("myViewModel")) { MyViewModel() }
}
...
val viewModel: MyViewModel by viewModel(named("myViewModel"))
https://engineering.bigshyft.com/koin-2-0-for-android/
I have single activity application and number of fragments. Some of these fragments are using my viewmodel, typically like this:
private val myViewModel: MyViewModel by sharedViewModel()
What if I want to have the model both shared and keep its state with SavedStateHandle? I cannot figure out if this is supported and if so, how it needs to be used (declaring viewmodel as stateViewModel in hosting activity is not working).
Update: as koin 2.1.6 is around, they introduced org.koin.androidx.viewmodel.ext.android.stateSharedViewModel that you can use in your fragments.
Ok after an hour of digging Koin samples and figuring out a few gotchas:
Assuming your view model is something similar to this:
class SavedStateViewModel(val handle: SavedStateHandle, val service: SimpleService)
...and your DI looks like this:
viewModel { (handle: SavedStateHandle) -> SavedStateViewModel(handle, get()) }
Your shared state view model can be consumed in your fragments like this:
val sharedSaved: SavedStateViewModel by sharedViewModel()
(important!) You need this declaration in your activity:
lateinit var savedVm: SavedStateViewModel
(important) You need to call this right after super.onCreate(savedInstanceState) in your activity:
savedVm = getStateViewModel()
It is important not to use lazy version for the above (stateViewModel).