Jak używać Dagger 2 do wstrzykiwania ViewModel tych samych fragmentów w ViewPager


10

Próbuję dodać Dagger 2 do mojego projektu. Udało mi się wstrzyknąć ViewModels (komponent architektury AndroidX) dla moich fragmentów.

Mam ViewPager, który ma 2 instancje tego samego fragmentu (tylko niewielka zmiana dla każdej karty) i na każdej karcie obserwuję, LiveDataaby uzyskać aktualizację dotyczącą zmiany danych (z API).

Problem polega na tym, że gdy przychodzi odpowiedź API i aktualizuje LiveData, te same dane w obecnie widocznym fragmencie są wysyłane do obserwatorów we wszystkich zakładkach. (Myślę, że jest to prawdopodobnie ze względu na zakres ViewModel).

Oto jak obserwuję moje dane:

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

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

Korzystam z tej klasy, aby zapewnić 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"
    }
}

Wiążę moje w ViewModelten sposób:

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

Korzystam @ContributesAndroidInjectorz mojego fragmentu w następujący sposób:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

I dodaję te moduły do ​​mojego MainActivitypodskładnika w następujący sposób:

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

Oto moje 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
    }
}

Rozciągam DaggerFragmenti wstrzykuję w ViewModelProviderFactoryten sposób:

@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)
}

keybędą różne dla obu tych fragmentów.

Jak mogę się upewnić, że tylko obserwator bieżącego fragmentu jest aktualizowany.

EDYCJA 1 ->

Dodałem przykładowy projekt z błędem. Wygląda na to, że problem występuje tylko po dodaniu zakresu niestandardowego. Sprawdź przykładowy projekt tutaj: link Github

masteroddział ma aplikację z problemem. Po odświeżeniu dowolnej karty (przeciągnij, aby odświeżyć) zaktualizowana wartość zostanie odzwierciedlona na obu kartach. Dzieje się tak tylko wtedy, gdy dodam do niego niestandardowy zakres ( @MainScope).

working_fine oddział ma tę samą aplikację bez niestandardowego zakresu i działa dobrze.

Daj mi znać, jeśli pytanie nie jest jasne.


Nie rozumiem, dlaczego nie skorzystasz z podejścia z working_fineoddziału? Dlaczego potrzebujesz lunety?
azizbekian

@azizbekian Obecnie używam działającej gałęzi dobrze .. ale chcę wiedzieć, dlaczego użycie zakresu mogłoby to zepsuć.
hushed_voice

Odpowiedzi:


1

Chcę podsumować oryginalne pytanie, oto:

Obecnie używam działającego fine_branch, ale chcę wiedzieć, dlaczego użycie zakresu mogłoby to zepsuć.

Zgodnie z moim rozumieniem masz wrażenie, że tylko dlatego, że próbujesz uzyskać instancję ViewModelprzy użyciu różnych kluczy, powinieneś otrzymać różne instancje ViewModel:

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

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

Rzeczywistość jest nieco inna. Jeśli wstawisz następujący fragment dziennika, zobaczysz, że te dwa fragmenty używają dokładnie tego samego wystąpienia PagerItemViewModel:

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

Zanurzmy się i zrozummy, dlaczego tak się dzieje.

Wewnętrznie ViewModelProvider#get()spróbuje uzyskać instancję PagerItemViewModelz, ViewModelStorektóra jest w zasadzie mapą Stringdo ViewModel.

W przypadku FirstFragmentprosi o wystąpieniu jest pusta, a więc są wykonane, który kończy się . kończy dzwonienie z następującym kodem: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;
  }

