Inject property into ViewModel using Dagger 2 - android

I try to learn how to use Dagger 2. Please help with follow exception:
Exception:
UninitializedPropertyAccessException: lateinit property trips has not
been initialized
MainActivityViewModel:
class MainActivityViewModel : ViewModel() {
private lateinit var tripsLiveData: MutableLiveData<List<Trip>>
#Inject
lateinit var trips : List<Trip>
fun getTrips() : LiveData<List<Trip>> {
if (!::tripsLiveData.isInitialized){
tripsLiveData = MutableLiveData()
tripsLiveData.value = trips
}
return tripsLiveData
}
}
TripModule:
#Module
class TripModule{
#Provides
fun provideTrips(): List<Trip> {
var list = ArrayList<Trip>()
list.add(Trip(100,10))
list.add(Trip(200,20))
return list
}
}
AppComponent:
#Singleton
#Component(modules = [
AndroidSupportInjectionModule::class,
ActivityBuilder::class,
TripModule::class])
interface AppComponent{
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(app: MyApplication)
}
MainActivity:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var tripsAdapter: TripsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
// Inject external dependencies
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupRecyclerView();
setUpViewModel();
}
private fun setupRecyclerView() {
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
adapter = tripsAdapter
}
}
private fun setUpViewModel(){
val model = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
model.getTrips().observe(this, Observer { tripsAdapter.trips = it!! })
}
}

If you want your viewmodel's to be part of the dagger graph, you need to do several things - using dagger's multibindings (just once, for newer viewmodels it will be easier). You'd create new viewmodel factory which will take care of instantiating viewmodels. This factory will be part of dagger graph and therefore will have references to anything provided via dagger. You can then have either constructor injection via #Inject constructor(anyParameterFromDagger: Param) or #Inject lateinit var someParam: Param inside the body of viewmodel.
1) Create qualifier for view model classes
#MustBeDocumented
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
2) create viewmodel factory which takes values from dagger's multibindings
#Singleton
class DaggerViewModelFactory #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)
}
}
}
3) have dagger module which will provide the factory (from point 2) and then your viewmodels
abstract class YourDaggerModuleWhichThenNeedToBePartOfYourGraphAsIncluded {
#Binds
abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory // this needs to be only one for whole app (therefore marked as `#Singleton`)
#Binds
#IntoMap
#ViewModelKey(MainActivityViewModel::class)
abstract fun bindMainActivityViewModel(vm: MainActivityViewModel): ViewModel // for every viewmodel you have in your app, you need to bind them to dagger
}
4) in your activity, when you get your viewmodel, you need to use the factory from dagger: (places changed marked as // TODO in the code below)
class MainActivity : AppCompatActivity() {
#Inject
lateinit var tripsAdapter: TripsAdapter
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory // TODO this was added to the activity
override fun onCreate(savedInstanceState: Bundle?) {
// Inject external dependencies
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupRecyclerView();
setUpViewModel();
}
private fun setupRecyclerView() {
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
adapter = tripsAdapter
}
}
private fun setUpViewModel(){
val model = ViewModelProviders.of(this, viewModelFactory)[MainActivityViewModel::class.java] // TODO this was changed
model.getTrips().observe(this, Observer { tripsAdapter.trips = it!! })
}
}
I didn't provide the code for including module to dagger component as I hope this is something you already did.
You can read more about this e.g. in this medium article (I'm not author of the article):

#Provides
fun provideTrips(): List<Trip> {
I'm a bit wary of this in the sense that I doubt that it's really Dagger's job to provide this for you directly, but I'll just take that for granted and ignore that for now.
Your code should be:
class MainActivityViewModel #Inject constructor(
trips: List<Trip>
): ViewModel() {
private val tripsLiveData: MutableLiveData<List<Trip>> = MutableLiveData()
init {
tripsLiveData.setValue(trips)
}
fun getTrips() : LiveData<List<Trip>> = tripsLiveData
}
#Module
class TripModule{
#Provides
// #Singleton // <-- possibly should be here?
fun provideTrips(): List<Trip> = listOf(
Trip(100,10),
Trip(200,20)
)
}
#Singleton
#Component(modules = [
AndroidSupportInjectionModule::class,
ActivityBuilder::class,
TripModule::class
])
interface AppComponent{
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(app: MyApplication)
}
class MainActivity : AppCompatActivity() {
#Inject
lateinit var viewModelProvider: Provider<MainActivityViewModel>
// this is specifically not marked with `#Inject`
lateinit var viewModel: MainActivityViewModel
private val tripsAdapter = TripsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
// Inject external dependencies
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupRecyclerView();
setUpViewModel();
}
private fun setupRecyclerView() {
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
adapter = tripsAdapter
}
}
private fun setUpViewModel(){
viewModel = ViewModelProviders.of(this, object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass == MainActivityViewModel::class.java) {
#Suppress("UNCHECKED_CAST")
return viewModelProvider.get() as T
}
throw IllegalArgumentException("Unexpected argument: $modelClass")
}
}).get(MainActivityViewModel::class.java)
viewModel.getTrips().observe(this, Observer {
val trips = it ?: return#observe
tripsAdapter.trips = trips
})
}
}
Aka you should use constructor injection, use the Provider<T> to only get the ViewModel from Dagger when you actually need it, and otherwise initialize it via a ViewModelProviders.Factory so that you actually get the ViewModel from Dagger.

