ОБНОВЛЕНИЕ:
Некоторые части этой статьи устарели. Теперь есть функции расширения делегата Kotlin, которые значительно упрощают часть этого процесса. Я не буду здесь вдаваться в подробности, но приведу короткие примеры их использования.

Добавлять ViewModels к действиям теперь можно следующим образом.

class MainActivity : Activity() {
    @Inject
    lateinit var factory: ViewModelProvider.Factory
    
    private val mainViewModel: MainViewModel by viewModels { factory }
    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
    }
}

Точно так же предоставление ViewModels фрагментам теперь можно выполнить следующим образом.

class MainFragment : Fragment() {
    @Inject
    lateinit var factory: ViewModelProvider.Factory
    private val mainViewModel: MainViewModel by activityViewModels { factory }
    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

Это не только очищает ваш код, но и устраняет необходимость в ViewModelClassMap, используемом ниже.

Вы можете прочитать оригинальную статью ниже.

Я недавно начал работать с компонентами архитектуры Android, и они потрясающие. Несмотря на опоздание на вечеринку (эти компоненты существуют уже некоторое время), мне было трудно найти документацию для того, что я считаю обычным вариантом использования: реализация моделей общего представления с инъекцией Dagger, которая могла бы легко поддерживать тестирование.

Сама ViewModel довольно хорошо документирована и проста в реализации. Если вы новичок в Android ViewModels, я предлагаю документацию Android как способ изучить основы.

После реализации моей первой модели общего представления я начал понимать мощь таких компонентов, как ViewModel и LiveData. Однако, выходя за рамки базового варианта использования ViewModels, я столкнулся с трудностями. Природа компонента с учетом жизненного цикла, совместно используемого несколькими действиями / фрагментами, может в определенном смысле усложнить жизнь. Я хотел бы использовать эту статью, чтобы рассказать вам о некоторых болевых точках, которые у меня были, и о том, как мне удалось их решить.

Первой серьезной трудностью, которую я заметил, было внедрение зависимостей с помощью Dagger. К счастью, мы можем прочитать эту отличную статью от Chili Labs, чтобы начать работу с ViewModel DI.

Я настоятельно рекомендую статью Chili Labs, если у вас возникли трудности с внедрением зависимостей ViewModel с помощью Dagger. Остальная часть этой статьи основана на подходе, принятом Chili Labs, поэтому перед тем, как двигаться дальше, настоятельно рекомендуется понять этот подход.

Второе препятствие, с которым мы сталкиваемся, - это сокрытие реализации ViewModel за абстракцией. Обычно это делается путем создания интерфейса и его расширения, а затем привязки интерфейса к реализации в Dagger, чтобы он знал, какую реализацию предоставить при запросе интерфейса. Примерно так.

interface MyUseCase {
    fun getInt(): Int
}
class MyUseCaseImpl : MyUseCase {
    override fun getInt() = 20
}
@Module
abstract class UseCaseBindingsModule {
    @Binds
    abstract fun myUseCase(impl: MyUseCaseImpl): MyUseCase
}

Это дает нам возможность легко заменить реализацию на заглушку, чтобы мы могли тестировать пользовательский интерфейс отдельно от логики UseCase.

К сожалению, из-за того, как мы внедряем ViewModels, мы не можем использовать описанный выше подход. Наша инъекция настроена на использование имени класса ViewModel, чтобы определить, какой поставщик использовать в ViewModelProvider.Factory, а интерфейсы не имеют имени класса. Итак, мы должны использовать абстрактный класс вместо интерфейса.

abstract class MainViewModel: ViewModel() {
    abstract val rand: LiveData<Int>
}
class MainViewModelImpl : MainViewModel() {
    override val rand = MutableLiveData().apply { 
        value = Random().nextInt()
    }
}

Внеся несколько небольших изменений в настройку Chili Labs, мы можем предоставить нашу реализацию всякий раз, когда абстракция запрашивается @Inject.

// Slightly altered code from the Chili Labs example to support abstract ViewModels
@Provides
@Singleton
fun viewModelFactory(
        providers: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) = object : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val provider = providers[modelClass] ?: providers
                .asIterable()
                .firstOrNull { it.key.isAssignableFrom(modelClass) }
                ?.value

        return requireNotNull(provider).get() as T
    }
}
@Module(includes = [
    ViewModelModule.ProvideViewModel::class,
    ViewModelModule.ProvideViewModelAbstractionMap::class
])
abstract class ViewModelModule {

    @Module
    class InjectViewModel {
        // Define the return type to be the abstraction, but ViewModelProviders 'gets' the implementation
        @Provides
        fun mainViewModel(): MainViewModel =
                ViewModelProviders.of(target, factory)
                    .get(MainViewModelImpl::class.java)
    }

    // Used so fragments can get the ViewModel from their activity without knowing what the implementation is
    @Module
    class ProvideViewModelAbstractionMap {
        @Provides
        fun viewModelClassMap(): ViewModelClassMap = 
                mapOf(
          MainViewModel::class.java to MainViewModelImpl::class.java
       )
    }
}
@Module
class ProvideViewModel {
    @Provides
    @IntoMap
    @ViewModelKey(MainViewModel::class) 
    // another small alteration, using the abstract class as the ViewModelKey and return type
    fun mainViewModel(): MainViewModel = MainViewModelImpl()
}

