Should the Viewmodel of a DetailFragment be shared amongst instances?

huangapple go评论61阅读模式
英文:

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&lt;StoreItemViewModel&gt;
    private val storeItemViewModel: StoreItemViewModel by lazy {
        storeItemViewModelFactory.get&lt;StoreItemViewModel&gt;(
            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 -&gt;
            event.getContentIfNotHandled()?.apply {
                val fragment = StoreDetailFragment.newInstance(id)
                supportFragmentManager.beginTransaction()
                    .replace(
                        R.id.container, fragment
                    ).addToBackStack(&quot;feed_to_item_tag&quot;)
                    .commit()
            }
        }
    }
}

Event is just an event wrapper to prevent observing the selected store again after a backpress from StoreDetailFragment:

class Event&lt;T&gt;(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

  1. Click into store 1 on StoreFeedFragment.
  2. StoreDetailFragment shows details for store 1.
  3. Click back to return to StoreFeedFragment.
  4. After returning to StoreFeedFragment click on store 2.
  5. 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&lt;StoreDetailViewModel&gt;
private val viewmodel: StoreDetailViewModel by lazy {
    viewModelFactory.get&lt;StoreDetailViewModel&gt;(
        requireActivity()
    )
}

companion object {
    private const val SELECTED_ID = &quot;selected&quot;

    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 -&gt;
        when (resultState) {
            is ResultState.Loading -&gt; { }
            is ResultState.Success -&gt; { showDetails(resultState.value) }
            is ResultState.Failure -&gt; {
                Snackbar.make(
                view,
                &quot;Error getting store details ${resultState.ex.message}&quot;,
                500
            ).show() }
        }
    }
    arguments?.getString(SELECTED_ID)?.let { storeId -&gt; viewmodel.loadStoreDetails(storeId) }
    return view
}

private fun showDetails(storeDetails: StoreDetail) {
    view?.apply {
        findViewById&lt;TextView&gt;(R.id.name).text = storeDetails.name
        findViewById&lt;TextView&gt;(R.id.phoneNo).text = storeDetails.phoneNo
    }
}
}


class StoreDetailViewModel @Inject constructor(
    private val storeDetailRepository: StoreDetailRepository
): ViewModel(), CoroutineScope by MainScope() {

    private val _storeDetailResult = MutableLiveData&lt;ResultState&lt;StoreDetail&gt;&gt;()
    val storeDetailResult: LiveData&lt;ResultState&lt;StoreDetail&gt;&gt; = _storeDetailResult

    fun loadStoreDetails(storeId: String) {
        viewModelScope.launch {
            try {
                storeDetailRepository.getStoreDetail(storeId)
                    .collect { storeDetail -&gt;
                        _storeDetailResult.postValue(
                            ResultState.Success(
                                storeDetail
                            )
                        )
                    }
            } catch (e: Exception) {
                _storeDetailResult.postValue(
                    ResultState.Failure(
                        &quot;getStoreDetail($storeId)&quot;, 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&lt;T: ViewModel&gt;
@Inject constructor(private val viewModel: Lazy&lt;T&gt;) : ViewModelProvider.Factory {

    @Suppress(&quot;UNCHECKED_CAST&quot;)
    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {
        return viewModel.get() as T
    }

    /**
     * Returns an instance of a defined ViewModel class.
     */
    inline fun &lt;reified R: T&gt; 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&lt;StoreDetailViewModel&gt;
private val viewmodel: StoreDetailViewModel by lazy {
    viewModelFactory.get&lt;StoreDetailViewModel&gt;(
        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&lt;StoreDetailViewModel&gt;
private val viewmodel: StoreDetailViewModel by lazy {
    viewModelFactory.get&lt;StoreDetailViewModel&gt;(
        this
    )
}

See: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis

huangapple
  • 本文由 发表于 2023年5月15日 08:14:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/76250185.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定