同一活动中各个片段的ViewModel作用域

时间:2019-11-17 03:09:27

标签: android android-fragments android-livedata android-architecture-components android-viewmodel

我遇到的问题可以追溯到ViewModel范围界定。我在同一活动下的同一导航图中有两个片段。第一个片段LoginFragment实例化一个UserDataViewModel并将其一些数据填充到名为UserData

的对象中

第二个片段TodayFragment应该拾取UserDataViewModel并使用已经填充的属性来做进一步的工作。但是,在TodayFragment尝试访问UserData对象时,该对象为null。要强调的是,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内部的观察者,并且视图模型内部的值具有空指针异常。

我认为在实例化视图模型本身的范围内是有道理的,但是为什么在视图模型中的值 inside 为空?

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

LoginFragment的屏幕截图 enter image description here

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

5 个答案:

答案 0 :(得分:1)

在这种情况下,您可以使用navGraphViewModels()属性委托从片段目标中检索ViewModel,这样就可以访问“附加”到导航图的viewmodel

因此通常您可以在LoginFragmentTodayFragment

中使用以下内容
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")

--- 更新 ---

我只是面临同样的情况。我将保留以前的答案,但我相信以下解决方案可以解决您的问题。

初始化“活动/片段”作用域视图模型时,不应在this中传递ViewModelProviders.of(),因为这意味着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")

答案 1 :(得分:0)

根据 https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycle

ViewModel保留在内存中,直到其生命周期范围永久消失:对于活动而言,它完成时,对而言,对于片段而言,当它脱离时,

这意味着视图模型的寿命绑定到活动/片段,而不绑定到应用程序。

在某些情况下,ViewModelProviders会重用该视图模型,但是当您从屏幕上的LoginFragment导航到TodayFragment时,由提供者创建并绑定到LoginFragment的ViewModel将会被销毁,然后创建一个新模型并将其绑定到TodayFragment。 >

我认为这就是为什么您获得空引用的原因。

注意: 我无法解释为什么您在新旧地址之间获得相同的内存地址。根据我的经验,如果没有其他操作覆盖释放的内存,则可以重用释放的内存。也许这就是为什么内存地址没有更改的原因。

答案 2 :(得分:0)

在创建ViewModel时,传递活动上下文而不是片段上下文。只要活动存在,ViewModels就会一直存在。

val activityContext = getActivity()
ViewModelProviders.of(activityContext, ...)

答案 3 :(得分:0)

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

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

请注意this,它指向Fragment.this。这意味着,作为LifecycleOwner,您要提供Fragment的实例。因此,每个Fragment都会创建自己的UserDataViewModel实例。

实际上,您真正想要的是重用ViewModel的相同实例,这意味着作为LifecycleOwner,您应该传递托管活动:

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

这样,您将在两个ViewModel中获得Fragments的相同实例。

答案 4 :(得分:0)

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

  1. 尝试将您的ViewModel更改为class UserDataViewModel(application: Application) : AndroidViewModel(application)。从而使实例与应用程序生命周期相关联。

  2. 设置clubId可以为it?.apply { setClubId(club_id) },以便应用程序在实际数据可用时起作用。

  3. 每个片段都应彼此独立,以便您从LoginFragment内使用说view.findNavController().navigate(R.id.action_today_fragment)进行导航。本质上,您完全依赖该图进行导航。并使用ViewModel来保存数据。

此外,不应从“活动/片段”中传递数据源(例如,您的SharedPref使用似乎不正确)。应该是

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