วิธีใช้ Dagger 2 เพื่อ Inject ViewModel ของ Fragments ที่เหมือนกันภายใน ViewPager


10

ฉันกำลังพยายามเพิ่ม Dagger 2 ในโครงการของฉัน ฉันสามารถฉีด ViewModels (ส่วนประกอบ AndroidX Architecture) สำหรับชิ้นส่วนของฉัน

ฉันมีViewPager ซึ่งมี 2 อินสแตนซ์ของแฟรกเมนต์เดียวกัน (เฉพาะการเปลี่ยนแปลงเล็กน้อยสำหรับแต่ละแท็บ) และในแต่ละแท็บฉันกำลังเฝ้าดูLiveDataเพื่อรับการอัปเดตเกี่ยวกับการเปลี่ยนแปลงข้อมูล (จาก API)

ปัญหาคือเมื่อการตอบสนอง api มาและอัปเดตLiveDataข้อมูลเดียวกันในส่วนที่มองเห็นได้ในปัจจุบันจะถูกส่งไปยังผู้สังเกตการณ์ในแท็บทั้งหมด (ฉันคิดว่านี่อาจเป็นเพราะขอบเขตของViewModel)

นี่คือวิธีที่ฉันสังเกตข้อมูลของฉัน:

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

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

ฉันใช้คลาสนี้ในการจัดหา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"
    }
}

ฉันผูกพันของฉันViewModelเช่นนี้:

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

ฉันใช้@ContributesAndroidInjectorชิ้นส่วนของฉันเช่นนี้:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

และฉันกำลังเพิ่มโมดูลเหล่านี้ไปยังMainActivityคอมโพเนนต์ย่อยของฉันเช่นนี้:

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

นี่คือของฉัน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
    }
}

ฉันกำลังขยายDaggerFragmentและฉีดViewModelProviderFactoryแบบนี้:

@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จะแตกต่างกันสำหรับทั้งเศษ

ฉันจะมั่นใจได้อย่างไรว่ามีเพียงผู้สังเกตการณ์ของแฟรกเมนต์ปัจจุบันเท่านั้นที่ได้รับการอัปเดต

แก้ไข 1 ->

ฉันได้เพิ่มโครงการตัวอย่างพร้อมข้อผิดพลาด ดูเหมือนว่าปัญหาจะเกิดขึ้นเฉพาะเมื่อมีการเพิ่มขอบเขตที่กำหนดเอง กรุณาตรวจสอบโครงการตัวอย่างที่นี่: ลิงค์ Github

masterสาขามีแอปที่มีปัญหา หากคุณรีเฟรชแท็บใด ๆ (ปัดเพื่อรีเฟรช) ค่าที่อัพเดตจะได้รับการสะท้อนในแท็บทั้งสอง สิ่งนี้จะเกิดขึ้นเมื่อฉันเพิ่มขอบเขตที่กำหนดเองลงในมัน ( @MainScope)

working_fine Branch มีแอพเดียวกันที่ไม่มีขอบเขตที่กำหนดเองและใช้ได้ดี

โปรดแจ้งให้เราทราบหากคำถามไม่ชัดเจน


ฉันไม่เข้าใจว่าทำไมคุณไม่ใช้วิธีการจากworking_fineสาขา ทำไมคุณถึงต้องการขอบเขต?
azizbekian

@azizbekian ฉันกำลังใช้สาขาที่ทำงานได้ดี .. แต่ฉันอยากรู้ว่าทำไมจะใช้ขอบเขตนี้
hushed_voice

คำตอบ:


1

ฉันต้องการสรุปคำถามต้นฉบับนี่คือ:

ขณะนี้ฉันกำลังใช้งานอยู่fine_branchแต่ฉันต้องการทราบว่าเพราะเหตุใดฉันจึงใช้ขอบเขตนี้