instanceJest teraz null, stąd nowa instancja PagerItemViewModeljest tworzona i jest zapisany w instance(patrz // 2).

Teraz dokładnie taka sama procedura ma miejsce dla SecondFragment:

  • fragment prosi o instancję PagerItemViewModel
  • mapteraz nie jest pusty, ale nie zawiera instancji PagerItemViewModelz kluczemfalse
  • nowa instancja PagerItemViewModelzostanie zainicjowana do utworzenia za pośrednictwemmFactory.create(modelClass)
  • ViewModelProviderFactoryWykonanie wewnętrzne dociera do creator.get()którego wykonaniaDoubleCheck

Teraz kluczowy moment. To DoubleCheckjest ta sama instancja o DoubleCheckktóry został użyty do tworzenia ViewModelinstancji, gdy FirstFragmentpoprosił o nią. Dlaczego to ta sama instancja? Ponieważ zastosowałeś zakres do metody dostawcy.

if (result == UNINITIALIZED)(// 1) ocenia się fałszywy i dokładnie w tym samym wystąpienie ViewModeljest zwrócony do wywołującego - SecondFragment.

Teraz oba fragmenty używają tej samej instancji, ViewModeldlatego doskonale jest, że wyświetlają te same dane.


Dziękuję za odpowiedź. To ma sens. Ale czy nie ma sposobu, aby to naprawić podczas korzystania z zakresu?
hushed_voice

To było moje pytanie wcześniej: dlaczego potrzebujesz korzystać z lunety? To tak, jakbyś chciał korzystać z samochodu podczas wspinaczki na górę, a teraz mówisz „ok, rozumiem, dlaczego nie mogę korzystać z samochodu, ale jak mogę użyć samochodu, aby wspiąć się na górę?” Twoje intencje nie są oczywiste, proszę wyjaśnij.
azizbekian

Może się mylę. Oczekiwałem, że użycie zakresu jest lepszym podejściem. Na przykład Jeśli w mojej aplikacji są 2 aktywności (logowanie i główna), użycie 1 niestandardowego zakresu dla logowania i 1 niestandardowego zakresu dla głównego usunie niepotrzebne instancje, gdy jedna aktywność jest aktywna
hushed_voice

> Oczekiwałem, że użycie lunety jest lepszym podejściem Nie chodzi o to, że jedno jest lepsze od drugiego. Rozwiązują różne problemy, każdy ma swój przypadek użycia.
azizbekian

> usunie niepotrzebne instancje, gdy jedno działanie jest aktywne Nie widać, skąd z tych „niepotrzebnych instancji” należy utworzyć. ViewModeljest tworzony z cyklem życia działania / fragmentu i jest niszczony, gdy tylko cykl życia hosta zostanie zniszczony. Nie powinieneś samodzielnie zarządzać cyklem życia / niszczenia kreacji ViewModel, to właśnie robią dla ciebie komponenty architektury jako klient tego API.
azizbekian

0

Oba fragmenty otrzymują aktualizację z Liveata, ponieważ viewpager utrzymuje oba fragmenty w stanie wznowionym. Ponieważ wymagana jest aktualizacja tylko dla bieżącego fragmentu widocznego w przeglądarce, kontekst bieżącego fragmentu jest zdefiniowany przez działanie hosta, działanie powinno wyraźnie kierować aktualizacje do żądanego fragmentu.

Musisz utrzymywać mapę Fragmentu na LiveData zawierającą wpisy dla wszystkich fragmentów (upewnij się, że masz identyfikator, który może odróżnić dwa wystąpienia tego samego fragmentu) dodanego do przeglądarki.

Teraz działanie będzie zawierało MediatorLiveData obserwujące pierwotne żywe obserwowane bezpośrednio przez fragmenty. Ilekroć oryginalna Liveata opublikuje aktualizację, zostanie ona dostarczona do mediatorLivedata, a mediatorlivedata w turen opublikuje tylko wartość do liveata aktualnie wybranego fragmentu. Te dane zostaną pobrane z powyższej mapy.

Kod impl mógłby wyglądać następująco:

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 
})
    }
}

Dziękuję za odpowiedź. Używam, FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)więc w jaki sposób viewer utrzymuje oba fragmenty w stanie wznowionym? Tak się nie stało, zanim dodałem sztylet 2 do projektu.
hushed_voice

Spróbuję dodać przykładowy projekt z tym zachowaniem
hushed_voice

Hej, dodałem przykładowy projekt. Czy możesz to sprawdzić? Dodam również nagrodę za to. (Przepraszam za opóźnienie)
hushed_voice

@hushed_voice Na pewno się odezwie.
Vishal Arora
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.