Используйте один и тот же экземпляр модели представления в нескольких фрагментах с помощью dagger2

Я использую в своем проекте только dagger2 (не dagger-android). Он отлично работает, чтобы внедрить ViewModel с использованием множественной привязки. Но есть одна проблема с тем, что раньше без dagger2 я использовал тот же экземпляр модели представления, который использовался в активности в нескольких фрагментах (с использованием метода fragment-ktx activityViewModels ()), но теперь, поскольку dagger2 вводит модель представления, он всегда дает новый экземпляр (проверяется с помощью hashCode в каждом фрагменте) модели просмотра для каждого фрагмента, что просто прерывает связь между фрагментами с использованием модели просмотра.

Код фрагмента и модели просмотра приведен ниже:

class MyFragment: Fragment() {
    @Inject lateinit var chartViewModel: ChartViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)
        (activity?.application as MyApp).appComponent.inject(this)
    }

}

//-----ChartViewModel class-----

class ChartViewModel @Inject constructor(private val repository: ChartRepository) : BaseViewModel() {
   //live data code...
}

Вот код для внедрения зависимости модели представления:

//-----ViewModelKey class-----

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

//-----ViewModelFactory class------

@Singleton
@Suppress("UNCHECKED_CAST")
class ViewModelFactory
@Inject constructor(
    private val viewModelMap: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = viewModelMap[modelClass] ?: viewModelMap.asIterable()
            .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
        ?: throw IllegalArgumentException("Unknown ViewModel class $modelClass")

        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

//-----ViewModelModule class-----

@Module
abstract class ViewModelModule {
    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(ChartViewModel::class)
    abstract fun bindChartViewModel(chartViewModel: ChartViewModel): ViewModel
}

Есть ли способ получить один и тот же экземпляр модели представления для нескольких фрагментов, а также в то же время внедрить модель представления во фрагменты. Также есть ли необходимость в методе bindViewModelFactory, поскольку он, кажется, не имеет никакого эффекта в приложении даже без этого метода.

Один обходной путь может заключаться в создании BaseFragment для фрагментов, которые имеют общую модель представления, но он снова будет включать шаблонный код, а также я не большой поклонник BaseFragment / BaseActivity. .

Это сгенерированный код для ChartViewModel, который всегда создает новый экземпляр viewModel:

@SuppressWarnings({
    "unchecked",
    "rawtypes"
})
public final class ChartViewModel_Factory implements Factory<ChartViewModel> {
  private final Provider<ChartRepository> repositoryProvider;

  public ChartViewModel_Factory(Provider<ChartRepository> repositoryProvider) {
    this.repositoryProvider = repositoryProvider;
  }

  @Override
  public ChartViewModel get() {
    return newInstance(repositoryProvider.get());
  }

  public static ChartViewModel_Factory create(Provider<ChartRepository> repositoryProvider) {
    return new ChartViewModel_Factory(repositoryProvider);
  }

  public static ChartViewModel newInstance(ChartRepository repository) {
    return new ChartViewModel(repository);
  }
}

person Deepak Kumar    schedule 23.05.2020    source источник
comment
Просто почему бы вам не попробовать activityViewModels? Вы хотите, чтобы 2 фрагмента имели возможность доступа к одному и тому же экземпляру ViewModel, верно?   -  person Tony    schedule 06.12.2020


Ответы (3)


Проблема в том, что когда вы вводите такую ​​модель просмотра

class MyFragment: Fragment() {
    @Inject lateinit var chartViewModel: ChartViewModel

dagger просто создает новый экземпляр модели просмотра. Магия viewmodel-fragment-lifecycle не происходит, потому что эта viewmodel не находится в viewmodelstore активности / фрагмента и не предоставляется созданной вами viewmodelfactory. Здесь вы можете думать о модели представления как о любом обычном классе. В качестве примера:

class MyFragment: Fragment() {
    @Inject lateinit var anything: AnyClass
}
class AnyClass @Inject constructor(private val repository: ChartRepository) {
   //live data code...
}

Ваша модель просмотра эквивалентна этой AnyClass, потому что модель просмотра не находится в хранилище моделей просмотра и не привязана к жизненному циклу фрагмента / действия.

Есть ли способ достичь одного и того же экземпляра модели представления для нескольких фрагментов, а также одновременно ввести модель представления во фрагменты

Нет. По причинам, перечисленным выше.

Также есть необходимость в методе bindViewModelFactory, поскольку он, похоже, не влияет на приложение даже без этого метода.

Это не имеет никакого эффекта, потому что (я предполагаю, что) вы нигде не используете ViewModelFactory. Поскольку он нигде не упоминается, этот код кинжала для viewmodelfactory бесполезен.

@Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

Вот что делает @binds: 1 2

Поэтому его удаление не влияет на приложение.

Так какое же решение? Вам нужно ввести фабрику во фрагмент / действие и получить экземпляр модели представления, используя фабрику

class MyFragment: Fragment() {
    @Inject lateinit var viewModelFactory: ViewModelFactory

    private val vm: ChartViewModel by lazy {
        ViewModelProvider(X, YourViewModelFactory).get(ChartViewModel::class.java)
    }

Что здесь X? X - это ViewModelStoreOwner. ViewModelStoreOwner - это то, что имеет под собой модели просмотра. ViewModelStoreOwner реализуется активностью и фрагментом. Итак, у вас есть несколько способов создания модели представления:

  1. viewmodel в действии
ViewModelProvider(this, YourViewModelFactory)
  1. модель просмотра во фрагменте
ViewModelProvider(this, YourViewModelFactory)
  1. viewmodel во фрагменте (B) с областью видимости родительского фрагмента (A) и общей для дочерних фрагментов в A
ViewModelProvider(requireParentFragment(), YourViewModelFactory)
  1. viewmodel во фрагменте, ограниченном родительской активностью и разделяемым между фрагментами в рамках действия
ViewModelProvider(requireActivity(), YourViewModelFactory)

Одним из обходных путей может быть создание BaseFragment для фрагментов, которые разделяют общую модель представления, но он снова будет включать шаблонный код, а также я не большой поклонник BaseFragment / BaseActivity.

Да, это действительно плохая идея. Решение состоит в том, чтобы использовать requireParentFragment() и requireActivity() для получения экземпляра модели просмотра. Но вы будете писать то же самое в каждом фрагменте / действии, имеющем модель просмотра. Чтобы избежать этого, вы можете абстрагироваться от этой ViewModelProvider(x, factory) части в базовом классе фрагмента / активности, а также внедрить фабрику в базовые классы, что упростит код вашего дочернего фрагмента / действия следующим образом:

class MyFragment: BaseFragment() {

    private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()
person denvercoder9    schedule 23.05.2020
comment
Спасибо @sonnet, сработало как шарм. Следует упомянуть одну вещь, вместо инициализации модели представления с помощью ViewModelProvider я использую fragment-ktx функцию activityViewModels, которая более чистая. - person Deepak Kumar; 23.05.2020
comment
Да, это еще один способ, эквивалентный 4. Кроме того, если вы используете компонент навигации, вы также можете использовать by navgraphviewmodels, что эквивалентно номеру 3. - person denvercoder9; 23.05.2020
comment
Мне удается реализовать это с помощью ViewModelProvider и lazy, но когда я использую fragment-ktx и viewModels, я не могу обеспечить связь между Activity и Fragment @ denvercoder9. У вас есть идея. В действии: val viewModel : ShopViewModel by viewModels{ viewModelFactory } во фрагменте: private val viewModel: ShopViewModel by viewModels{ requireActivity() viewModelFactory } - person Vasilisfoo; 24.07.2020
comment
Я считаю, что есть функция расширения под названием activityViewModels(). Вы можете использовать этот stackoverflow.com/a/56811245/2235972 - person denvercoder9; 24.07.2020
comment
Я попытался добавить val viewModel: NewViewModelCheckIn с помощью ленивого {ViewModelProvider (this, viewModelFactory) .get (NewViewModelCheckIn :: class.java)} ко всем трем моим фрагментам, которые разделяют модель просмотра, но это не работает - person AndroidDev123; 27.09.2020
comment
Если я установил для всех трех фрагментов значение requireParentFragment (), это сработает. однако, когда я завершаю сценарий использования и запускаю его снова, все данные из предыдущего раза в модели просмотра все еще там. Итак, после завершения трех фрагментов эта модель представления не уничтожается, она сохраняется и сохраняет данные ... должно ли это произойти? - person AndroidDev123; 27.09.2020
comment
@ AndroidDev123, так что предположим, что у вас есть родительский фрагмент A, а под A у вас есть три других дочерних фрагмента B, C и D. Если вы сделаете requireParentFragment() в B, C и D, это означает, что модель просмотра привязана к жизненному циклу фрагмента A, который является родительским для B, C и D. Только когда родительский фрагмент A уничтожается, модель представления уничтожается. - person denvercoder9; 27.09.2020
comment
Привет @ denvercoder9, Спасибо за ответ выше. Это действительно полезно. Я изучаю новую концепцию. В дополнение к вышесказанному, при внедрении ViewModel с Dagger2 меня беспокоит то, что мой метод onClear ViewModel не вызывается, когда его прикрепленный фрагмент уничтожается. Как решить эту проблему. Пожалуйста, направляйте. - person SVK; 28.05.2021
comment
@SVK, вероятно, потому, что вы вставляете модель просмотра во фрагмент. Пожалуйста, посмотрите ответ еще раз. Если вы не связываете lifecycleOwner с моделью представления, тогда модель представления не учитывает жизненный цикл. - person denvercoder9; 28.05.2021

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

FragmentOne

class FragmentOne: Fragment() {

private lateinit var viewmodel: SharedViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewmodel= activity?.run {
        ViewModelProviders.of(this).get(SharedViewModel::class.java)
    } : throw Exception("Invalid Activity")
  }
}

FragmentTwo

class FragmentTwo: Fragment() {

private lateinit var viewmodel: SharedViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewmodel= activity?.run {
        ViewModelProviders.of(this).get(SharedViewModel::class.java)
    } ?: throw Exception("Invalid Activity")

 }
}
person Mohammed Alaa    schedule 23.05.2020
comment
Спасибо, но я хочу ввести модель представления с помощью кинжала. - person Deepak Kumar; 23.05.2020
comment
о, я нашел это, я думаю, что вы можете применить ту же концепцию - person Mohammed Alaa; 23.05.2020
comment
Спасибо! См. Также аналогичное решение: blog.mindorks.com/. - person CoolMind; 31.03.2021

Добавьте вашу ViewModel как PostListViewModel внутри ViewModelModule:

@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}

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

@Module
abstract class ViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(PostListViewModel::class)
    internal abstract fun postListViewModel(viewModel: PostListViewModel): ViewModel

    //Add more ViewModels here
}