Related

dagger 2 Subcomponent

while Dagger implementation i have below error. please help me resolve it.
dagger/component/AppComponent.java:10: error: [com.example.testproject.app.dagger.component.PostLoginComponent.inject(com.example.testproject.view.photo.PhotoListActivity)] com.example.testproject.data.UserStore cannot be provided without an #Inject constructor or from an #Provides-annotated method.
My understanding - I have created AppComponent and AppModule which is parent for PostLoginComponent and PostLoginModule (as they are subcomponent).
photoListViewModel is injected in PhotoListActivity. photoListViewModel is provide by postLoginComponent and PostLoginModule.Now photoListViewModel need UserStore as dependency which is present in AppModule.
So as per my understanding all object from parent module are available for child module.
in this case UserStore from AppModule should be available for PhotoListViewModel in PostLoginModule.
But as per error UserStore is not provided.
#Module
abstract class AppModule {
#Module
companion object {
#Provides
fun provideUserStore(context: Context): UserStore {
return UserStore(context)
}
}
}
#Component(modules = [AppModule::class])
interface AppComponent {
fun postLoginComponent(): PostLoginComponent.Builder
#Component.Builder
interface Builder {
#BindsInstance
fun bindData(context: Context): Builder
fun build(): AppComponent
}
}
#Module
abstract class PostLoginModule {
#Binds
#IntoMap
#ViewModelKey(PhotoListViewModel::class)
abstract fun bindViewModel(viewModel: PhotoListViewModel): ViewModel
#MustBeDocumented
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
}
#Subcomponent(modules = [PostLoginModule::class])
interface PostLoginComponent {
fun inject(activity: PhotoListActivity)
#Subcomponent.Builder
interface Builder {
fun build(): PostLoginComponent
}
}
class PhotoListActivity() : PostLoginActivity() {
var adapter = PhotoListAdapter(
mutableListOf(
PhotoItem("", "akash"),
PhotoItem("", "akash"),
PhotoItem("", "akash"),
PhotoItem("", "akash")
)
)
#Inject
lateinit var factory: ViewModelFactory
private val photoListViewModel: PhotoListViewModel by lazy {
ViewModelProvider(this, factory).get(PhotoListViewModel::class.java)
}
override fun setLayoutId(): Int? {
return R.layout.activity_photo_list
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
TestApp.get(this).appComponent().postLoginComponent().build().inject(this)
initView()
}
private fun initView() {
textView.setOnClickListener {
adapter.clearData()
}
rvPhotoList.initRecyclerView {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
adapter.setItemClickListener(OnRecyclerViewOnItemClickListener { parent, view, position -> })
rvPhotoList.addErrorView(EmptyErrorView(this))
getPhotoList()
}
private fun getPhotoList() {
adapter.addItems(photoListViewModel.getPhotoList())
}
}
class PhotoListViewModel #Inject constructor(userStore: UserStore) : BaseViewModel() {
fun getPhotoList(): List<PhotoItem> {
return mutableListOf(
PhotoItem("", "aher"),
PhotoItem("", "aher"),
PhotoItem("", "aher"),
PhotoItem("", "aher")
)
}
}

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

Dagger not initizalize var

