英文:
Should the Viewmodel of a DetailFragment be shared amongst instances?
问题
The issue you're encountering is due to the shared instance of StoreDetailViewModel among different instances of StoreDetailFragment. To resolve this, you should scope the StoreDetailViewModel to each StoreDetailFragment individually. You can achieve this by using the ViewModelProvider with the specific fragment as the viewModelStoreOwner. Here's the change you should make in your code:
In StoreDetailFragment, when obtaining the StoreDetailViewModel, use the fragment itself as the viewModelStoreOwner:
private val viewmodel: StoreDetailViewModel by lazy {
viewModelFactory.get<StoreDetailViewModel>(this)
}
This will ensure that each StoreDetailFragment gets its own instance of StoreDetailViewModel, preventing the issue you described.
No other changes are needed in your code. This change should resolve the problem you mentioned.
英文:
I wrote a master fragment that shows a list of stores, call it StoreFeedFragment. If you click on a store it calls newInstance of StoreDetailFragment to provide details of the clicked store. Both fragments exist in MainActivity that swaps the 2 fragments using replace().
class MainActivity : AppCompatActivity() {
@Inject
lateinit var storeItemViewModelFactory: ViewModelFactory<StoreItemViewModel>
private val storeItemViewModel: StoreItemViewModel by lazy {
storeItemViewModelFactory.get<StoreItemViewModel>(
this
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
TCApplication.getAppComponent().inject(this)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
val storeFeedFragment = StoreFeedFragment()
supportFragmentManager.beginTransaction()
.replace(
R.id.container, storeFeedFragment,
StoreFeedFragment.TAG
)
.commit()
}
storeItemViewModel.selectedItem.observe(this) { event ->
event.getContentIfNotHandled()?.apply {
val fragment = StoreDetailFragment.newInstance(id)
supportFragmentManager.beginTransaction()
.replace(
R.id.container, fragment
).addToBackStack("feed_to_item_tag")
.commit()
}
}
}
}
Event is just an event wrapper to prevent observing the selected store again after a backpress from StoreDetailFragment:
class Event<T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) null
else {
hasBeenHandled = true
content
}
}
}
Recently I noticed a bug
- Click into store 1 on StoreFeedFragment.
- StoreDetailFragment shows details for store 1.
- Click back to return to StoreFeedFragment.
- After returning to StoreFeedFragment click on store 2.
- StoreDetailFragment shows details for store 1, then store 2. It should not show details for store 1 because store 2 was clicked.
class StoreDetailFragment : Fragment() {
@Inject
lateinit var viewModelFactory: ViewModelFactory<StoreDetailViewModel>
private val viewmodel: StoreDetailViewModel by lazy {
viewModelFactory.get<StoreDetailViewModel>(
requireActivity()
)
}
companion object {
private const val SELECTED_ID = "selected"
fun newInstance(storeId: String): StoreDetailFragment {
val fragment = StoreDetailFragment().also {
it.arguments = bundleOf(Pair(SELECTED_ID, storeId))
}
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
TCApplication.getAppComponent().inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_store_detail, container, false)
viewmodel.storeDetailResult.observe(viewLifecycleOwner) { resultState ->
when (resultState) {
is ResultState.Loading -> { }
is ResultState.Success -> { showDetails(resultState.value) }
is ResultState.Failure -> {
Snackbar.make(
view,
"Error getting store details ${resultState.ex.message}",
500
).show() }
}
}
arguments?.getString(SELECTED_ID)?.let { storeId -> viewmodel.loadStoreDetails(storeId) }
return view
}
private fun showDetails(storeDetails: StoreDetail) {
view?.apply {
findViewById<TextView>(R.id.name).text = storeDetails.name
findViewById<TextView>(R.id.phoneNo).text = storeDetails.phoneNo
}
}
}
class StoreDetailViewModel @Inject constructor(
private val storeDetailRepository: StoreDetailRepository
): ViewModel(), CoroutineScope by MainScope() {
private val _storeDetailResult = MutableLiveData<ResultState<StoreDetail>>()
val storeDetailResult: LiveData<ResultState<StoreDetail>> = _storeDetailResult
fun loadStoreDetails(storeId: String) {
viewModelScope.launch {
try {
storeDetailRepository.getStoreDetail(storeId)
.collect { storeDetail ->
_storeDetailResult.postValue(
ResultState.Success(
storeDetail
)
)
}
} catch (e: Exception) {
_storeDetailResult.postValue(
ResultState.Failure(
"getStoreDetail($storeId)", e
)
)
}
}
}
}
From log statements it became clear that this occurred because ViewModelFactory only creates a single instance of StoreDetailViewModel that is shared between StoreDetailFragment instance for store 1 and StoreDetailFragment instance for store 2. StoreDetailFragment for store 2 was observing the latest emission from StoreDetailViewModel (info for store 1), then calling loadStoreDetails for store2.
class ViewModelFactory<T: ViewModel>
@Inject constructor(private val viewModel: Lazy<T>) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return viewModel.get() as T
}
/**
* Returns an instance of a defined ViewModel class.
*/
inline fun <reified R: T> get(viewModelStoreOwner: ViewModelStoreOwner): T {
return ViewModelProvider(viewModelStoreOwner, this)[R::class.java]
}
}
Should StoreDetailViewModel be shared among instance 1 and 2 of StoreDetailFragment? What do I need to change in my code to follow the typical solution for this problem? Thanks in advance.
答案1
得分: 0
Should StoreDetailViewModel be shared among instance 1 and 2 of StoreDetailFragment?
是的,因为您将ViewModel的范围限定在Activity中。
What do I need to change in my code to follow the typical solution for this problem?
将ViewModel的范围限定在Fragment中。
英文:
> Should StoreDetailViewModel be shared among instance 1 and 2 of StoreDetailFragment?
Yes, because you scoped the ViewModel the the Activity.
@Inject
lateinit var viewModelFactory: ViewModelFactory<StoreDetailViewModel>
private val viewmodel: StoreDetailViewModel by lazy {
viewModelFactory.get<StoreDetailViewModel>(
requireActivity()
)
}
See: https://developer.android.com/guide/fragments/communicate#host-activity
> What do I need to change in my code to follow the typical solution for this problem?
Scope your ViewModel to the Fragment.
@Inject
lateinit var viewModelFactory: ViewModelFactory<StoreDetailViewModel>
private val viewmodel: StoreDetailViewModel by lazy {
viewModelFactory.get<StoreDetailViewModel>(
this
)
}
See: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论