How to inject viewmodel factory by dagger 2 - android

Trying to create a view model in a fragment by providing the factory with a dagger but app crashes with error. Move injection to another lifecycle doesn't work
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bigproject.planyourlife, PID: 13934
kotlin.UninitializedPropertyAccessException: lateinit property factory has not been initialized
at com.bigproject.planyourlife.view.SignupFragment.getFactory(SignupFragment.kt:27)
ViewModel
class SignupViewModel(private val service: AuthService) : ViewModel() {
private var email: String = ""
private var password: String = ""
private val _uiState = MutableStateFlow<SignupState>(SignupState.Success(null))
val uiState: StateFlow<SignupState> = _uiState
#Suppress("UNCHECKED_CAST")
class Factory #Inject constructor(
private val service: AuthService
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
require(modelClass == SignupViewModel::class)
return SignupViewModel(service) as T
}
}
Fragment
class SignupFragment : Fragment(R.layout.signup_page) {
private val binding by viewBinding(SignupPageBinding::bind)
private val signupViewModel: SignupViewModel by viewModels {
factory
}
#Inject
lateinit var factory: SignupViewModel.Factory
override fun onAttach(context: Context) {
context.appComponent.inject(this)
super.onAttach(context)
}
}

First, create a module for your ViewModelFactory. This is important for you to provide ViewModelFactory for dagger.
ViewModelModule.kt
#Module
abstract class ViewModelModule {
#Binds
abstract fun provideViewModelFactory(factory: Factory): ViewModelProvider.Factory
// Binds your viewmodel in here.
}
Next, you need to include that module in your component. Specifically here is AppComponent.
AppComponent.kt
#Singleton
#Component(modules = [ViewModelModule::class, YourFragmentModule::class, YourActivityModule::class, ...])
interface AppComponent {
#Component.Factory
abstract class Factory : AndroidInjector.Factory<YourApplication>
}
And now, viewModelFactory is ready for you to inject into your Fragment, Activity, ....
YourFragment.kt
class YourFragment: Fragment() {
#Inject
lateinit var viewModelFactory: Factory
}

Related

Hilt - How to inject ViewModel interface?

Based on the Hilt tutorial, ViewModels needs to be inject the following way:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
However, in my case, I want to use an interface:
interface ExampleViewModel()
#HiltViewModel
class ExampleViewModelImp #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ExampleViewModel, ViewModel() {
...
}
Then I want to inject it via the interface
#AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
How to make this work?
viewModels requires child of ViewModel class
val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()
Had a similar problem where I wanted to Inject the ViewModel via interface, primarily because to switch it with a fake implementation while testing. We are migrating from Dagger Android to Hilt, and we had UI tests that used fake view models. Adding my findings here so that it could help someone whose facing a similar problem.
Both by viewModels() and ViewModelProviders.of(...) expects a type that extends ViewModel(). So interface won't be possible, but we can still use an abstract class that extends ViewModel()
I don't think there is a way to use #HiltViewModel for this purpose, since there was no way to switch the implementation.
So instead, try to inject the ViewModelFactory in the Fragment. You can switch the factory during testing and thereby switch the ViewModel.
#AndroidEntryPoint
class ListFragment : Fragment() {
#ListFragmentQualifier
#Inject
lateinit var factory: AbstractSavedStateViewModelFactory
private val viewModel: ListViewModel by viewModels(
factoryProducer = { factory }
)
}
abstract class ListViewModel : ViewModel() {
abstract fun load()
abstract val title: LiveData<String>
}
class ListViewModelImpl(
private val savedStateHandle: SavedStateHandle
) : ListViewModel() {
override val title: MutableLiveData<String> = MutableLiveData()
override fun load() {
title.value = "Actual Implementation"
}
}
class ListViewModelFactory(
owner: SavedStateRegistryOwner,
args: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, args) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return ListViewModelImpl(handle) as T
}
}
#Module
#InstallIn(FragmentComponent::class)
object ListDI {
#ListFragmentQualifier
#Provides
fun provideFactory(fragment: Fragment): AbstractSavedStateViewModelFactory {
return ListViewModelFactory(fragment, fragment.arguments)
}
}
#Qualifier
annotation class ListFragmentQualifier
Here, ListViewModel is the abstract class and ListViewModelImpl is the actual implementation. You can switch the ListDI module while testing using TestInstallIn. For more information on this, and a working project refer to this article
Found a solution using HiltViewModel as a proxy to the actual class I wish to inject. It is simple and works like a charm ;)
Module
#Module
#InstallIn(ViewModelComponent::class)
object MyClassModule{
#Provides
fun provideMyClas(): MyClass = MyClassImp()
}
class MyClassImp : MyClass {
// your magic goes here
}
Fragment
#HiltViewModel
class Proxy #Inject constructor(val ref: MyClass) : ViewModel()
#AndroidEntryPoint
class MyFragment : Fragment() {
private val myClass by lazy {
val viewModel by viewModels<Proxy>()
viewModel.ref
}
}
Now you got myClass of the type MyClass interface bounded to viewModels<Proxy>() lifeCycle
It's so simple to inject an interface, you pass an interface but the injection injects an Impl.
#InstallIn(ViewModelComponent::class)
#Module
class DIModule {
#Provides
fun providesRepository(): YourRepository = YourRepositoryImpl()
}

