How can I create ViewModel and inject repository to it with dagger 2?
Asked Answered
G

4

10

I try understanding ViewModel. I create ViewModel:

public class UsersViewModel extends ViewModel {

    private final UsersRepository usersRepository;

    public UsersViewModel(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    public LiveData<List<User>> loadAll() {
        return usersRepository.getAll();
    }

}

But I not understand 2 things:

  1. How can I inject UsersRepository to this VievModel? When I used presenter I can create it with dagger 2 like this:
@Module
public class PresentersModule {

    @Singleton
    @Provides
    UsersPresenter provideUsersPresenter(UsersRepository usersRepository) {
        return new UsersPresenter(usersRepository);
    }
}

but how can I do it with ViewModel? Like this?

@Module
public class ViewModelsModule {

    @Singleton
    @Provides
    UsersViewModel provideUsersViewModel(UsersRepository usersRepository) {
        return new UsersViewModel(usersRepository);
    }
}
  1. How can I get this ViewModel in fragment? With presenter I can this:

    presenter = MyApplication.get().getAppComponent().getUsersPresenter();

Gesso answered 8/7, 2018 at 12:47 Comment(0)
S
15

ViewModel is created via ViewModelProvider that uses a ViewModelFactory to create the instances. You can't inject ViewModels directly, instead you should use a custom factory like below

@Singleton
public class DaggerViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

@Inject
public DaggerViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
    this.creators = creators;
}

@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
    Provider<? extends ViewModel> creator = creators.get(modelClass);
    if (creator == null) {
        for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
            if (modelClass.isAssignableFrom(entry.getKey())) {
                creator = entry.getValue();
                break;
            }
        }
    }
    if (creator == null) {
        throw new IllegalArgumentException("unknown model class " + modelClass);
    }
    try {
        return (T) creator.get();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
}

Then you need a module for the dagger that creates the view model factory and view models itself.

@Module
abstract class ViewModelModule {
@Binds
abstract ViewModelProvider.Factory bindViewModelFactory(DaggerViewModelFactory factory);

@Binds
@IntoMap
@ViewModelKey(VideoListViewModel.class)
abstract ViewModel provideVideoListViewModel(VideoListViewModel videoListViewModel);

@Binds
@IntoMap
@ViewModelKey(PlayerViewModel.class)
abstract ViewModel providePlayerViewModel(PlayerViewModel playerViewModel);

@Binds
@IntoMap
@ViewModelKey(PlaylistViewModel.class)
abstract ViewModel providePlaylistViewModel(PlaylistViewModel playlistViewModel);

@Binds
@IntoMap
@ViewModelKey(PlaylistDetailViewModel.class)
abstract ViewModel providePlaylistDetailViewModel(PlaylistDetailViewModel playlistDetailViewModel);
}

ViewModelKey file is like this

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
    Class<? extends ViewModel> value();
}

Now to get your view model in the activity or fragment simply inject the view model factory and then use that factory to create the view model instances

public class PlayerActivity extends BaseActivity {
     @Inject DaggerViewModelFactory viewModelFactory;
     PlayerViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);
        viewModel = ViewModelProviders.of(this,viewModelFactory).get(PlayerViewModel.class);

}

To inject anything to your ViewModel like the repository simply use Constructor Injection.

public class PlayerViewModel extends ViewModel {
    private VideoRepository videoRepository;
    private AudioManager audioManager;


    @Inject
    public PlayerViewModel(VideoRepository videoRepository, AudioManager audioManager) {
         this.videoRepository = videoRepository;
         this.audioManager = audioManager;

    }
}

Check out the fully working example from here https://github.com/alzahm/VideoPlayer, Also I learned many of the dagger stuff from google samples, you can check them out as well.

Stunk answered 8/7, 2018 at 13:46 Comment(1)
Looking at this I seriously question whether this has anything to do with dependency injection. Calling get(PlayerViewModel.class) completely negates the purpose of DI which is to allow for testability.Bobbette
L
2

Dagger2 required you to create ViewModelModule and do the binding to your ViewModels

I know you are asking about Dagger2, but if you are starting a new project, maybe you can check out Koin which provides a lightweight injection.

I've used it in some of my production apps and it works fine with lesser lines of code.

You can just declare in your module like

viewModel { MyViewModel(get()) }

Then in your activities / fragments / classes (you need to extend KoinComponent), just write

val myViewModel : MyViewModel by viewModel()

It will handle the creation itself.

For more information can refer

https://insert-koin.io/

https://start.insert-koin.io/#/getting-started/koin-for-android?id=android-viewmodel

Layamon answered 9/12, 2019 at 7:24 Comment(2)
Yes, Koin more simple than DaggerLargish
And what do you mean by "lightweight injection"? BTW, Koin is a service locatorDisappointed
S
0

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
        })
    }
}
Skald answered 21/11, 2018 at 18:3 Comment(0)
P
0

This solution allows you to inject dependencies into ViewModels and Workers without factories. Instead, it uses static methods.

Injecting the Graph into a class

This line can be used in the init block or the onCreate method (Example ViewModel at the bottom)

    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
                .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
        }
    }

That's it!

Request injection like usual in your ApplicationComponent

ApplicationComponent

Singleton
@Component(modules = [add your modules])
interface ApplicationComponent {
    fun inject(someViewModel: SomeViewModel)
}

ViewModel

    class SomeViewModel : ViewModel(){

        @Inject
        lateinit var someClass: SomeClass //In your case, userRepository: UserRepository

        init{
            Injector.getComponent().inject(this)
        }
    }
Pesticide answered 9/6, 2021 at 8:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.