Cara menggunakan Dagger 2 untuk Menyuntikkan ViewModel dari Fragmen yang sama di dalam ViewPager

10

Saya mencoba menambahkan Belati 2 ke proyek saya. Saya dapat menyuntikkan ViewModels (komponen Arsitektur AndroidX) untuk fragmen saya.

Saya memiliki ViewPager yang memiliki 2 contoh fragmen yang sama (Hanya sedikit perubahan untuk setiap tab) dan di setiap tab, saya mengamati LiveDatauntuk mendapatkan pembaruan tentang perubahan data (dari API).

Masalahnya adalah ketika respons api datang dan memperbarui LiveData, data yang sama dalam fragmen yang saat ini terlihat dikirim ke pengamat di semua tab. (Saya pikir ini mungkin karena ruang lingkupnya ViewModel).

Inilah cara saya mengamati data saya:

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

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

Saya menggunakan kelas ini untuk menyediakan 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"
    }
}

Saya mengikat saya ViewModelseperti ini:

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

Saya menggunakan @ContributesAndroidInjectoruntuk fragmen saya seperti ini:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

Dan saya menambahkan modul ini ke MainActivitysubkomponen saya seperti ini:

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

Ini milik saya 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
    }
}

Saya memperpanjang DaggerFragmentdan menyuntikkan ViewModelProviderFactoryseperti ini:

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

yang keyakan berbeda untuk kedua fragmen.

Bagaimana saya bisa memastikan bahwa hanya pengamat fragmen saat ini yang diperbarui.

EDIT 1 ->

Saya telah menambahkan proyek sampel dengan kesalahan. Sepertinya masalah hanya terjadi ketika ruang lingkup kustom ditambahkan. Silakan periksa contoh proyek di sini: Tautan Github

mastercabang memiliki aplikasi dengan masalah ini. Jika Anda me-refresh tab apa pun (gesek untuk menyegarkan) nilai yang diperbarui tercermin di kedua tab. Ini hanya terjadi ketika saya menambahkan ruang lingkup kustom untuk itu ( @MainScope).

working_fine cabang memiliki aplikasi yang sama tanpa ruang lingkup khusus dan berfungsi dengan baik.

Tolong beri tahu saya jika pertanyaannya tidak jelas.

hushed_voice
sumber
Saya tidak mengerti mengapa Anda tidak menggunakan pendekatan dari working_finecabang? Mengapa Anda membutuhkan ruang lingkup?
azizbekian
@azizbekian Saat ini saya menggunakan cabang denda yang berfungsi .. tapi saya ingin tahu, mengapa menggunakan lingkup mematahkan ini.
hushed_voice

Jawaban:

1

Saya ingin rekap pertanyaan awal, ini dia:

Saat ini saya menggunakan kerja fine_branch, tetapi saya ingin tahu, mengapa menggunakan lingkup istirahat ini.

Sesuai pemahaman saya Anda memiliki kesan, bahwa hanya karena Anda mencoba untuk mendapatkan contoh ViewModelmenggunakan kunci yang berbeda, maka Anda harus diberikan contoh berbeda dari ViewModel:

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

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

Kenyataannya, sedikit berbeda. Jika Anda memasukkan fragmen log berikut, Anda akan melihat bahwa kedua fragmen tersebut menggunakan instance yang sama persis PagerItemViewModel:

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

Mari selami dan mengerti mengapa ini terjadi.

Internal ViewModelProvider#get()akan mencoba untuk mendapatkan sebuah instance dari PagerItemViewModeldari ViewModelStoreyang pada dasarnya merupakan peta Stringuntuk ViewModel.

