단검 2를 사용하여 ViewPager 내에서 동일한 조각의 ViewModel을 주입하는 방법


10

프로젝트에 Dagger 2를 추가하려고합니다. 조각에 ViewModels (AndroidX Architecture 구성 요소)를 주입 할 수있었습니다.

동일한 조각의 두 인스턴스 (각 탭마다 약간의 변경 만) 가있는 ViewPager가 있으며 각 탭 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
    }
}

나는 다음 DaggerFragmentViewModelProviderFactory같이 확장 하고 주입하고 있다 :

@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 link

master지점에 문제가있는 앱이 있습니다. 탭을 새로 고치면 (스 와이프하여 새로 고침) 업데이트 된 값이 두 탭에 모두 반영됩니다. 이것은 사용자 지정 범위를 추가 할 때만 발생합니다 ( @MainScope).

working_fine 지점에는 사용자 지정 범위가없고 동일한 작동을하는 동일한 앱이 있습니다.

질문이 명확하지 않은 경우 알려주십시오.


working_fine지점 에서 접근 방식을 사용하지 않습니까? 왜 범위가 필요합니까?
azizbekian

@azizbekian 나는 현재 잘 작동하는 분기를 사용하고 있습니다. 그러나 왜 범위를 사용하면이 문제가 발생하는지 알고 싶습니다.
hushed_voice

답변:


1

원래 질문을 요약하고 싶습니다. 여기 있습니다.

나는 현재 working을 사용하고 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()}")

이런 일이 일어나고 있는지 이해합시다

내부 ViewModelProvider#get()의 인스턴스 얻기 위해 노력할 것입니다 PagerItemViewModelA로부터 ViewModelStore기본적으로의 맵 String에를 ViewModel.

경우 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

어쩌면 내가 틀렸을 수도 있습니다. 내 범위는 범위를 사용하는 것이 더 나은 방법이라는 것이 었습니다. 예를 들어. 로그인 1 개 사용자 지정 범위 및 주요 1 개 사용자 지정 범위를 사용하여 내 응용 프로그램 (로그인 및 홈페이지) 2 개 활동이있을 경우, 불필요한 인스턴스를 제거하는 것은 하나 개의 활동이 활성화되어있는 동안
hushed_voice은

> 내 기대는 범위를 사용하는 것이 더 나은 방법이라는 것이 었습니다. 하나가 다른 것보다 낫지는 않습니다. 그들은 서로 다른 문제를 해결하고 있으며 각각에는 유스 케이스가 있습니다.
azizbekian

>는 하나의 활동이 활성화 된 상태에서 불필요한 인스턴스를 제거합니다. "불필요한 인스턴스"에서 어디를 작성해야하는지 알 수 없습니다. ViewModel활동 / 조각의 라이프 사이클로 생성되며 호스팅 라이프 사이클이 파괴 되 자마자 소멸됩니다. ViewModel의 수명주기 / 생성 파괴를 직접 관리해서는 안됩니다. 이것이 바로 아키텍처 API가 해당 클라이언트의 클라이언트로서 수행하는 작업입니다.
azizbekian

0

viewpager가 두 조각을 모두 재개 된 상태로 유지하기 때문에 두 조각 모두 라이브 데이터에서 업데이트를받습니다. 당신은 단지 viewpager에서 보이는 현재의 단편에 업데이트를 요구하기 때문에,의 문맥 현재의 단편, 호스트 활동에 의해 정의 된 활동을해야 원하는 조각에 명시 적으로 직접 업데이트됩니다.

viewpager에 추가 된 모든 프래그먼트에 대한 항목을 포함하는 Fragment 맵을 LiveData에 매핑해야합니다 (동일한 프래그먼트의 두 프래그먼트 인스턴스를 구별 할 수있는 식별자가 있는지 확인).

이제 액티비티는 조각에서 직접 관찰 한 원래 라이브 데이터를 관찰하는 MediatorLiveData를 갖습니다. 원본 라이브 데이터가 업데이트를 게시 할 때마다 업데이트가 mediatorLivedata에 전달되고 turen의 mediatorlivedata는 현재 선택한 조각의 라이브 데이터에만 값을 게시합니다. 이 라이브 데이터는 위의지도에서 검색됩니다.

코드 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)viewpager가 두 조각을 재개 된 상태로 유지하는 방법을 사용 하고 있습니까? 단검 2를 프로젝트에 추가하기 전에는 이런 일이 일어나지 않았습니다.
hushed_voice

나는 시도하고 말했다 행동 샘플 프로젝트를 추가합니다
hushed_voice

샘플 프로젝트를 추가했습니다. 확인해주세요. 또한 현상금도 추가하겠습니다. (지연 히 죄송합니다)
hushed_voice

@hushed_voice 물론 다시 연락 드리겠습니다.
Vishal Arora
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.