How to use AssistedInject to pass dynamic value as a parameter to ViewModel when using Dagger2 in Android

I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}

Viewmodel not retaining values after changing activities using dagger dependency injection

I'm trying to share a ViewModel between two different activities. The first activity is LoginActivity, there I make a request to an api using the users username and password to get a user object. Using live data that user is then changed in the ViewModel. To test I also have a variable that is a string and I change it once i make the API call.
My ViewModel:
class MainViewModel #Inject constructor() : ViewModel(){
var _request = MutableLiveData<LoginRequest>()
val userLogin: LiveData<LoggedInUser> = Transformations
.switchMap(_request){req ->
MainRepository.authenticatePlease(req.username, req.password)
}
var rando = "rando rando"
fun setUser(username: String, password: String){
val update = LoginRequest(username, password)
rando = "changed"
if(_request.value == update){
return
}
_request.value = update
}
}
As you can see the value of the variable rando is changed from rando rando to changed
In my first activity looks like this:
class LoginActivity : DaggerAppCompatActivity() {
#Inject lateinit var modelFactory: ViewModelProvider.Factory
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// AndroidInjection.inject(this)
val button: Button = findViewById(R.id.button)
button.setOnClickListener { onButtonClick() }
val label: TextView = findViewById(R.id.counter)
viewModel = ViewModelProvider(this, modelFactory).get(MainViewModel::class.java)
println("FACTORY: ${modelFactory.hashCode()}")
println("MODEL: ${viewModel.hashCode()}")
viewModel.userLogin.observe(this, Observer{ user ->
println("Debug LOGIN: ${user}")
println("RANDOM: ${viewModel.rando}")
if(user != null){
label.text = user.user.name
redirectToLogin()
}
})
}
fun onButtonClick(){
var username: EditText = findViewById(R.id.username)
var password: EditText = findViewById(R.id.password)
println(username.text.toString())
println(password.text.toString())
viewModel.setUser(username.text.toString(), password.text.toString())
}
fun redirectToLogin(){
println("Login done!")
val intent = Intent(this, SampleActivity::class.java)
intent.putExtra("extra", viewModel.userLogin.value)
startActivity(intent)
}
}
The second activity looks like this:
class SampleActivity : DaggerAppCompatActivity() {
lateinit var viewModel: MainViewModel
#Inject lateinit var modelFactory: ViewModelProvider.Factory
override fun onCreate(savedInstanceState: Bundle?) {
// AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sample)
// val loggedInUser: LoggedInUser? = intent.getParcelableExtra("extra")
// println("extra: $loggedInUser")
viewModel = ViewModelProvider(this, modelFactory).get(MainViewModel::class.java)
println("FACTORY: ${modelFactory.hashCode()}")
println("MODEL: ${viewModel.hashCode()}")
println(" $ WORKS: ${viewModel.userLogin.value.toString()}")
println(" $ WORKS: ${viewModel.userLogin.value}")
println(" $ WORKS: ${viewModel.rando}")
}
}
One thing to note is that the hashcode for my ViewModelFactory is the same in both activities, but the hashcodes for the ViewModels are different. Moreover, the variable rando in the ViewModel is rando rando in the second activity as opposed to changed
Thanks!
EDIT
Here is my ViewModelFactory:
#Singleton
class ViewModelProviderFactory #Inject constructor(
private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass]
?: throw IllegalArgumentException("Unknown model class $modelClass")
return creator.get() as T
}
AppComponent.kt:
#Singleton
#Component(
modules = [
AppModule::class,
AndroidInjectionModule::class,
ViewModelModule::class,
ActivityModule::class
]
)
interface AppComponent {
fun inject(app: MainApplication)
#Component.Factory
interface Factory {
fun create(#BindsInstance context: Context): AppComponent
}
}
ActivityModule.kt:
#Module
abstract class ActivityModule {
#ContributesAndroidInjector
abstract fun contributeLoginActivity(): LoginActivity
#ContributesAndroidInjector
abstract fun contributeSampleActivity(): SampleActivity
}
AppModule.kt:
#Module(includes = [ViewModelModule::class])
class AppModule {
#Provides
#Named("mainViewModel")
fun provideMainViewModel(): MainViewModel =
MainViewModel()
}
and ViewModelModule.kt:
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(MainViewModel::class) // PROVIDE YOUR OWN MODELS HERE
internal abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
#Binds
internal abstract fun bindViewModelProviderFactory(factory: ViewModelProviderFactory): ViewModelProvider.Factory
}
one thing to notice is that in ViewModelModule I get the following warning Function bindViewModelProviderFactory is never used and
Function bindMainViewModel is never used