В конце, наша активность будет внедрена ViewModelProvider.Factory, и она будет передана вprivate val viewModel: PostListViewModel by viewModels { viewModelFactory }

class PostListActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory
    private val viewModel: PostListViewModel by viewModels { viewModelFactory }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post_list)
        getAppInjector().inject(this)
        viewModel.posts.observe(this, Observer(::updatePosts))
    }

    //...
}

Дополнительные сведения см. В этом сообщении: Inject ViewModel with Dagger2 и Проверить github

person Krishna Sony    schedule 23.05.2020
comment
Спасибо, но я хочу ввести модель представления с помощью кинжала. - person Deepak Kumar; 23.05.2020
comment
Это человек, вводящий Dagger, вот как вы вводите viewModel с помощью Dagger2. Проверьте ссылку, которую я упомянул, и прочтите ее - person Krishna Sony; 23.05.2020
comment
Это не работает, по-прежнему получается другой экземпляр viewmodel, а также ViewModelProviders устарел. И если мы не можем внедрить модель просмотра с помощью инъекции поля, тогда было бы лучше остановить DI на метке репозитория в случае общей модели просмотра и просто использовать ручное создание экземпляра репозитория в модели просмотра и использовать viewModels или activityViewModels (из библиотеки ktx) для создать экземпляр модели представления в активности / фрагментах. - person Deepak Kumar; 23.05.2020
comment
да ты прав. это устарело Я отредактировал ответ. использовать ленивую инъекцию. Проверьте эту ссылку proandroiddev.com/ это действительно поможет вам @deepakkumar - person Krishna Sony; 23.05.2020