ตามความเข้าใจของฉันคุณมีความรู้สึกว่าเพียงเพราะคุณพยายามรับอินสแตนซ์ของการViewModelใช้คีย์ที่แตกต่างกันดังนั้นคุณควรให้อินสแตนซ์ต่าง ๆ ของViewModel:

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

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

ความจริงมันแตกต่างกันเล็กน้อย หากคุณใส่ส่วนการเข้าสู่ระบบต่อไปนี้คุณจะเห็นว่าทั้งสองส่วนกำลังใช้อินสแตนซ์เดียวกันที่แน่นอนของPagerItemViewModel:

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

ดำน้ำ Let 's ในและเข้าใจว่าทำไมเกิดเหตุการณ์นี้

ภายในViewModelProvider#get()จะพยายามที่จะได้รับตัวอย่างของPagerItemViewModelจากViewModelStoreที่เป็นพื้นแผนที่จะStringViewModel

เมื่อFirstFragmentถามว่าสำหรับตัวอย่างของว่างเปล่าจึงจะถูกดำเนินการซึ่งจะสิ้นสุดลงใน สิ้นสุดการโทรด้วยรหัสต่อไปนี้: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;
  }

instanceอยู่ในขณะนี้nullจึงตัวอย่างใหม่PagerItemViewModelถูกสร้างขึ้นและถูกบันทึกไว้ในinstance(ดู // 2)

ตอนนี้กระบวนการเดียวกันนี้เกิดขึ้นกับSecondFragment:

  • ส่วนถามหาตัวอย่างของ PagerItemViewModel
  • mapคือตอนนี้ไม่ว่าง แต่ไม่ได้มีตัวอย่างของPagerItemViewModelที่มีคีย์false
  • มีการPagerItemViewModelเริ่มต้นอินสแตนซ์ใหม่ของผ่านทางmFactory.create(modelClass)
  • ภายในViewModelProviderFactoryการดำเนินการถึงการดำเนินการcreator.get()ซึ่งเป็นDoubleCheck

ตอนนี้ช่วงเวลาสำคัญ นี้DoubleCheckเป็นเช่นเดียวกับของDoubleCheckที่ถูกใช้สำหรับการสร้างViewModelเช่นเมื่อFirstFragmentถามว่า ทำไมมันเป็นเช่นเดียวกัน เนื่องจากคุณใช้ขอบเขตกับวิธีการของผู้ให้บริการ

if (result == UNINITIALIZED)(// 1) การประเมินเป็นเท็จและเช่นเดียวกันที่แน่นอนของViewModelจะถูกส่งกลับไปยังผู้โทร SecondFragment-

ตอนนี้ทั้งสองแฟรกเมนต์กำลังใช้อินสแตนซ์เดียวกันViewModelดังนั้นจึงเป็นการดีที่จะแสดงข้อมูลเดียวกัน


ขอบคุณสำหรับคำตอบ. มันสมเหตุสมผลแล้ว แต่ไม่มีวิธีแก้ไขในขณะที่ใช้ขอบเขตใช่ไหม
hushed_voice

นั่นคือคำถามของฉันก่อนหน้านี้: ทำไมคุณต้องใช้ขอบเขต? มันเหมือนกับว่าคุณต้องการใช้รถยนต์เมื่อปีนภูเขาและตอนนี้คุณกำลังพูดว่า"โอเคฉันเข้าใจว่าทำไมฉันไม่สามารถใช้รถยนต์ได้ แต่ฉันจะใช้รถยนต์เพื่อปีนภูเขาได้อย่างไร" ความตั้งใจของคุณไม่ชัดเจนโปรดชี้แจง
azizbekian

บางทีฉันผิด ความคาดหวังของฉันคือการใช้ขอบเขตเป็นวิธีที่ดีกว่า สำหรับเช่น หากมี 2 กิจกรรมในแอปพลิเคชันของฉัน (เข้าสู่ระบบและหลัก) โดยใช้ 1 ขอบเขตที่กำหนดเองสำหรับการเข้าสู่ระบบและ 1 ขอบเขตที่กำหนดเองสำหรับหลักจะลบอินสแตนซ์ที่ไม่จำเป็นในขณะที่กิจกรรมหนึ่งเปิดใช้งาน
hushed_voice

> ความคาดหวังของฉันคือการใช้ขอบเขตเป็นวิธีที่ดีกว่าไม่ใช่ว่าจะดีกว่าวิธีอื่น พวกเขากำลังแก้ไขปัญหาที่แตกต่างกันแต่ละกรณีมีการใช้งาน
azizbekian

> จะลบอินสแตนซ์ที่ไม่จำเป็นในขณะที่กิจกรรมหนึ่งทำงานอยู่ไม่สามารถดูได้ว่าควรสร้างที่ไหนจาก "อินสแตนซ์ที่ไม่จำเป็น" เหล่านั้น ViewModelถูกสร้างขึ้นด้วยวงจรชีวิตของกิจกรรม / ชิ้นส่วนและถูกทำลายทันทีที่วงจรชีวิตของโฮสติ้งถูกทำลาย คุณไม่ควรจัดการวงจรชีวิต / การสร้างการทำลายของ ViewModel ด้วยตัวคุณเองนั่นคือสิ่งที่องค์ประกอบด้านสถาปัตยกรรมกำลังทำเพื่อคุณในฐานะลูกค้าของ API นั้น
azizbekian

0

ทั้งสองแฟรกเมนต์ได้รับการอัพเดตจาก livesata เนื่องจาก viewpager เก็บทั้งสองแฟรกเมนต์ในสถานะที่กลับมาทำงานต่อ เนื่องจากคุณต้องการการอัพเดตเฉพาะในแฟรกเมนต์ปัจจุบันที่มองเห็นได้ใน viewpager บริบทของแฟรกเมนต์ปัจจุบันถูกกำหนดโดยกิจกรรมโฮสต์กิจกรรมจึงควรชี้นำการอัพเดตไปยังแฟรกเมนต์ที่ต้องการอย่างชัดเจน

คุณต้องรักษาแผนที่ของ Fragment เป็น LiveData ที่มีรายการสำหรับแฟรกเมนต์ทั้งหมด (ตรวจสอบให้แน่ใจว่ามีตัวระบุที่สามารถแยกอินสแตนซ์ของแฟรกเมนต์สองส่วนของแฟรกเมนต์เดียวกัน) เพิ่มลงใน viewpager

ตอนนี้กิจกรรมจะมี MediatorLiveData คอยสังเกตข้อมูลการอยู่อาศัยดั้งเดิมที่ตรวจพบโดยชิ้นส่วนโดยตรง เมื่อใดก็ตามที่ Livesa ดั้งเดิมโพสต์การอัปเดตมันจะถูกส่งไปยัง mediatorLivedata และ Mediatorlivedata ใน Turen จะโพสต์ค่าให้ Livesata ของชิ้นส่วนที่เลือกในปัจจุบันเท่านั้น liveata นี้จะถูกดึงจากแผนที่ด้านบน

รหัส impl ดูเหมือนว่า -

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

ขอบคุณสำหรับคำตอบ. ฉันกำลังใช้FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)ดังนั้นตัวรับชมจะเก็บเศษทั้งสองให้อยู่ในสถานะเริ่มต้นต่อไปอย่างไร สิ่งนี้ไม่ได้เกิดขึ้นก่อนที่ฉันจะเพิ่มกริช 2 เข้าในโครงการ
hushed_voice

ฉันจะลองและเพิ่มโครงการตัวอย่างด้วยพฤติกรรมดังกล่าว
hushed_voice

เฮ้ฉันได้เพิ่มโครงการตัวอย่าง คุณกรุณาตรวจสอบได้ไหม ฉันจะเพิ่มความโปรดปรานสำหรับสิ่งนี้ด้วย (ขออภัยในความล่าช้า)
hushed_voice

@hushed_voice แน่นอนว่าจะได้รับกลับมาหาคุณ
Vishal Arora
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.