#Inject lateinit property has not been initialized Dagger2

I try to use Dagger2 to my project. I have a Firebase service and a class called SyncFactory that makes a specific request. When i get a call from Firebase i make my request.
I have created a Mangers Module
#Module(includes = [RepositoryModule::class, NetworkModule::class, AppModule::class])
class ManagersModule {
...
#Singleton
#Provides
fun provideSyncFactory(context: Context, accountsRepository: AccountsRepository, messagesRepository: MessagesRepository) : SyncFactory {
return SyncFactory(context, accountsRepository, messagesRepository)
}
...
}
The SyncFactory class is like below
class SyncFactory #Inject constructor(
private val context: Context,
private val accountsRepository: AccountsRepository,
private val messagesRepository: MessagesRepository
) {
fun getAccounts(){....}
and i also have an interface
#Singleton
#Component(modules = [ViewModelsModule::class, DatabaseModule::class, RepositoryModule::class, AppModule::class, NetworkModule::class, ManagersModule::class])
interface ViewModelComponent {
fun inject(viewModels: ViewModels)
fun inject(firebaseService: AppFirebase)
}
And finally inside my firebase service i Inject the SyncFactory
class AppFirebase : FirebaseMessagingService(), SyncFactoryCallback {
#Inject
lateinit var syncFactory: SyncFactory
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
// lateinit property syncFactory has not been initialized
syncFactory.getAccounts()
}
And when my service gets called i get a lateinit property syncFactory has not been initialized exception.
What do i do wrong..?
The solution is to implement a HasServiceInjector in your Application class
class MyApplication : Application(), HasServiceInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Service>
companion object {
private lateinit var instance: MyApplication
}
override fun serviceInjector(): AndroidInjector<Service> {
return dispatchingAndroidInjector
}
}

Inject ViewModel using Dagger 2 + Kotlin + ViewModel

