Cách sử dụng Dagger 2 để tiêm ViewModel của cùng một đoạn trong ViewPager


10

Tôi đang cố gắng thêm Dagger 2 vào dự án của mình. Tôi đã có thể tiêm ViewModels (thành phần Kiến trúc AndroidX) cho các mảnh vỡ của mình.

Tôi có một ViewPager có 2 phiên bản của cùng một đoạn (Chỉ một thay đổi nhỏ cho mỗi tab) và trong mỗi tab, tôi đang quan sát một LiveDatađể được cập nhật về thay đổi dữ liệu (từ API).

Vấn đề là khi phản hồi api đến và cập nhật LiveData, cùng một dữ liệu trong đoạn hiện có thể nhìn thấy được gửi đến người quan sát trong tất cả các tab. (Tôi nghĩ rằng điều này có lẽ là do phạm vi của ViewModel).

Đây là cách tôi đang quan sát dữ liệu của mình:

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

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

Tôi đang sử dụng lớp này để cung cấp 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"
    }
}

Tôi ràng buộc ViewModelnhư thế này:

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

Tôi đang sử dụng @ContributesAndroidInjectorcho đoạn của tôi như thế này:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

Và tôi đang thêm các mô-đun này vào thành MainActivityphần con của mình như thế này:

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

Đây là 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
    }
}

Tôi đang gia hạn DaggerFragmentvà tiêm ViewModelProviderFactorynhư thế này:

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

những keysẽ khác nhau cho cả các mảnh vỡ.

Làm thế nào tôi có thể chắc chắn rằng chỉ người quan sát của đoạn hiện tại mới được cập nhật.

CHỈNH SỬA 1 ->

Tôi đã thêm một dự án mẫu với lỗi. Có vẻ như vấn đề chỉ xảy ra khi một phạm vi tùy chỉnh được thêm vào. Vui lòng kiểm tra dự án mẫu tại đây: Github link

masterchi nhánh có ứng dụng với vấn đề. Nếu bạn làm mới bất kỳ tab nào (vuốt để làm mới), giá trị cập nhật sẽ được phản ánh trong cả hai tab. Điều này chỉ xảy ra khi tôi thêm một phạm vi tùy chỉnh cho nó ( @MainScope).

working_fine chi nhánh có cùng một ứng dụng không có phạm vi tùy chỉnh và nó hoạt động tốt.

Xin vui lòng cho tôi biết nếu câu hỏi không rõ ràng.


Tôi không hiểu tại sao bạn sẽ không sử dụng cách tiếp cận từ working_finechi nhánh? Tại sao bạn cần phạm vi?
azizbekian

@azizbekian Tôi hiện đang sử dụng chi nhánh hoạt động tốt .. nhưng tôi muốn biết, tại sao việc sử dụng phạm vi sẽ phá vỡ điều này.
hushed_voice

Câu trả lời:


1

Tôi muốn tóm tắt lại câu hỏi ban đầu, đây là:

Tôi hiện đang sử dụng công việc fine_branch, nhưng tôi muốn biết, tại sao việc sử dụng phạm vi sẽ phá vỡ điều này.

Theo hiểu biết của tôi, bạn có một ấn tượng, rằng chỉ vì bạn đang cố lấy một thể hiện của ViewModelviệc sử dụng các khóa khác nhau, nên bạn sẽ được cung cấp các trường hợp khác nhau về ViewModel:

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

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

Thực tế, có một chút khác biệt. Nếu bạn đặt nhật ký sau vào đoạn, bạn sẽ thấy rằng hai đoạn đó đang sử dụng cùng một ví dụ PagerItemViewModel:

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

Hãy đi sâu vào và hiểu tại sao điều này xảy ra.

Trong nội bộ ViewModelProvider#get()sẽ cố gắng để có được một thể hiện của PagerItemViewModeltừ ViewModelStoređó là cơ bản bản đồ về Stringđến ViewModel.

Khi FirstFragmentyêu cầu một thể hiện của PagerItemViewModelsự maptrống rỗng, vì thếmFactory.create(modelClass) được thực thi, mà kết thúc trong ViewModelProviderFactory. creator.get()kết thúc cuộc gọi DoubleCheckvới mã sau:

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