I have problems when starting Dagger in Android project with Kotlin.
This estructure is the next one
Dagger is included in an Android module that is called by the client application
MagicBox.kt
interface MagicBox {
fun getDate(): Long?
}
MagicBoxImpl.kt
class MagicBoxImpl (): MagicBox{
var date: Long = Date().time
override fun getDate(): Long {
return date
}
}
MainModule.kt
#Module
class MainModule (private val app: Application) {
#Provides
#Singleton
fun provideMagicBox(): MagicBox {
return MagicBoxImpl()
}
}
MainComponent.kt
#Singleton
#Component(modules = [MainModule::class, PresenterModule::class])
interface MainComponent{
fun inject(target: Activity)
}
Application.kt
class Application: Application() {
lateinit var mainComponent: MainComponent
override fun onCreate() {
super.onCreate()
mainComponent = initDagger(this)
}
private fun initDagger(app: Application): MainComponent =
DaggerMainComponent.builder()
.mainModule(MainModule(app))
.build()
}
MainActivity.kt
#Inject
lateinit var magicBox: MagicBox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_layout)
(application as ClientSdk).mainComponent.inject(this)
tvDaggerTest = findViewById(R.id.tvDaggerTest)
tvDaggerTest!!.text = magicBox.getDate().toString()
}
Get the following error
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property magicBox has not been initialized
fun inject(target: Activity) should be fun inject(target: MainActivity)
Also for better Dagger usage, the following should be:
#Module
abstract class MainModule {
#Binds
abstract fun magicBox(impl: MagicBoxImpl): MagicBox
}
and
#Singleton class MagicBoxImpl #Inject constructor(): MagicBox {

Dagger2 + Kotlin: lateinit property has not been initialized

I'm trying to inject the ViewModelFactory into my Activity, but it keeps throwing this same error: lateinit property viewModelFactory has not been initialized. I can't find what I may be doing wrong. See the code above from my classes
AppComponent.kt
#Component(modules = [(AppModule::class), (NetworkModule::class), (MainModule::class)])
interface AppComponent {
fun inject(application: TweetSentimentsApplication)
fun inject(mainActivity: MainActivity)
fun context(): Context
fun retrofit(): Retrofit
}
MainModule.kt
#Module
class MainModule {
#Provides
fun mainViewModelFactorty(repository: TweetRepository): MainViewModelFactory = MainViewModelFactory(repository)
#Provides
fun local(database: AppDatabase): TweetLocal = TweetLocal(database)
#Provides
fun remote(tweetService: TweetService): TweetRemote = TweetRemote(tweetService)
#Provides
fun tweetService(retrofit: Retrofit): TweetService = retrofit.create(TweetService::class.java)
#Provides
fun repository(local: TweetLocal, remote: TweetRemote): TweetRepository = TweetRepository(local, remote)
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
#Inject lateinit var viewModelFactory: MainViewModelFactory
private val viewModel: MainViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
viewModel?.init("guuilp")
viewModel?.getTweetList()?.observe(this, Observer {
Toast.makeText(this, it?.size.toString(), Toast.LENGTH_LONG).show()
})
}
}
TweetSentimentsApplication.kt
open class TweetSentimentsApplication: Application(){
companion object {
lateinit var appComponent: AppComponent
}
override fun onCreate() {
super.onCreate()
initDI()
}
private fun initDI() {
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
}
}
You have to call the inject(mainActivity: MainActivity) method you've defined in AppComponent when you're initializing your MainActivity, that's how Dagger actually injects the dependencies you need.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
// This is where the dependencies are injected
TweetSentimentsApplication.appComponent.inject(this)
ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
...
}
Also, make sure that your application name is added in the AndroidManifest.xml file.
<application
android:name=".YourAppName"
..../>
You can also do this:
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val mainViewModel: MainViewModel by lazy {
ViewModelProviders.of(this, viewModelFactory)[MainViewModel::class.java]
}
and use abstract modules with #ContributesAndroidInjector for the activity, and
abstract module for view model. Using abstract is more efficient:
#Module
abstract class AndroidBindingModule {
#ContributesAndroidInjector
internal abstract fun contributesAnActivity(): AnActivity
}
#Module
abstract class ViewModelModule {
//the default factory only works with default constructor
#Binds
#IntoMap
#ViewModelKey(AViewModel::class)
abstract fun bindArtViewModel(aViewModel: AViewModel): ViewModel
#Binds
abstract fun bindViewModelFactory(factory: AViewModelFactory): ViewModelProvider.Factory
}
#Documented
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(RetentionPolicy.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
You could also extend DaggerAppCompatActivity in place of AppCompatActivity. E.g.
class MainActivity : DaggerAppCompatActivity() {
#Inject lateinit var viewModelFactory: MainViewModelFactory
private val viewModel: MainViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
viewModel?.init("guuilp")
viewModel?.getTweetList()?.observe(this, Observer {
Toast.makeText(this, it?.size.toString(), Toast.LENGTH_LONG).show()
})
}
}
My mistake was creating a new object and taking a component from it such as App().component.
So in this situation you need to put component field in companion object and replace code with App.component
Maybe you missed implementing "Injectable" Interface in fragment/activity. Which marks fragment/activity as injectable.

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