class SlideshowViewModel : ViewModel() {
#Inject lateinit var mediaItemRepository : MediaItemRepository
fun init() {
What goes here?
}
So I'm trying to learn Dagger2 so I can make my apps more testable. Problem is, I've already integrated Kotlin and am working on the Android Architectural components. I understand that constructor injection is preferable but this isn't possible with ViewModel. Instead, I can use lateinit in order to inject but I'm at a loss to figure out how to inject.
Do I need to create a Component for SlideshowViewModel, then inject it? Or do I use the Application component?
gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
kapt {
generateStubs = true
}
dependencies {
compile "com.google.dagger:dagger:2.8"
annotationProcessor "com.google.dagger:dagger-compiler:2.8"
provided 'javax.annotation:jsr250-api:1.0'
compile 'javax.inject:javax.inject:1'
}
Application Component
#ApplicationScope
#Component (modules = PersistenceModule.class)
public interface ApplicationComponent {
void injectBaseApplication(BaseApplication baseApplication);
}
BaseApplication
private static ApplicationComponent component;
#Override
public void onCreate() {
super.onCreate();
component = DaggerApplicationComponent
.builder()
.contextModule(new ContextModule(this))
.build();
component.injectBaseApplication(this);
}
public static ApplicationComponent getComponent() {
return component;
}
You can enable constructor injection for your ViewModels. You can check out Google samples to see how to do it in Java. (Update: looks like they converted the project to Kotlin so this URL no longer works)
Here is how to do a similar thing in Kotlin:
Add ViewModelKey annotation:
import android.arch.lifecycle.ViewModel
import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import dagger.MapKey
import kotlin.reflect.KClass
#Suppress("DEPRECATED_JAVA_ANNOTATION")
#Documented
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Add ViewModelFactory:
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
#Singleton
class ViewModelFactory #Inject constructor(
private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("unknown model class " + modelClass)
}
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Add ViewModelModule:
import dagger.Module
import android.arch.lifecycle.ViewModel
import dagger.multibindings.IntoMap
import dagger.Binds
import android.arch.lifecycle.ViewModelProvider
import com.bubelov.coins.ui.viewmodel.EditPlaceViewModel
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(EditPlaceViewModel::class) // PROVIDE YOUR OWN MODELS HERE
internal abstract fun bindEditPlaceViewModel(editPlaceViewModel: EditPlaceViewModel): ViewModel
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
Register your ViewModelModule in your component
Inject ViewModelProvider.Factory in your activity:
#Inject lateinit var modelFactory: ViewModelProvider.Factory
private lateinit var model: EditPlaceViewModel
Pass your modelFactory to each ViewModelProviders.of method:
model = ViewModelProviders.of(this, modelFactory)[EditPlaceViewModel::class.java]
Here is the sample commit which contains all of the required changes: Support constructor injection for view models
Assuming you have a Repository class that can be injected by Dagger and a MyViewModel class that has a dependency on Repository defined as such:
class Repository #Inject constructor() {
...
}
class MyViewModel #Inject constructor(private val repository: Repository) : ViewModel() {
...
}
Now you can create your ViewModelProvider.Factory implementation:
class MyViewModelFactory #Inject constructor(private val myViewModelProvider: Provider<MyViewModel>) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return myViewModelProvider.get() as T
}
}
Dagger setup does not look too complicated:
#Component(modules = [MyModule::class])
interface MyComponent {
fun inject(activity: MainActivity)
}
#Module
abstract class MyModule {
#Binds
abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
Here's the activity class (might be fragment as well), where the actual injection takes place:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var factory: ViewModelProvider.Factory
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// retrieve the component from application class
val component = MyApplication.getComponent()
component.inject(this)
viewModel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
}
}
No. You create a component where you are declaring (using) your viewModel. It is normally an activity/fragment. The viewModel has dependencies (mediaitemrepository), so you need a factory. Something like this:
class MainViewModelFactory (
val repository: IExerciseRepository): ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(p0: Class<T>?): T {
return MainViewModel(repository) as T
}
}
Then the dagger part (activity module)
#Provides
#ActivityScope
fun providesViewModelFactory(
exerciseRepos: IExerciseRepository
) = MainViewModelFactory(exerciseRepos)
#Provides
#ActivityScope
fun provideViewModel(
viewModelFactory: MainViewModelFactory
): MainViewModel {
return ViewModelProviders
.of(act, viewModelFactory)
.get(MainViewModel::class.java)
}
Refer to a repo I created when I was learning dagger+kotlin
Essentially you need a ViewModelFactory instance to the UI layer, you use that to create a viewmodel.
#AppScope
class ViewModelFactory
#Inject
constructor(private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>)
: ViewModelProvider.Factory {
#SuppressWarnings("Unchecked")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator = creators[modelClass]
if (creator == null) {
for (entry in creators) {
if (modelClass.isAssignableFrom(entry.key)) {
creator = entry.value
break
}
}
}
if (creator == null) throw IllegalArgumentException("Unknown model class" + modelClass)
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Your ViewModelModule should look like (this is where you store all viewmodels).
#Module
abstract class ViewModelModule {
#AppScope
#Binds
#IntoMap
#ViewModelKey(YourViewModel::class)
abstract fun bindsYourViewModel(yourViewModel: YourViewModel): ViewModel
// Factory
#AppScope
#Binds abstract fun bindViewModelFactory(vmFactory: ViewModelFactory): ViewModelProvider.Factory
}
Then create a dagger map key
#Documented
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Then on your UI layer, inject the factory and instantiate your viewmodel using ViewModelProviders
class YourActivity : BaseActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var yourViewModel: YourViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
...
(application as App).component.inject(this)
}
override fun onStart() {
super.onStart()
yourViewModel = ViewModelProviders.of(this, viewModelFactory).get(YourViewModel::class.java)
// you can now use your viewmodels properties and methods
yourViewModel.methodName()
yourViewModel.list.observe(this, { ... })
}
you expose the ViewModel on your component:
#Singleton
#Component(modules={...})
public interface SingletonComponent {
BrandsViewModel brandsViewModel();
}
And now you can access this method on the component inside the ViewModelFactory:
// #Inject
BrandsViewModel brandsViewModel;
...
brandsViewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {
#Override
public <T extends ViewModel> create(Class<T> modelClazz) {
if(modelClazz == BrandsViewModel.class) {
return singletonComponent.brandsViewModel();
}
throw new IllegalArgumentException("Unexpected class: [" + modelClazz + "]");
}).get(BrandsViewModel.class);
All this can be simplified and hidden with Kotlin:
inline fun <reified T: ViewModel> AppCompatActivity.createViewModel(crossinline factory: () -> T): T = T::class.java.let { clazz ->
ViewModelProvider(this, object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass == clazz) {
#Suppress("UNCHECKED_CAST")
return factory() as T
}
throw IllegalArgumentException("Unexpected argument: $modelClass")
}
}).get(clazz)
}
which now lets you do
brandsViewModel = createViewModel { singletonComponent.brandsViewModel() }
Where now BrandsViewModel can receive its parameters from Dagger:
class BrandsViewModel #Inject constructor(
private val appContext: Context,
/* other deps */
): ViewModel() {
...
}
Though the intent might be cleaner if a Provider<BrandsViewModel> is exposed from Dagger instead
interface SingletonComponent {
fun brandsViewModel(): Provider<BrandsViewModel>
}
brandsViewModel = createViewModel { singletonComponent.brandsViewModel().get() }
Try with below code :
#Provides
#Singleton
fun provideRepository(): Repository {
return Repository(DataSource())
}
I wrote a library that should make this more straightforward and way cleaner, no multibindings or factory boilerplate needed, while also giving the ability to further parametrise the ViewModel at runtime:
https://github.com/radutopor/ViewModelFactory
#ViewModelFactory
class UserViewModel(#Provided repository: Repository, userId: Int) : ViewModel() {
val greeting = MutableLiveData<String>()
init {
val user = repository.getUser(userId)
greeting.value = "Hello, $user.name"
}
}
In the view:
class UserActivity : AppCompatActivity() {
#Inject
lateinit var userViewModelFactory2: UserViewModelFactory2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
appComponent.inject(this)
val userId = intent.getIntExtra("USER_ID", -1)
val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
.get(UserViewModel::class.java)
viewModel.greeting.observe(this, Observer { greetingText ->
greetingTextView.text = greetingText
})
}
}
Here is my solution using reflection.
Let's say for simplicity you have AppComponent
#AppScope
#Component(modules = [AppModule::class])
interface AppComponent {
fun getAppContext(): Context
fun getRepository(): Repository
fun inject(someViewModel: SomeViewModel)
class App : Application() {
companion object {
lateinit var appComponent: AppComponent
private set
}
...
}
fun appComponent() = App.appComponent
And you need inject SomeViewModel class
class SomeViewModel: ViewModel() {
#Inject
lateinit var repository: Repository
}
Create custom lazy property delegate
inline fun <reified T: ViewModel> Fragment.viewModel(component: Any?) = lazy {
val vm = ViewModelProvider(this).get(T::class.java)
component?.let {
val m = component.javaClass.getMethod("inject", T::class.java)
m.invoke(component, vm)
}
vm
}
And use it
class SomeFragment: Fragment() {
private val vm: SomeViewModel by viewModel(appComponent())
...
}
With the solution below, I found I can use injection anywhere I want by including this line in the init or onCreate methods (No factories needed, so it works with ViewModel and WorkManager)
Injector.getComponent().inject(this)
BaseApplication
class BaseApplication : Application() {
lateinit var applicationComponent: ApplicationComponent
override fun onCreate() {
super.onCreate()
INSTANCE = this
applicationComponent = DaggerApplicationComponent
.builder()
//Add your modules like you did in your question above
.build()
}
companion object {
private var INSTANCE: BaseApplication? = null
#JvmStatic
fun get(): BaseApplication= INSTANCE!!
}
}
Injector
class Injector private constructor() {
companion object {
#JvmStatic
fun getComponent(): ApplicationComponent = BaseApplication.get().applicationComponent
}
}
Essentially, you access applicationComponent with a static method. With that, you should be able to inject any class you've made an inject method for in your component with this line:
Injector.getComponent().inject(this)
in your case
init{
Injector.getComponent().inject(this)
}

Categories

Resources