Як використовувати Dagger 2 для ін'єкції ViewModel тих же фрагментів всередині ViewPager


10

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

У мене є ViewPager, який має два екземпляри одного фрагмента (лише незначні зміни для кожної вкладки), і в кожній вкладці я спостерігаю за 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 відділення має те саме додаток, що не користується сферою застосування, і він працює добре.

Будь ласка, повідомте мене, якщо питання не ясно.


Я не розумію, чому ти не використаєш підхід з working_fineфілії? Для чого потрібна область застосування?
azizbekian

@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від А , 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;
  }

The Now 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тому абсолютно добре, що вони відображають однакові дані.


Дякую за відповідь. Це має сенс. Але хіба немає способу виправити це під час використання області застосування?
hushed_voice

Раніше це було моє запитання: навіщо вам використовувати область застосування? Це так, як ви хочете користуватися автомобілем під час підйому на гору, і тепер ви говорите "добре, я розумію, чому я не можу використовувати машину, але як я можу використовувати машину, щоб піднятися на гору?" Ваші наміри не очевидні, поясніть, будь ласка.
azizbekian

Можливо, я помиляюся. Моє сподівання було, що використання області є кращим підходом. Наприклад, наприклад. Якщо у моїй програмі є 2 дії (Логін та Основна), використовуючи 1 користувацьку область для входу та 1 спеціальну область для основного, вилучите зайві екземпляри, поки одна активність активна
hushed_voice

> Моє сподівання було, що використання області є кращим підходом. Це не те, що один краще, ніж інший. Вони вирішують різні проблеми, у кожного є свій приклад використання.
azizbekian

> видалить зайві екземпляри, поки одна активність активна. Неможливо побачити, де з цих "зайвих екземплярів" слід створити. ViewModelстворюється з життєвим циклом діяльності / фрагментом і знищується, як тільки знищується життєвий цикл хостингу. Ви не повинні керувати життєвим циклом / руйнуванням створення ViewModel самостійно, ось що компоненти архітектури роблять для вас як клієнта цього API.
azizbekian

0

Обидва фрагменти отримують оновлення від livedata, оскільки viewpager підтримує обидва фрагменти у відновленому стані. Оскільки вам потрібне оновлення лише для поточного фрагмента, видимого на панелі перегляду, контекст поточного фрагмента визначається активністю хоста, діяльність повинна явно спрямовувати оновлення до потрібного фрагмента.

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

Тепер у активі з'явиться MediatorLiveData, що спостерігає за оригінальними живими даними, які спостерігаються безпосередньо за фрагментами. Кожного разу, коли оригінальні дані живих даних публікують оновлення, вони будуть доставлені до mediatorLivedata, а посередницькі дані в turen надсилають лише значення livedata поточного вибраного фрагмента. Ці живі дані буде отримано з наведеної вище карти.

Код імпульсу виглядатиме так:

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 Впевнений, повернеться до вас.
Вішал Арора
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.