Внедрить ViewModel с помощью Dagger 2 + Kotlin + ViewModel

class SlideshowViewModel : ViewModel() {

@Inject lateinit var mediaItemRepository : MediaItemRepository

fun init() {
    What goes here?
}

Поэтому я пытаюсь изучить Dagger2, чтобы сделать свои приложения более тестируемыми. Проблема в том, что я уже интегрировал Kotlin и работаю над архитектурными компонентами Android. Я понимаю, что внедрение конструктора предпочтительнее, но с ViewModel это невозможно. Вместо этого я могу использовать lateinit для инъекции, но я не понимаю, как вводить.

Мне нужно создать Component для SlideshowViewModel, а затем ввести его? Или я использую компонент Application?

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

Компонент приложения

@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;
    }

person easycheese    schedule 23.06.2017    source источник
comment
Итак, я пытаюсь изучить Dagger 2, чтобы сделать свои приложения более тестируемыми - я бы сказал, что Dagger 2 либо не влияет на тестируемость, либо его влияние незначительно. Если это единственная причина, вы можете отказаться от нее. * Выступая от имени пользователя Dagger, который опубликовал много руководств по этому поводу   -  person Vasiliy    schedule 17.02.2018
comment
Не ответ на ваш вопрос, но вы забыли apply plugin: 'kotlin-kapt' в своем build.gradle файле. Кроме того, вы должны использовать kapt вместо annotationProcessor .   -  person Benjamin    schedule 21.02.2018


Ответы (9)


Вы можете включить внедрение конструктора для своих ViewModels. Вы можете проверить Примеры Google, чтобы узнать, как это сделать на Java. (Обновление: похоже, они преобразовали проект в Kotlin, поэтому этот URL-адрес больше не работает)

Вот как сделать то же самое в Котлине:

Добавить аннотацию ViewModelKey:

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>)

Добавить 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)
        }
    }
}

Добавить 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
}

Зарегистрируйте свой ViewModelModule в своем компоненте

Вставьте ViewModelProvider.Factory в свою активность:

@Inject lateinit var modelFactory: ViewModelProvider.Factory
private lateinit var model: EditPlaceViewModel

Передайте свой modelFactory каждому методу ViewModelProviders.of:

model = ViewModelProviders.of(this, modelFactory)[EditPlaceViewModel::class.java]

Вот образец фиксации, который содержит все необходимые изменения: Поддержка внедрения конструктора для моделей представления

person Igor Bubelov    schedule 17.02.2018
comment
@ 11m0 да, похоже, они наконец преобразовали его в Kotlin - person Igor Bubelov; 31.05.2018
comment
Если область видимости ViewModelFactory равна Singleton, означает ли это, что область видимости всех моделей представления в ViewModelModule также будет Singleton? - person 11m0; 31.05.2018

Предположим, у вас есть класс Repository, который может быть введен Dagger, и класс MyViewModel, который имеет зависимость от Repository, определенного как таковой:


    class Repository @Inject constructor() {
       ...
    }

    class MyViewModel @Inject constructor(private val repository: Repository) : ViewModel() {
        ...
    }

Теперь вы можете создать свою ViewModelProvider.Factory реализацию:

    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
      }

    }

Настройка кинжала не выглядит слишком сложной:


    @Component(modules = [MyModule::class])
    interface MyComponent {
      fun inject(activity: MainActivity)
    }

    @Module
    abstract class MyModule {
      @Binds
      abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
    }

Вот класс активности (также может быть фрагментом), в котором происходит фактическая инъекция:


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

    }

person azizbekian    schedule 17.02.2018
comment
Сработало отлично. У меня была ошибка, если модель просмотра была определена до фабрики, есть идеи, почему? - person Leandro Temperoni; 01.06.2018

Нет. Вы создаете компонент, в котором объявляете (используете) вашу модель представления. Обычно это действие / фрагмент. ViewModel имеет зависимости (mediaitemrepository), поэтому вам нужна фабрика. Что-то вроде этого:

    class MainViewModelFactory (
            val repository: IExerciseRepository): ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(p0: Class<T>?): T {
            return MainViewModel(repository) as T
        }
    }

