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ę, LiveData
aby 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ć 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"
}
}
Wiążę moje w ViewModel
ten sposób:
@Module
abstract class ActivityViewModelModule {
@MainScope
@Binds
@IntoMap
@ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
Korzystam @ContributesAndroidInjector
z mojego fragmentu w następujący sposób:
@Module
abstract class MainFragmentBuildersModule {
@ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
I dodaję te moduły do mojego MainActivity
podskł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 DaggerFragment
i wstrzykuję w ViewModelProviderFactory
ten 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)
}
key
bę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
master
oddział 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.
źródło
working_fine
oddziału? Dlaczego potrzebujesz lunety?Odpowiedzi:
Chcę podsumować oryginalne pytanie, oto:
Zgodnie z moim rozumieniem masz wrażenie, że tylko dlatego, że próbujesz uzyskać instancję
ViewModel
przy użyciu różnych kluczy, powinieneś otrzymać różne instancjeViewModel
: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
:Zanurzmy się i zrozummy, dlaczego tak się dzieje.
Wewnętrznie
ViewModelProvider#get()
spróbuje uzyskać instancjęPagerItemViewModel
z,ViewModelStore
która jest w zasadzie mapąString
doViewModel
.W przypadku
FirstFragment
prosi o wystąpieniu jest pusta, a więc są wykonane, który kończy się . kończy dzwonienie z następującym kodem:PagerItemViewModel
map
mFactory.create(modelClass)
ViewModelProviderFactory
creator.get()
DoubleCheck
instance
Jest teraznull
, stąd nowa instancjaPagerItemViewModel
jest tworzona i jest zapisany winstance
(patrz // 2).Teraz dokładnie taka sama procedura ma miejsce dla
SecondFragment
:PagerItemViewModel
map
teraz nie jest pusty, ale nie zawiera instancjiPagerItemViewModel
z kluczemfalse
PagerItemViewModel
zostanie zainicjowana do utworzenia za pośrednictwemmFactory.create(modelClass)
ViewModelProviderFactory
Wykonanie wewnętrzne dociera docreator.get()
którego wykonaniaDoubleCheck
Teraz kluczowy moment. To
DoubleCheck
jest ta sama instancja oDoubleCheck
który został użyty do tworzeniaViewModel
instancji, gdyFirstFragment
poprosił 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ąpienieViewModel
jest zwrócony do wywołującego -SecondFragment
.Teraz oba fragmenty używają tej samej instancji,
ViewModel
dlatego doskonale jest, że wyświetlają te same dane.źródło
ViewModel
jest 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.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:
źródło
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.