Ketika FirstFragmentmeminta contoh PagerItemViewModelyang mapkosong, maka mFactory.create(modelClass)dijalankan, yang berakhir di ViewModelProviderFactory. creator.get()akhirnya memanggil DoubleCheckdengan kode berikut:

  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 instancesekarang null, maka contoh baru dari PagerItemViewModeldibuat dan disimpan dalam instance(lihat // 2).

Sekarang prosedur yang sama persis terjadi untuk SecondFragment:

  • fragmen meminta instance dari PagerItemViewModel
  • mapsekarang tidak kosong, tetapi tidak mengandung instance PagerItemViewModeldengan kuncifalse
  • contoh baru PagerItemViewModeldimulai untuk dibuat melaluimFactory.create(modelClass)
  • ViewModelProviderFactoryEksekusi di dalam mencapai creator.get()yang implementasinyaDoubleCheck

Sekarang, momen kuncinya. Ini DoubleCheckadalah contoh yang sama dari DoubleCheckyang digunakan untuk membuat ViewModelinstance ketika FirstFragmentdiminta untuk itu. Mengapa itu contoh yang sama? Karena Anda telah menerapkan cakupan pada metode penyedia.

The if (result == UNINITIALIZED)(// 1) sedang mengevaluasi ke false dan contoh yang sama persis ViewModeldikembalikan ke pemanggil - SecondFragment.

Sekarang, kedua fragmen tersebut menggunakan contoh yang sama sehingga sama-sama ViewModelbaik bahwa mereka menampilkan data yang sama.

azizbekian
sumber
Terima kasih atas jawabannya. Ini masuk akal. Tetapi tidak adakah cara untuk memperbaikinya saat menggunakan cakupan?
hushed_voice
Itu adalah pertanyaan saya sebelumnya: mengapa Anda perlu menggunakan cakupan? Sepertinya Anda ingin menggunakan mobil saat mendaki gunung dan sekarang Anda berkata, "Baik, saya mengerti mengapa saya tidak bisa menggunakan mobil, tetapi bagaimana saya bisa menggunakan mobil untuk mendaki gunung?" Niat Anda tidak jelas, mohon jelaskan.
azizbekian
Mungkin saya salah. Harapan saya adalah menggunakan lingkup adalah pendekatan yang lebih baik. Untuk misalnya. Jika ada 2 aktivitas dalam aplikasi saya (Masuk dan Utama), menggunakan 1 ruang lingkup khusus untuk masuk dan 1 ruang khusus untuk utama, akan menghapus instance yang tidak perlu saat satu aktivitas aktif
hushed_voice
> Harapan saya adalah menggunakan lingkup adalah pendekatan yang lebih baik. Bukan berarti yang satu lebih baik dari yang lain. Mereka memecahkan masalah yang berbeda, masing-masing memiliki kasus penggunaan.
azizbekian
> akan menghapus instance yang tidak perlu saat satu aktivitas aktif Tidak dapat melihat dari mana "instance yang tidak perlu" harus dibuat. ViewModeldibuat dengan siklus aktivitas / fragmen dan dihancurkan segera setelah siklus hosting itu dihancurkan. Anda seharusnya tidak mengelola siklus hidup / penghancuran ViewModel sendiri, itulah yang dilakukan komponen arsitektur untuk Anda sebagai klien API itu.
azizbekian
0

Kedua fragmen menerima pembaruan dari livedata karena viewpager menjaga kedua fragmen dalam keadaan dilanjutkan. Karena Anda memerlukan pembaruan hanya pada fragmen saat ini yang terlihat di viewpager, konteks fragmen saat ini ditentukan oleh aktivitas host, aktivitas tersebut harus secara eksplisit mengarahkan pembaruan ke fragmen yang diinginkan.

Anda perlu mempertahankan peta Fragmen ke LiveData yang berisi entri untuk semua fragmen (pastikan memiliki pengidentifikasi yang dapat membedakan dua instance fragmen dari fragmen yang sama) ditambahkan ke viewpager.

Sekarang aktivitas akan memiliki MediatorLiveData yang mengamati livata asli yang diamati oleh fragmen secara langsung. Setiap kali livedata asli memposting pembaruan, itu akan dikirim ke mediatorLivedata dan mediatorlivedata di turen hanya akan memposting nilai ke livedata dari fragmen yang dipilih saat ini. Livedata ini akan diambil dari peta di atas.

Implan kode akan terlihat seperti -

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
sumber
Terima kasih atas jawabannya. Saya menggunakan FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)jadi bagaimana viewpager menjaga kedua fragmen dalam keadaan dilanjutkan? Ini tidak terjadi sebelum saya menambahkan belati 2 ke proyek.
hushed_voice
Saya akan mencoba dan menambahkan proyek sampel dengan perilaku tersebut
hushed_voice
Hei, saya telah menambahkan proyek sampel. Bisakah Anda memeriksanya. Saya juga akan menambahkan hadiah untuk ini. (Maaf atas keterlambatannya)
hushed_voice
@hushed_voice Tentu akan membalas Anda.
Vishal Arora