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/
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
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
I have found a similar question here. At the time of writing this question there is only this answer avaliable, which does not provide any help to me, and I believe also to the person who asked the question.
I checked the repo which is linked in the answer and it "solves" the problem by creating an init method in the viewmodel and calling it in the Activity/Fragment.
Since the viewmodel has already been injected, this solution does not seem like ideal to me, and I would like to know if there are other options available when using hilt.
As per this comment and the release of AndroidX Hilt 1.0.0-alpha03, Hilt has supported ViewModels that take a SavedStateHandle as a parameter (right alongside your other injected parameters).
This SavedStateHandle is automatically, without you doing anything, populated with the arguments passed to your fragment (i.e., the same arguments you get from requireArguments() and the same arguments that are read by Safe Args).
Therefore in your ViewModel's constructor, you can immediately access those arguments from the SavedStateHandle, without having to do any manual passing of arguments to your ViewModel.
#HiltViewModel
class MainViewModel #Inject constructor(
val userDataManager: UserDataManager,
savedStateHandle: SavedStateHandle
) : ViewModel() {
init {
// Use the same argName as in your navigation graph
val yourArgument: String = savedStateHandle["argName"]
// Now use that argument to load your data, etc.
}
}
The feature request for Safe Args integration with SavedStateHandle is already fixed and will be part of the upcoming Navigation 2.4.0-alpha01 release. Once that is released, you'd be able to do something like MainFragmentArgs.fromSavedStateHandle(savedStateHandle) to get the same Args class you're currently able to get from by navArgs() within your ViewModel.
For anyone facing the same challenge and using Hilt, the trick is to initialize the SavedStateHandle in the viewModel constructor i.e.
savedStateHandle: SavedStateHandle = SaveStateHandle()
Then you can access the passed argument like:
val id: String? = savedStateHandle["id"]
or
val id = savedStateHandle.get<String?>("id")
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.
My software specifications are as follows:
Android Studio 3.4
dagger-android 2.16
I have the following class that passes a MapboxGeocoder that will execute and return a response.
class GeocodingImp(private val mapboxGeocoder: MapboxGeocoder) : Geocoding {
override fun getCoordinates(address: String, criteria: String): AddressCoordinate {
val response = mapboxGeocoder.execute()
return if(response.isSuccess && !response.body().features.isEmpty()) {
AddressCoordinate(
response.body().features[0].latitude,
response.body().features[0].longitude)
}
else {
AddressCoordinate(0.0, 0.0)
}
}
}
However, the MapboxGeocoder is generated in a dagger module at compile time. So I have to specify the string for the address and TYPE_ADDRESS.
#Reusable
#Named("address")
#Provides
fun provideAddress(): String = "the address to get coordinates from"
#Reusable
#Provides
#Named("geocoder_criteria")
fun provideGeocoderCriteria(): String = GeocoderCriteria.TYPE_ADDRESS
#Reusable
#Provides
fun provideMapboxGeocoder(#Named("address") address: String, #Named("geocoder_criteria") geocoderCriteria: String): MapboxGeocoder =
MapboxGeocoder.Builder()
.setAccessToken("api token")
.setLocation(address)
.setType(geocoderCriteria)
.build()
#Reusable
#Provides
fun provideGeocoding(mapboxGeocoder: MapboxGeocoder): Geocoding =
GeocodingImp(mapboxGeocoder)
my component class:
interface TMDispatchMobileUIComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: TMDispatchMobileUIApplication): Builder
fun build(): TMDispatchMobileUIComponent
}
fun inject(application: TMDispatchMobileUIApplication)
}
In the main activity I would use this like this as the user can enter in a different address or change the criteria to something else. But as the module are compiled I cannot pass any parameters to them at runtime:
presenter.getAddressCoordinates("this should be the actual address", GeocoderCriteria.TYPE_ADDRESS)
For my injection into the Activity I use the following:
AndroidInjection.inject(this)
Is there any solution to this problem?
The problem you have can be solved using "Assisted injection" approach.
It means that you need a class to be built both using dependencies provided from the existing scopes and some dependencies from the instance's creator, in this case, your main activity. Guice from Google has a nice description of what it is and why it is needed
Unfortunately, Dagger 2 does not have this feature out from the box. However, Jake Wharton is working on a separate library that can be attached to Dagger. Moreover, you can find more details in his talk on Droidcon London 2018, where he dedicated a whole talk section for this question:
https://jakewharton.com/helping-dagger-help-you/
You can recreate your whole component at runtime if you wish, where you'd then pass in the parameters to your module as a constructor parameter. Something like:
fun changeAddress(address: String) {
val component = DaggerAppComponent.builder() //Assign this to wherever we want to keep a handle on the component
.geoModule(GeoModule(address))
.build()
component.inject(this) //To reinject dependencies
}
And your module would look like:
#Module
class AppModule(private val address: String) {...}
This method may be wasteful though, if you're creating many different objects in your component.
A different approach compared to the already given answers would be to get a "Factory" via dagger dependency injection called GeoModelFactory which can create new instances of GeoModel for you.
You can pass the address and type to the factory which creates your instance. For optimization you can either store references for all different address/types that have already been requested (can result in a memory leak if there are a lot of different ones if old ones are not removed) or it could also be enough if you store only the latest instance and in other parts of the code to simply ask the factory to provide you with the GeoModel that has been created last.
The MapboxGeocoder are dynamically constructed at runtime, in this case, dagger doesn't help much as its objective is to help you construct the object graph at compile time like you hand write the code.
So in my opinion, you should create a MapboxGeocoder inside getCoordinates().