Android 片段和 ViewModel 问题
Android Fragment and ViewModel issue
我已经创建了 FragmentA 并在 onViewCreated() 中初始化了 ViewModel。我用同样的方法附加了观察者。
在 FragmentA 中,我正在进行 API 调用,在 API 调用成功后,我将 FragmentA 替换为 FragmentB 和 addToBackStack。
现在真正的问题开始了,当我在 FragmentB 中按下后退按钮时,返回堆栈中的 FragmentA 被调用但立即再次被 FragmentB 替换。
class FragmentA : Fragment(){
private var viewModel: TrackingViewModel?=null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViewModel()
attachObservers()
}
private fun initViewModel(){
viewModel = ViewModelProvider(requireActivity()).get(TrackingViewModel::class.java)
}
private fun attachObservers() {
viewModel?.mResult?.observe(viewLifecycleOwner, {
it?.let { resource ->
parseResource(resource)
}
})
}
//Called this method on Button CLick in UI
private fun validate(data:String){
viewModel?.coroutineSearch(data)
}
private fun parseResource(resource: Resource<GetsApiResponse>) {
when (resource.status) {
Status.SUCCESS -> {
showLoading(false)
//replaceFragmentWithBackStack is an Extension function
replaceFragmentWithBackStack(FragmentB(), R.id.container)
}
Status.ERROR -> {
showLoading(false)
infoError(resource.responseCode)
}
Status.EXCEPTION -> {
showLoading(false)
infoException()
}
Status.LOADING -> {
showLoading(true)
}
}
}
}
对于使用 LiveData
的每个人来说,这在某些时候是一个普遍的问题。这里的问题是由 intentional LiveData
行为引起的:它 return 在您开始观察它时立即向您提供它存储的值(如果有的话)。 LiveData
主要设计用于 view/data-binding 解决方案。一旦 UI 组件正在观察 LiveData
它应该接收值并适当地显示它。所以你得到的行为是故意的。
包括我在内的许多其他开发人员都遇到过这个问题。我能够通过使用推荐的事件包装器来解决它,作为我在 this post 中发现的这个问题的解决方案。它简单易懂,并且按照 post.
中的描述工作
使用此事件包装器,您的观察者代码将更新为:
private fun attachObservers() {
viewModel?.mResult?.observe(viewLifecycleOwner, {
it?.getContentIfNotHandled()?.let { resource ->
// Only proceed if the event has never been handled
parseResource(resource)
}
})
}
如果您想知道为什么一开始您会立即收到此 LiveData
的结果 - 这是因为您的视图模型已被缓存。当您使用 activity 作为视图模型商店或商店所有者 (ViewModelProvider(requireActivity())
) 时,您的视图模型将继续存在,直到您使用的 activity 未被销毁。这意味着即使您通过按后退按钮返回到其前一个片段而离开 FragmentA
,然后通过创建新实例 return 回到 FragmentA
,您将获得相同的视图模型。
事件包装器源代码
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
我已经创建了 FragmentA 并在 onViewCreated() 中初始化了 ViewModel。我用同样的方法附加了观察者。
在 FragmentA 中,我正在进行 API 调用,在 API 调用成功后,我将 FragmentA 替换为 FragmentB 和 addToBackStack。
现在真正的问题开始了,当我在 FragmentB 中按下后退按钮时,返回堆栈中的 FragmentA 被调用但立即再次被 FragmentB 替换。
class FragmentA : Fragment(){
private var viewModel: TrackingViewModel?=null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViewModel()
attachObservers()
}
private fun initViewModel(){
viewModel = ViewModelProvider(requireActivity()).get(TrackingViewModel::class.java)
}
private fun attachObservers() {
viewModel?.mResult?.observe(viewLifecycleOwner, {
it?.let { resource ->
parseResource(resource)
}
})
}
//Called this method on Button CLick in UI
private fun validate(data:String){
viewModel?.coroutineSearch(data)
}
private fun parseResource(resource: Resource<GetsApiResponse>) {
when (resource.status) {
Status.SUCCESS -> {
showLoading(false)
//replaceFragmentWithBackStack is an Extension function
replaceFragmentWithBackStack(FragmentB(), R.id.container)
}
Status.ERROR -> {
showLoading(false)
infoError(resource.responseCode)
}
Status.EXCEPTION -> {
showLoading(false)
infoException()
}
Status.LOADING -> {
showLoading(true)
}
}
}
}
对于使用 LiveData
的每个人来说,这在某些时候是一个普遍的问题。这里的问题是由 intentional LiveData
行为引起的:它 return 在您开始观察它时立即向您提供它存储的值(如果有的话)。 LiveData
主要设计用于 view/data-binding 解决方案。一旦 UI 组件正在观察 LiveData
它应该接收值并适当地显示它。所以你得到的行为是故意的。
包括我在内的许多其他开发人员都遇到过这个问题。我能够通过使用推荐的事件包装器来解决它,作为我在 this post 中发现的这个问题的解决方案。它简单易懂,并且按照 post.
中的描述工作使用此事件包装器,您的观察者代码将更新为:
private fun attachObservers() {
viewModel?.mResult?.observe(viewLifecycleOwner, {
it?.getContentIfNotHandled()?.let { resource ->
// Only proceed if the event has never been handled
parseResource(resource)
}
})
}
如果您想知道为什么一开始您会立即收到此 LiveData
的结果 - 这是因为您的视图模型已被缓存。当您使用 activity 作为视图模型商店或商店所有者 (ViewModelProvider(requireActivity())
) 时,您的视图模型将继续存在,直到您使用的 activity 未被销毁。这意味着即使您通过按后退按钮返回到其前一个片段而离开 FragmentA
,然后通过创建新实例 return 回到 FragmentA
,您将获得相同的视图模型。
事件包装器源代码
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}