Hiện instancetại null, do đó một phiên bản mới PagerItemViewModelđược tạo và được lưu trong instance(xem // 2).

Bây giờ quy trình chính xác tương tự xảy ra cho SecondFragment:

  • đoạn yêu cầu một ví dụ về PagerItemViewModel
  • mapbây giờ không trống, nhưng không chứa một thể hiện của PagerItemViewModelkhóafalse
  • một ví dụ mới của PagerItemViewModel được bắt đầu được tạo thông quamFactory.create(modelClass)
  • Bên trong ViewModelProviderFactorythực hiện đạtcreator.get() thực hiện của aiDoubleCheck

Bây giờ, thời điểm quan trọng. Đây DoubleChecktrường hợp tương tự của DoubleCheckđã được sử dụng để tạoViewModel Ví dụ khi FirstFragmentđược hỏi cho nó. Tại sao nó là ví dụ tương tự? Bởi vì bạn đã áp dụng một phạm vi cho phương thức nhà cung cấp.

Các if (result == UNINITIALIZED)(// 1) được đánh giá là false và cùng một ví dụ chính xác củaViewModel đang được trả lại cho người gọi - SecondFragment.

Bây giờ, cả hai mảnh đang sử dụng cùng một ví dụ ViewModeldo đó hoàn toàn ổn khi chúng hiển thị cùng một dữ liệu.


Cảm ơn câu trả lời. Điều này thật ý nghĩa. Nhưng không có cách nào để khắc phục điều này trong khi sử dụng phạm vi?
hushed_voice

Đó là câu hỏi của tôi trước đây: tại sao bạn cần sử dụng phạm vi? Giống như bạn muốn sử dụng xe hơi khi leo núi và bây giờ bạn đang nói "ok, tôi hiểu tại sao tôi không thể sử dụng xe hơi, nhưng làm thế nào tôi có thể sử dụng xe hơi để leo núi?" Ý định của bạn không rõ ràng, xin vui lòng làm rõ.
azizbekian

Có lẽ tôi đã sai lầm. Kỳ vọng của tôi là sử dụng phạm vi là một cách tiếp cận tốt hơn. Ví dụ. Nếu có 2 hoạt động trong ứng dụng của tôi (Đăng nhập và Chính), sử dụng 1 phạm vi tùy chỉnh để đăng nhập và 1 phạm vi tùy chỉnh cho chính, sẽ xóa các trường hợp không cần thiết trong khi một hoạt động đang hoạt động
hushed_voice

> Kỳ vọng của tôi là sử dụng phạm vi là một cách tiếp cận tốt hơn Không phải cái nào tốt hơn cái kia. Họ đang giải quyết các vấn đề khác nhau, mỗi người đều có trường hợp sử dụng.
azizbekian

> sẽ loại bỏ các trường hợp không cần thiết trong khi một hoạt động đang hoạt động Không thể xem nơi nào sẽ tạo ra các "trường hợp không cần thiết" đó. ViewModelđược tạo ra với vòng đời của hoạt động / đoạn và bị hủy ngay khi vòng đời lưu trữ của nó bị phá hủy. Bạn không nên tự mình quản lý việc phá hủy vòng đời / sáng tạo của ViewModel, đó là những gì các thành phần kiến ​​trúc đang làm cho bạn với tư cách là khách hàng của API đó.
azizbekian

0

Cả hai mảnh đều nhận được bản cập nhật từ liveata vì viewpager giữ cả hai mảnh ở trạng thái tiếp tục. Vì bạn chỉ yêu cầu cập nhật trên đoạn hiện tại hiển thị trong chế độ xem, bối cảnh của hiện tại được xác định bởi hoạt động máy chủ, nên hoạt động nên cập nhật trực tiếp đến đoạn mong muốn.

Bạn cần duy trì bản đồ Fragment to LiveData chứa các mục nhập cho tất cả các đoạn (đảm bảo có một mã định danh có thể phân biệt hai trường hợp phân đoạn của cùng một đoạn) được thêm vào chế độ xem.

Bây giờ, hoạt động sẽ có MediatorLiveData quan sát trực tiếp ban đầu được quan sát bởi các mảnh vỡ trực tiếp. Bất cứ khi nào bản gốc được đăng một bản cập nhật, nó sẽ được gửi đến người hòa giải và người hòa giải trong turen sẽ chỉ đăng giá trị lên bản sống của đoạn được chọn hiện tại. Sống này sẽ được lấy từ bản đồ trên.

Mã impl sẽ trông như thế nào -

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

Cảm ơn câu trả lời. Tôi đang sử dụng FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)vậy làm thế nào là viewpager giữ cả hai mảnh ở trạng thái tiếp tục? Điều này đã không xảy ra trước khi tôi thêm dao găm 2 vào dự án.
hushed_voice

Tôi sẽ thử và thêm một dự án mẫu với hành vi đã nói
hushed_voice

Này tôi đã thêm một dự án mẫu. Bạn có thể vui lòng kiểm tra nó Tôi cũng sẽ thêm một tiền thưởng cho việc này. (Xin lỗi vì sự chậm trễ)
hushed_voice

@hushed_voice Chắc chắn sẽ lấy lại cho bạn.
Vishal Arora
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.