Затем часть кинжала (модуль активности)

    @Provides
    @ActivityScope
    fun providesViewModelFactory(
            exerciseRepos: IExerciseRepository
    ) = MainViewModelFactory(exerciseRepos)

    @Provides
    @ActivityScope
    fun provideViewModel(
            viewModelFactory: MainViewModelFactory
    ): MainViewModel {
        return ViewModelProviders
                .of(act, viewModelFactory)
                .get(MainViewModel::class.java)
    }
person johnny_crq    schedule 23.06.2017
comment
Будьте осторожны с этим. Я использовал аналогичную настройку, но понял, что мои ViewModel не пережили изменений конфигурации. Область видимости ViewModel должна быть за пределами Activity, чтобы ее можно было использовать, когда Activity будет уничтожен и воссоздан. - person RussHWolf; 24.09.2017
comment
Как вы используете свой ответ во фрагменте и во вьюмодели? Пожалуйста, предоставьте немного больше информации. - person Damia Fuentes; 18.10.2017

Обратитесь к репо, которое я создал, когда изучал кинжал + котлин

По сути, вам нужен экземпляр ViewModelFactory для слоя пользовательского интерфейса, который вы используете для создания модели просмотра.

@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)
        }
    }
}

Ваш ViewModelModule должен выглядеть так (здесь вы храните все модели просмотра).

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

Затем создайте ключ карты кинжала

@Documented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

Затем на уровне пользовательского интерфейса вставьте фабрику и создайте экземпляр своей модели представления с помощью 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, { ... })

    }
person Vaughn Armada    schedule 27.03.2018

Попробуйте использовать код ниже:

@Provides
@Singleton
fun provideRepository(): Repository {
    return Repository(DataSource())
}
person Harsh Agrawal    schedule 21.02.2018

Я написал библиотеку, которая должна сделать это более простым и понятным, не требуя множественных привязок или заводских шаблонов, а также предоставляя возможность дальнейшей параметризации ViewModel во время выполнения: 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"
    }    
}

В представлении:

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
        })
    }
}
person Radu Topor    schedule 21.11.2018

Вот мое решение с использованием отражения.

Скажем, для простоты у вас есть 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

И вам нужно ввести SomeViewModel класс

class SomeViewModel: ViewModel() {
    @Inject
    lateinit var repository: Repository
}

Создание настраиваемого ленивого делегата свойства

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
}

И использовать это

class SomeFragment: Fragment() {
    private val vm: SomeViewModel by viewModel(appComponent())
    ...
}
person Dmitry Kopytov    schedule 12.02.2021

С помощью приведенного ниже решения я обнаружил, что могу использовать инъекцию где угодно, включив эту строку в методы init или onCreate (фабрики не нужны, поэтому она работает с ViewModel и 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!!
    }
}

Инжектор

class Injector private constructor() {
    companion object {
        @JvmStatic
        fun getComponent(): ApplicationComponent = BaseApplication.get().applicationComponent
    }
}

По сути, вы обращаетесь к applicationComponent с помощью статического метода. При этом вы должны иметь возможность внедрить любой класс, для которого вы создали метод инъекции в своем компоненте, с помощью этой строки:

Injector.getComponent().inject(this)

в твоем случае

init{
    Injector.getComponent().inject(this)
}
person Darryl Johnson    schedule 09.06.2021

вы выставляете ViewModel на своем компоненте:

@Singleton
@Component(modules={...})
public interface SingletonComponent {
    BrandsViewModel brandsViewModel();
}

И теперь вы можете получить доступ к этому методу в компоненте внутри 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);

Все это можно упростить и скрыть с помощью 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)
}

что теперь позволяет вам делать

brandsViewModel = createViewModel { singletonComponent.brandsViewModel() }

Где теперь BrandsViewModel может получать свои параметры от Dagger:

class BrandsViewModel @Inject constructor(
    private val appContext: Context,
    /* other deps */
): ViewModel() {
    ...
}

Хотя намерение может быть чище, если вместо этого Кинжал показывает Provider<BrandsViewModel>.

interface SingletonComponent {
    fun brandsViewModel(): Provider<BrandsViewModel>
}

brandsViewModel = createViewModel { singletonComponent.brandsViewModel().get() }
person EpicPandaForce    schedule 09.06.2021