Как использовать Dagger 2 для внедрения ViewModel из тех же фрагментов внутри ViewPager - PullRequest
10 голосов
/ 30 января 2020

Я пытаюсь добавить Dagger 2 в свой проект. Мне удалось ввести ViewModels (компонент архитектуры AndroidX) для моих фрагментов.

У меня есть ViewPager, у которого есть 2 экземпляра одного и того же фрагмента (только незначительное изменение для каждой вкладки), и на каждой вкладке я наблюдаю LiveData, чтобы получить обновление данных изменить (из API).

Проблема заключается в том, что при получении ответа API и обновлении LiveData те же данные в видимом в данный момент фрагменте отправляются наблюдателям на всех вкладках. (я думаю, что это, вероятно, из-за объема ViewModel).

Вот как я наблюдаю за моими данными:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        activityViewModel.expenseList.observe(this, Observer {
            swipeToRefreshLayout.isRefreshing = false
            viewAdapter.setData(it)
        })
    ....
}

Я использую этот класс для предоставления ViewModel s:

class ViewModelProviderFactory @Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) :
    ViewModelProvider.Factory {
    private val creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? = creators
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel?>? = creators!![modelClass]
        if (creator == null) { // if the viewmodel has not been created
// loop through the allowable keys (aka allowed classes with the @ViewModelKey)
            for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel>
                if (modelClass.isAssignableFrom(entry.key!!)) {
                    creator = entry.value
                    break
                }
            }
        }
        // if this is not one of the allowed keys, throw exception
        requireNotNull(creator) { "unknown model class $modelClass" }
        // return the Provider
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }

    companion object {
        private val TAG: String? = "ViewModelProviderFactor"
    }
}

Я связываю свой ViewModel следующим образом:

@Module
abstract class ActivityViewModelModule {
    @MainScope
    @Binds
    @IntoMap
    @ViewModelKey(ActivityViewModel::class)
    abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}

Я использую @ContributesAndroidInjector для своего фрагмента следующим образом:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

И я добавляю эти модули к своему MainActivity подкомпоненту так:

@Module
abstract class ActivityBuilderModule {
...
    @ContributesAndroidInjector(
        modules = [MainViewModelModule::class, ActivityViewModelModule::class,
            AuthModule::class, MainFragmentBuildersModule::class]
    )
    abstract fun contributeMainActivity(): MainActivity
}

Вот мой AppComponent:

@Singleton
@Component(
    modules =
    [AndroidSupportInjectionModule::class,
        ActivityBuilderModule::class,
        ViewModelFactoryModule::class,
        AppModule::class]
)
interface AppComponent : AndroidInjector<SpenmoApplication> {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }
}

Я расширяю DaggerFragment и введите ViewModelProviderFactory следующим образом:

@Inject
lateinit var viewModelFactory: ViewModelProviderFactory

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
....
activityViewModel =
            ViewModelProviders.of(this, viewModelFactory).get(key, ActivityViewModel::class.java)
        activityViewModel.restartFetch(hasReceipt)
}

key будет отличаться для обоих фрагментов.

Как я могу убедиться, что только o bserver текущего фрагмента обновляется.

EDIT 1 ->

Я добавил пример проекта с ошибкой. Похоже, проблема возникает только при добавлении пользовательской области. Пожалуйста, ознакомьтесь с примером проекта здесь: Ссылка Github

master В филиале есть приложение с проблемой. Если вы обновите sh любой вкладки (проведите пальцем, чтобы обновить sh), обновленное значение отобразится на обеих вкладках. Это происходит только тогда, когда я добавляю в него настраиваемую область (@MainScope).

working_fine В филиале есть то же приложение без настраиваемой области и оно работает нормально.

Пожалуйста, дайте мне знать, если вопрос не ясен.

Ответы [ 2 ]

2 голосов
/ 28 марта 2020

Я хочу напомнить исходный вопрос, вот он:

В настоящее время я использую рабочий fine_branch, но я хочу знать, почему при использовании области видимости это нарушается.

Насколько я понимаю, у вас сложилось впечатление, что только из-за того, что вы пытаетесь получить экземпляр ViewModel с использованием разных ключей, вам должны быть предоставлены разные экземпляры ViewModel:

// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)

// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)

Реальность немного другая. Если вы поместите следующий фрагмент журнала, вы увидите, что эти два фрагмента используют точно такой же экземпляр PagerItemViewModel:

Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")

Давайте углубимся и поймем , почему это происходит.

Внутренне ViewModelProvider#get() попытается получить экземпляр PagerItemViewModel от ViewModelStore, который в основном является картой от String до ViewModel.

Когда FirstFragment запрашивает для экземпляра PagerItemViewModel map пусто, следовательно, mFactory.create(modelClass) выполняется, что заканчивается ViewModelProviderFactory. creator.get() в итоге вызывает DoubleCheck со следующим кодом:

  public T get() {
    Object result = instance;
    if (result == UNINITIALIZED) { // 1
      synchronized (this) {
        result = instance;
        if (result == UNINITIALIZED) {
          result = provider.get();
          instance = reentrantCheck(instance, result); // 2
          /* Null out the reference to the provider. We are never going to need it again, so we
           * can make it eligible for GC. */
          provider = null;
        }
      }
    }
    return (T) result;
  }

instance теперь null, следовательно, создается новый экземпляр PagerItemViewModel и сохраняется в instance ( см. // 2).

Теперь точно такая же процедура происходит для SecondFragment:

  • фрагмент запрашивает экземпляр PagerItemViewModel
  • map теперь не пусто, но содержит не экземпляр PagerItemViewModel с ключом false
  • инициируется создание нового экземпляра PagerItemViewModel через mFactory.create(modelClass)
  • Внутри ViewModelProviderFactory выполнение достигает creator.get(), реализация которого DoubleCheck

Теперь ключевой момент. Этот DoubleCheck является тем же экземпляром из DoubleCheck, который использовался для создания ViewModel экземпляра, когда FirstFragment запросил его. Почему это тот же экземпляр? Поскольку вы применили область к методу провайдера.

if (result == UNINITIALIZED) (// 1) оценивается как ложное, и тот же самый экземпляр ViewModel возвращается вызывающей стороне - SecondFragment .

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

0 голосов
/ 31 января 2020

Оба фрагмента получают обновление от liveata, поскольку viewpager сохраняет оба фрагмента в возобновленном состоянии. Поскольку требуется обновление только для текущего фрагмента, видимого в окне просмотра, контекст current фрагмента определяется действием хоста, действие должно явно направлять обновления на нужный фрагмент.

Вам нужно поддерживать карту Fragment в LiveData, содержащую записи для всех фрагментов (убедитесь, что у вас есть идентификатор, который может различать два экземпляра фрагмента одного и того же фрагмента), добавленный в viewpager.

Теперь у действия будет MediatorLiveData, наблюдающий за исходными живыми данными, наблюдаемыми фрагментами напрямую. Всякий раз, когда исходные liveata публикуют обновление, оно будет доставлено mediatorLivedata, а mediatorlivedata в turen будет публиковать значение в liveata текущего выбранного фрагмента. Эти данные будут извлечены из карты выше.

Код impl будет выглядеть как -

class Activity {
    val mapOfFragmentToLiveData<FragmentId, MutableLiveData> = mutableMapOf<>()

    val mediatorLiveData : MediatorLiveData<OriginalData> = object : MediatorLiveData() {
        override fun onChanged(newData : OriginalData) {
           // here get the livedata observed by the  currently selected fragment
           val currentSelectedFragmentLiveData = mapOfFragmentToLiveData.get(viewpager.getSelectedItem())
          // now post the update on this livedata
           currentSelectedFragmentLiveData.value = newData
        }
    }

  fun getOriginalLiveData(fragment : YourFragment) : LiveData<OriginalData> {
     return mapOfFragmentToLiveData.get(fragment) ?: MutableLiveData<OriginalData>().run {
       mapOfFragmentToLiveData.put(fragment, this)
  }
} 

class YourFragment {
    override fun onActivityCreated(bundle : Bundle){
       //get activity and request a livedata 
       getActivity().getOriginalLiveData(this).observe(this, Observer { _newData ->
           // observe here 
})
    }
}
...