Estou tentando adicionar o Dagger 2 ao meu projeto. Consegui injetar ViewModels (componente AndroidX Architecture) nos meus fragmentos.
Eu tenho um ViewPager que possui 2 instâncias do mesmo fragmento (apenas uma pequena alteração para cada guia) e, em cada guia, estou observando um LiveData
para ser atualizado sobre a alteração de dados (da API).
O problema é que, quando a resposta da API chega e atualiza LiveData
, os mesmos dados no fragmento visível no momento estão sendo enviados aos observadores em todas as guias. (Eu acho que isso é provavelmente devido ao escopo do ViewModel
).
É assim que estou observando meus dados:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activityViewModel.expenseList.observe(this, Observer {
swipeToRefreshLayout.isRefreshing = false
viewAdapter.setData(it)
})
....
}
Estou usando esta classe para fornecer 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"
}
}
Estou ligando meu ViewModel
assim:
@Module
abstract class ActivityViewModelModule {
@MainScope
@Binds
@IntoMap
@ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
Estou usando @ContributesAndroidInjector
para o meu fragmento assim:
@Module
abstract class MainFragmentBuildersModule {
@ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
E estou adicionando esses módulos ao meu MainActivity
subcomponente assim:
@Module
abstract class ActivityBuilderModule {
...
@ContributesAndroidInjector(
modules = [MainViewModelModule::class, ActivityViewModelModule::class,
AuthModule::class, MainFragmentBuildersModule::class]
)
abstract fun contributeMainActivity(): MainActivity
}
Aqui está o meu 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
}
}
Estou estendendo DaggerFragment
e injetando ViewModelProviderFactory
assim:
@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)
}
o key
será diferente para os dois fragmentos.
Como posso ter certeza de que apenas o observador do fragmento atual está sendo atualizado.
EDIÇÃO 1 ->
Eu adicionei um projeto de amostra com o erro. Parece que o problema está acontecendo apenas quando um escopo personalizado é adicionado. Confira o projeto de amostra aqui: link do Github
master
ramo tem o aplicativo com o problema. Se você atualizar qualquer guia (deslize para atualizar), o valor atualizado será refletido nas duas guias. Isso só acontece quando adiciono um escopo personalizado a ele ( @MainScope
).
working_fine
O ramo tem o mesmo aplicativo sem escopo personalizado e está funcionando bem.
Informe-me se a pergunta não estiver clara.
fonte
working_fine
branch? Por que você precisa do escopo?Respostas:
Quero recapitular a pergunta original, eis a seguinte:
De acordo com meu entendimento, você tem uma impressão de que, apenas porque você está tentando obter uma instância do
ViewModel
uso de chaves diferentes, deve receber instâncias diferentes deViewModel
:A realidade é um pouco diferente. Se você colocar o seguinte fragmento de logon, verá que esses dois fragmentos estão usando exatamente a mesma instância de
PagerItemViewModel
:Vamos nos aprofundar e entender por que isso acontece.
Internamente
ViewModelProvider#get()
, tentará obter uma instância dePagerItemViewModel
de aViewModelStore
que é basicamente um mapa deString
paraViewModel
.Quando
FirstFragment
pede uma instânciaPagerItemViewModel
domap
está vazia, por conseguinte,mFactory.create(modelClass)
é executado, o que acaba noViewModelProviderFactory
.creator.get()
acaba chamandoDoubleCheck
com o seguinte código:O
instance
é agoranull
, portanto, uma nova instância dePagerItemViewModel
é criada e salva eminstance
(consulte // 2).Agora, exatamente o mesmo procedimento acontece para
SecondFragment
:PagerItemViewModel
map
agora não está vazio, mas não contém uma instância dePagerItemViewModel
with keyfalse
PagerItemViewModel
é iniciada para ser criada viamFactory.create(modelClass)
ViewModelProviderFactory
execução interna atingecreator.get()
cuja implementação éDoubleCheck
Agora, o momento chave. Esta
DoubleCheck
é a mesma instância deDoubleCheck
que foi usado para a criação deViewModel
exemplo, quandoFirstFragment
pediu para ele. Por que é a mesma instância? Porque você aplicou um escopo ao método do provedor.O
if (result == UNINITIALIZED)
(// 1) está sendo avaliado como falso e a mesma instância exataViewModel
está sendo retornada ao chamador -SecondFragment
.Agora, os dois fragmentos estão usando a mesma instância,
ViewModel
portanto, é perfeitamente bom que eles estejam exibindo os mesmos dados.fonte
ViewModel
é criado com o ciclo de vida da atividade / fragmento e é destruído assim que é destruído. Você não deve gerenciar o ciclo de vida / destruição de criação do ViewModel por conta própria, é isso que os componentes da arquitetura estão fazendo por você como cliente dessa API.Ambos os fragmentos recebem a atualização de livedata porque o viewpager mantém ambos os fragmentos no estado retomado. Como você exige a atualização apenas no fragmento atual visível no viewpager, o contexto do atual fragmento é definido pela atividade do host, a atividade deve direcionar explicitamente as atualizações para o fragmento desejado.
Você precisa manter um mapa de Fragment to LiveData contendo entradas para todos os fragmentos (certifique-se de ter um identificador que possa diferenciar duas instâncias de fragmento do mesmo fragmento) adicionado ao viewpager.
Agora a atividade terá um MediatorLiveData observando os dados originais observados diretamente pelos fragmentos. Sempre que o liveata original postar uma atualização, ele será entregue ao mediadorLivedata e o mediadorlivedata in turen somente postará o valor no liveData do fragmento selecionado atual. Esses dados vivos serão recuperados do mapa acima.
O código impl ficaria parecido com -
fonte
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
então como o viewpager mantém os dois fragmentos no estado retomado? Isso não estava acontecendo antes de eu adicionar a adaga 2 ao projeto.