Como usar o Dagger 2 para injetar o ViewModel dos mesmos fragmentos no ViewPager

10

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 LiveDatapara 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 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"
    }
}

Estou ligando meu ViewModelassim:

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

Estou usando @ContributesAndroidInjectorpara o meu fragmento assim:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

E estou adicionando esses módulos ao meu MainActivitysubcomponente 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 DaggerFragmente injetando ViewModelProviderFactoryassim:

@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 keyserá 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

masterramo 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.

hushed_voice
fonte
Não estou entendendo por que você não usa a abordagem do working_finebranch? Por que você precisa do escopo?
azizbekian 28/03
@azizbekian Atualmente, estou usando o ramo de trabalho bem ... mas quero saber, por que o uso do escopo quebraria isso.
hushed_voice 28/03

Respostas:

1

Quero recapitular a pergunta original, eis a seguinte:

Atualmente, estou usando o trabalho fine_branch, mas quero saber por que usar o escopo quebraria isso.

De acordo com meu entendimento, você tem uma impressão de que, apenas porque você está tentando obter uma instância do ViewModeluso de chaves diferentes, deve receber instâncias diferentes de ViewModel:

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

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

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:

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

Vamos nos aprofundar e entender por que isso acontece.

Internamente ViewModelProvider#get(), tentará obter uma instância de PagerItemViewModelde a ViewModelStoreque é basicamente um mapa de Stringpara ViewModel.

Quando FirstFragmentpede uma instância PagerItemViewModeldo mapestá vazia, por conseguinte, mFactory.create(modelClass)é executado, o que acaba no ViewModelProviderFactory. creator.get()acaba chamando DoubleCheckcom o seguinte código:

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

O instanceé agora null, portanto, uma nova instância de PagerItemViewModelé criada e salva em instance(consulte // 2).

Agora, exatamente o mesmo procedimento acontece para SecondFragment:

  • fragmento pede uma instância de PagerItemViewModel
  • mapagora não está vazio, mas não contém uma instância dePagerItemViewModel with keyfalse
  • uma nova instância de PagerItemViewModelé iniciada para ser criada viamFactory.create(modelClass)
  • A ViewModelProviderFactoryexecução interna atinge creator.get()cuja implementação éDoubleCheck

Agora, o momento chave. Esta DoubleChecké a mesma instância de DoubleCheckque foi usado para a criação de ViewModelexemplo, quando FirstFragmentpediu 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 exata ViewModelestá sendo retornada ao chamador - SecondFragment.

Agora, os dois fragmentos estão usando a mesma instância, ViewModelportanto, é perfeitamente bom que eles estejam exibindo os mesmos dados.

azizbekian
fonte
Obrigado pela resposta. Isso faz sentido. Mas não há uma maneira de corrigir isso enquanto estiver usando o escopo?
hushed_voice 29/03
Essa foi a minha pergunta anteriormente: por que você precisa usar o escopo? É como você querer usar um carro ao escalar uma montanha e agora você está dizendo "ok, entendi por que não posso usar um carro, mas como posso usar um carro para escalar uma montanha?" Suas intenções não são óbvias, por favor, esclareça.
azizbekian 29/03
Talvez eu esteja errado. Minha expectativa era que o uso do escopo fosse uma abordagem melhor. Por exemplo. Se houver duas atividades no meu aplicativo (Login e Principal), o uso de 1 escopo personalizado para login e 1 escopo personalizado para main, removerá as instâncias desnecessárias enquanto uma atividade estiver ativa
hushed_voice
> Minha expectativa era que o uso do escopo fosse uma abordagem melhor. Não é que um seja melhor que o outro. Eles estão resolvendo problemas diferentes, cada um com seu caso de uso.
azizbekian 29/03
> removerá as instâncias desnecessárias enquanto uma atividade estiver ativa. Não é possível ver de onde essas "instâncias desnecessárias" devem ser criadas. 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.
azizbekian 29/03
0

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 -

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 
})
    }
}
Vishal Arora
fonte
Obrigado pela resposta. Estou usando, 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.
hushed_voice 31/01
Vou tentar adicionar um projeto de amostra com o referido comportamento
hushed_voice
Ei, eu adicionei um projeto de amostra. Você pode conferir. Também adicionarei uma recompensa por isso. (Desculpe pelo atraso)
hushed_voice 27/03
@hushed_voice Claro que vou voltar para você.
Vishal Arora