同一Activity中Fragment之间的ViewModel作用范围问题

4
我有一个问题,我认为它源于ViewModel的范围。我在同一活动下的同一导航图中有两个片段。第一个片段LoginFragment实例化了一个UserDataViewModel并在名为UserData的对象中填充了其中的某些数据。

第二个片段TodayFragment应该接收UserDataViewModel并使用已经填充的属性进行进一步的工作。但是,当TodayFragment尝试访问UserData对象时,该对象为空。需要强调的是,UserDataViewModel仍然具有相同的内存地址,但问题是它已经丢失了对其数据值的引用。

我已将此简化,以便数据仅从SharedPreferences中提取字符串并使用GSON进行解析。

LoginFragment:
private lateinit var userDataViewModel: UserDataViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    MyApplication.appComponent?.inject(this)
    prefs = PreferenceManager.getDefaultSharedPreferences(context)

    userDataViewModel = activity?.run {
        ViewModelProviders
            .of(this, UserDataViewModelFactory(prefs, dataFetcherService))
            .get(UserDataViewModel::class.java)
    } ?: throw Exception("Invalid Activity")

    userDataViewModel.getUserData()
}

TodayFragment

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    prefs = PreferenceManager.getDefaultSharedPreferences(activity)

    userDataViewModel = activity?.run {
        ViewModelProviders
            .of(this, UserDataViewModelFactory(prefs, dataFetcherService))
            .get(UserDataViewModel::class.java)
    } ?: throw Exception("Invalid Activity")
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    userDataViewModel
        .getUserData()
        .observe(this, Observer {
            clubId = it.club_id  // <-- this is where it crashes since 'it' is null
        })
}

UserDataViewModel.kt

class UserDataViewModel(
    prefs: SharedPreferences,
    dataFetcherService: DataFetcherService)
    : ViewModel() {

    private val useCase = UserDataUseCase(prefs, dataFetcherService)

    val userData: MutableLiveData<UserData> by lazy {
        MutableLiveData<UserData>().apply { loadUserData() }
    }

    fun getUserData(): LiveData<UserData> {
        return userData
    }

    private fun loadUserData() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                try {
                    return@withContext useCase.loadUserData(USER_UNKNOWN)
                } catch (t: Throwable) {
                    return@withContext null
                }
            }
                ?.let {
                    userData.postValue(it)
                }
        }
    }
}

问题出在视图模型中的getUserData()方法。当我在这些行上设置断点时:

return userData
MutableLiveData<UserData>().apply { loadUserData() }
return@withContext useCase.loadUserData(USER_UNKNOWN)

这些断点都会在LoginFragment中被触发。但是当它进入TodayFragment时,它会在视图模型中的return userData处触发,然后立即跳回到TodayFragment内部的观察者,并出现一个空指针异常,因为视图模型内的值为空。

我认为这在视图模型本身被实例化的程度上是有道理的,但为什么视图模型内的值为空呢?

出于调试目的,我正在硬编码来自用例和存储库的响应,以确保ViewModel中有一个填充的值。

LoginFragment的屏幕截图 enter image description here

TodayFragment的屏幕截图(extends AbstractWorkoutFragment)
注意现在mData为空,但ViewModel是相同的。 enter image description here


你如何定义实现 UserDataViewModelFactory - tynn
5个回答

3

在每个代码片段中,您可以通过以下方式获取ViewModel的实例:

ViewModelProviders
        .of(this, UserDataViewModelFactory(prefs, dataFetcherService))

注意 this 指向 Fragment.this。这意味着,作为 LifecycleOwner,您正在提供 Fragment 的实例。因此,每个 Fragment 都将创建自己的全新实例 UserDataViewModel
相反,实际上想要的是重用同一实例的 ViewModel。这意味着,作为 LifecycleOwner,应该传入托管的活动:
ViewModelProviders
        .of(requireActivity(), UserDataViewModelFactory(prefs, dataFetcherService))

