[android] 단검 2를 사용하여 ViewPager 내에서 동일한 조각의 ViewModel을 주입하는 방법
프로젝트에 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)
})
....
}
ViewModel
s 를 제공하기 위해이 클래스를 사용하고 있습니다 .
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 link
master
지점에 문제가있는 앱이 있습니다. 탭을 새로 고치면 (스 와이프하여 새로 고침) 업데이트 된 값이 두 탭에 모두 반영됩니다. 이것은 사용자 지정 범위를 추가 할 때만 발생합니다 ( @MainScope
).
working_fine
지점에는 사용자 지정 범위가없고 동일한 작동을하는 동일한 앱이 있습니다.
질문이 명확하지 않은 경우 알려주십시오.
답변
원래 질문을 요약하고 싶습니다. 여기 있습니다.
나는 현재 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()
의 인스턴스 얻기 위해 노력할 것입니다 PagerItemViewModel
A로부터 ViewModelStore
기본적으로의 맵 String
에를 ViewModel
.
경우 FirstFragment
의 예를 요청 따라서 비어 에서 끝나는 실행된다 . 다음 코드로 호출 합니다.PagerItemViewModel
map
mFactory.create(modelClass)
ViewModelProviderFactory
creator.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
하므로 동일한 데이터를 표시하는 것이 좋습니다.
답변
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
})
}
}