Обратите внимание на использование следующих псевдонимов

typealias ViewModelClassMap = Map<Class<out ViewModel>, @JvmSuppressWildcards Class<out ViewModel>>
// This extension is used next
@Suppress("UNCHECKED_CAST")
inline fun <reified T: ViewModel> ViewModelClassMap.getImplClass(clazz: Class<out ViewModel>): Class<T> =
        requireNotNull(get(clazz)) as Class<T>

Как и в случае с примером Chili Labs, наша деятельность может напрямую внедрять ViewModel, как и раньше, но теперь не зная, какую реализацию она использует.

class MainActivity : Activity() {
    private lateinit var mainViewModel: MainViewModel
    @Inject
    fun inject(mainViewModel: MainViewModel) {
        this.mainViewModel = mainViewModel
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
    }
}

Нашим фрагментам нужно всего лишь несколько изменений, чтобы они могли внедрить ViewModel, не зная о реализации.

class MainFragment : Fragment() {
    private lateinit var classMap: ViewModelClassMap
    private lateinit var mainViewModel: MainViewModel
    @Inject
    fun inject(classMap: ViewModelClassMap) {
        this.classMap = classMap
    }
    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProviders
            .of(requireActivity())
            .get(classMap.getImplClass<MainViewModel>(MainViewModel::class.java))
    }
}

Теперь мы полностью работаем в абстракциях, разделяя один экземпляр модели ViewModel с учетом жизненного цикла для действия и фрагмента. Можно легко добавить больше фрагментов, которые также будут использовать один и тот же экземпляр ViewModel.

Последнее препятствие возникает, когда мы начинаем писать тесты для этой деятельности. Используя ViewModelClassMap, мы можем достаточно легко заменить нашу реализацию ViewModel заглушкой. Затем проблема заключается в том, чтобы использовать один и тот же экземпляр этой заглушки для действия и фрагментов. Для этого нам просто нужно использовать часть головоломки, которую мы уже разместили.

@Provides
@Singleton
fun viewModelFactory(
        providers: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) = object : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val provider = providers[modelClass] ?: providers
                .asIterable()
                .firstOrNull { it.key.isAssignableFrom(modelClass) }
                ?.value

        return requireNotNull(provider).get() as T
    }
}

В настоящее время наш фрагмент не использует ViewModelProvider.Factory, который Dagger настроен предоставлять нам, потому что в этом нет необходимости. Если мы запустим наше приложение, мы заметим, что фрагмент использует тот же экземпляр ViewModel, что и действие. Но когда мы запускаем тест, мы видим, что активность и фрагмент используют разные экземпляры модели представления. Это связано с тем, что мы предоставляем активность непосредственно экземпляру в нашем тесте, но фрагмент использует ViewModelProviders для получения экземпляра.

Мы можем предотвратить появление у фрагмента другого экземпляра, используя фабрику, которую мы создали для Dagger.

class MainFragment : Fragment() {
    private lateinit var classMap: ViewModelClassMap
    private lateinit var factory: ViewModelProvider.Factory
    private lateinit var mainViewModel: MainViewModel
    @Inject
    fun inject(
        classMap: ViewModelClassMap,
        factory: ViewModelProvider.Factory
    ) {
        this.classMap = classMap
        this.factory = factory
    }
    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mainViewModel = ViewModelProviders
            .of(requireActivity(), factory)
            .get(classMap.getImplClass<MainViewModel>(MainViewModel::class.java))
    }

При тестировании мы создаем фабрику-заглушку, которая предоставляет тот же экземпляр ViewModel, который мы «вводим» в действие.

class MainViewModelStub : MainViewModel() {
    override val rand = MutableLiveData<Int>().apply { 
        value = Random().nextInt()
    }
}
class MainActivityTests {
    private val targetContext = ApplicationProvider.getApplicationContext<Context>()

    @Rule
    @JvmField
    val testActivityRule = ActivityTestRule(MainActivity::class.java, true, false)

    @Rule
    @JvmField
    var androidInjectionRule = AndroidInjectionRule()

    private val classMapStub: ViewModelClassMap = mapOf(MainViewModel::class.java to MainViewModelStub::class.java)
    private val viewModelStub = MainViewModelStub()
    private val factoryStub = object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModelStub as T
    }

@Before
fun setUp() {
    androidInjectionRule.fragmentInjector = AndroidInjector { fragment ->
        when (fragment) {
            is MainFragment -> {
                fragment.inject(classMapStub, factoryStub)
            }
        }
    }

    androidInjectionRule.activityInjector = AndroidInjector { activity ->
        when (activity) {
            is MainActivity -> activity.inject(viewModelStub)
        }
    }
}

@Test
fun test_that_it_works() {
    val activity = MainActivity.newIntent(targetContext)
    testActivityRule.launchActivity(activity)
}

Теперь активность и фрагмент (ы) используют один и тот же экземпляр MainViewModel, и мы можем начать тестирование нашего пользовательского интерфейса!

Полный пример проекта можно найти здесь.

Если у вас есть предложения по улучшению, оставьте комментарий ниже и / или создайте запрос на перенос!