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

10

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

У меня есть 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)
        })
    ....
}

Я использую этот класс для обеспечения ViewModels:

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будет отличаться для обоих фрагментов.

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

РЕДАКТИРОВАТЬ 1 ->

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

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

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

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

hushed_voice
источник
Я не понимаю, почему вы не будете использовать подход из working_fineфилиала? Зачем вам нужен прицел?
Азизбекян
@azizbekian В настоящее время я использую рабочую ветвь, но я хочу знать, почему использование видимости сломало бы это.
hushed_voice

Ответы:

1

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

Я сейчас использую рабочий 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от a, ViewModelStoreкоторый в основном является картой Stringдля ViewModel.

Когда FirstFragmentпрошу экземпляр пуст, следовательно , выполняются, который заканчивается . в итоге звонит со следующим кодом:PagerItemViewModelmapmFactory.create(modelClass)ViewModelProviderFactorycreator.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поэтому совершенно нормально, что они отображают одни и те же данные.

azizbekian
источник
Спасибо за ответ. Это имеет смысл. Но разве нет способа исправить это, используя область?
hushed_voice
Это был мой вопрос ранее: зачем вам использовать область? Как будто вы хотите использовать машину при восхождении на гору, и теперь вы говорите: «Хорошо, я понимаю, почему я не могу использовать машину, но как я могу использовать машину, чтобы подняться на гору?» Ваши намерения не очевидны, пожалуйста, уточните.
Азизбекян
Возможно я не прав. Я ожидал, что использование объема является лучшим подходом. Например, Если в моем приложении есть 2 действия («Вход в систему» ​​и «Основное»), использование 1 настраиваемой области для входа в систему и 1 настраиваемой области для основной, удалит ненужные экземпляры, пока активно одно действие
hushed_voice
> Я ожидал, что использование объема - лучший подход. Дело не в том, что одно лучше другого. Они решают разные проблемы, у каждого есть свой вариант использования.
Азизбекян
> удалит ненужные экземпляры, пока активно одно действие. Не видит, где из этих «ненужных экземпляров» следует создать. ViewModelсоздается с жизненным циклом действия / фрагмента и уничтожается, как только уничтожается его жизненный цикл. Вы не должны управлять жизненным циклом / созданием-разрушением ViewModel самостоятельно, это то, что компоненты архитектуры делают для вас как клиента этого API.
Азизбекян
0

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

Вам необходимо сохранить карту 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 
})
    }
}
Вишал Арора
источник
Спасибо за ответ. Я использую FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)так, как viewpager держит оба фрагмента в возобновленном состоянии? Этого не было до того, как я добавил кинжал 2 в проект.
hushed_voice
Я попытаюсь добавить пример проекта с указанным поведением
hushed_voice
Привет, я добавил пример проекта. Можете ли вы проверить это. Я также добавлю вознаграждение за это. (Извините за задержку)
hushed_voice
@hushed_voice Конечно, вернемся к вам.
Вишал Арора