这样做可以在两个Fragments中获得相同的ViewModel实例。

3
在这种情况下,您可以使用navGraphViewModels()属性委托在片段目的地中检索ViewModel,并通过这样做,您可以访问与您的导航图"附加"的ViewModel。
因此,通常您可以在LoginFragment和TodayFragment中使用以下内容。
private val userDataViewModel: UserDataViewModel by navGraphViewModels(R.id.youNavGraphID)

但在您的情况下,您在初始化视图模型时将参数传递给它,因此您还需要传递一个 ViewModelProvider.Factory

private val sharedViewModel: BookViewModel by navGraphViewModels(R.id.navGraph, ::viewModelProducer)

fun viewModelProducer(): ViewModelProvider.Factory {
    return object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return UserDataViewModelFactory(prefs, dataFetcherService) as T
        }
    }
}

而不仅仅是

 userDataViewModel = activity?.run {
    ViewModelProviders
        .of(this, UserDataViewModelFactory(prefs, dataFetcherService))
        .get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")

--- 更新 ---

我刚刚遇到了与你相同的情况。 我会保留我之前的答案,但我相信下面的解决方案应该可以解决您的问题。

当您初始化一个活动/片段作用域的视图模型时,不应该在 ViewModelProviders.of() 中传递this,因为那将意味着传递给Fragment本身而不是活动。

 userDataViewModel = activity?.run {
    ViewModelProviders
        .of(this, UserDataViewModelFactory(prefs, dataFetcherService))
        .get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")

相反,您应该按以下方式使用该活动。

userDataViewModel = activity?.run {
    ViewModelProviders
        .of(it, UserDataViewModelFactory(prefs, dataFetcherService))
        .get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")

0
根据https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycle,ViewModel会一直保留在内存中,直到其所属的生命周期永久消失:对于Activity来说,是在其结束时;而对于Fragment来说,则是在其被分离时。
这意味着,ViewModel的生命周期绑定在Activity/Fragment上,而不是应用程序。
ViewModelProviders会为某些情况重用ViewModel。但当您从LoginFragment导航到TodayFragment时,由提供程序创建并绑定到LoginFragment的ViewModel将被销毁,然后新的ViewModel将被创建并绑定到TodayFragment。
我认为这就是您获取null引用的原因。
注意:我无法解释为什么旧的ViewModel和新的ViewModel具有相同的内存地址。根据我的经验,如果没有其他操作覆盖它,已释放的内存可以被重复使用。也许这就是为什么内存地址没有改变的原因。

0
在创建ViewModel时,应传递Activity Context而不是Fragment Context。ViewModels的生命周期与Activity相同。
val activityContext = getActivity()
ViewModelProviders.of(activityContext, ...)

当Activity被销毁时,您需要在ViewModel中保留对它的引用。 - RonTLV
@RonTLV,你怎么解释这个呢?这是谷歌推荐的:https://developer.android.com/topic/libraries/architecture/viewmodel#sharing - Marek Kondracki
为什么要对答案进行负投票?传递活动上下文可以为所有片段保留通用视图模型。UPVOTE。这是推荐的方式。 - Anbarasu Chinna

0

希望我能理解你的问题,这里是一些建议

  1. 尝试将您的ViewModel更改为 class UserDataViewModel(application: Application) : AndroidViewModel(application)。这样实例就绑定到应用程序生命周期。

  2. 设置clubId可以是it?.apply { setClubId(club_id) },以便在实际数据可用时应用程序会执行相应操作。

  3. 每个Fragment都应该独立于其他Fragment,例如您可以从LoginFragment内使用view.findNavController().navigate(R.id.action_today_fragment)进行导航。基本上您完全依赖于图形进行导航,并使用ViewModel保存数据。

此外,数据源不应从Activity / Fragment中传递(例如,您的SharedPref使用似乎不正确)。它应该是

Activities/Fragments <--- ViewModel <------ DataSources

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接