Navigation component – Why ViewModel is not re-created even though its owner (Fragment) is being re-created in NavHostFragment?

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

Navigation component - Why ViewModel is not re-created even though its owner (Fragment) is being re-created in NavHostFragment?

问题

我正在提到由Android Studio创建的底部导航视图活动模板项目。

我注意到,每当我点击底部视图以从DashboardFragment页面切换到另一个片段页面时,然后再次点击底部视图以切换回DashboardFragment时,将会构建一个新的DashboardFragment

我通过在其init函数中记录日志来验证了这种行为。

DashboardFragment.kt

class DashboardFragment : Fragment() {

    ...

    init {
        Log.i("CHEOK", "DashboardFragment constructed")
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        Log.i("CHEOK", "ViewModelProvider(this).get in DashboardFragment onCreateView")

        val dashboardViewModel =
            ViewModelProvider(this).get(DashboardViewModel::class.java)

DashboardViewModel.kt

class DashboardViewModel : ViewModel() {

    ...

    init {
        Log.i("CHEOK", "DashboardViewModel constructed")
    }
}

然而,令我惊讶的是,DashboardViewModel,这是一个由DashboardFragment拥有的ViewModel,并没有被重新构建。

以下是我在第一次将DashboardFragment切换为可见时观察到的日志记录。

DashboardFragment constructed

ViewModelProvider(this).get in DashboardFragment onCreateView

DashboardViewModel constructed

当我切换到另一个片段,然后再次切换回DashboardFragment以进行第二次切换时,我观察到以下日志记录。

DashboardFragment constructed

ViewModelProvider(this).get in DashboardFragment onCreateView

我期望DashboardViewModel将被重新创建。这是因为ViewModelProviderDashboardFragment作为其所有者,而不是MainActivity

val dashboardViewModel =
            ViewModelProvider(this).get(DashboardViewModel::class.java)

当第一个DashboardFragment被销毁时,DashboardViewModel也应该被销毁。但是,似乎情况并非如此。

我可以知道,在NavHostFragment中重新创建Fragment的情况下,为什么ViewModel没有被重新创建吗?

演示

您可以在https://github.com/yccheok/lifecycle-NavHostFragment/tree/f58b7aa6773de09811e9858a84a3b4614edbe3b3上测试演示。

英文:

I am referring to the Bottom Navigation Views Activity template project created by Android Studio.

I notice, whenever I tap on the bottom view to switch from DashboardFragment page, to another fragment page. Then, I tap on the bottom view again to switch back to DashboardFragment, a new DashboardFragment will be constructed.

I have verified such a behaviour by having logging in its init function.

DashboardFragment.kt

class DashboardFragment : Fragment() {

    ...

    init {
        Log.i("CHEOK", "DashboardFragment constructed")
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        Log.i("CHEOK", "ViewModelProvider(this).get in DashboardFragment onCreateView")

        val dashboardViewModel =
            ViewModelProvider(this).get(DashboardViewModel::class.java)

DashboardViewModel.kt

class DashboardViewModel : ViewModel() {

    ...

    init {
        Log.i("CHEOK", "DashboardViewModel constructed")
    }
}

However, to my surprise, DashboardViewModel, which is a DashboardFragment owned ViewModel, is not being re-constructed.


Here's is the logging I am observing, when DashboardFragment is switched to visible for the 1st time.

> DashboardFragment constructed
>
> ViewModelProvider(this).get in DashboardFragment onCreateView
>
> DashboardViewModel constructed

When I switch to another fragment, and then switch back to DashboardFragment for the 2nd time, the following is the logging I am observing

> DashboardFragment constructed
>
> ViewModelProvider(this).get in DashboardFragment onCreateView

I am expecting DashboardViewModel will be re-created again. This is because, ViewModelProvider is having DashboardFragment as its owner, and not MainActivity.

val dashboardViewModel =
            ViewModelProvider(this).get(DashboardViewModel::class.java)

When the 1st DashboardFragment is destroyed, the DashboardViewModel should be destroyed too. But, seems like this is not the case.

May I know, why ViewModel is not re-created even though its owner (Fragment) is being re-created in NavHostFragment?

Demo

You may test the demo at https://github.com/yccheok/lifecycle-NavHostFragment/tree/f58b7aa6773de09811e9858a84a3b4614edbe3b3

答案1

得分: 1

以下是您要翻译的内容:

To expand on Arda Kazancı's answer, the back stack is a record of the navigation history within a NavController.
Each time you navigate to a Fragment, that Fragment is added to the back stack.
When you navigate back, the current Fragment is popped off the back stack, and the previous Fragment is recreated.

However, if you are scoping your ViewModel to the NavController's lifecycle, the ViewModel will not be recreated along with the Fragment; instead, the existing ViewModel instance will be reused.

When we talk about scoping a ViewModel to a Fragment's lifecycle or to a NavController's lifecycle, we are talking about when the ViewModel is created and destroyed.

  • If the ViewModel is scoped to the Fragment's lifecycle (using viewLifecycleOwner), the ViewModel will be created when the Fragment's view is created and destroyed when the Fragment's view is destroyed. This is true whether the Fragment is destroyed because it was popped off the back stack or because of a configuration change like a screen rotation.

  • If the ViewModel is scoped to the NavController's lifecycle (the default behavior when using ViewModelProvider(this) in a Fragment), the ViewModel will be created the first time the Fragment is launched and will not be destroyed until the NavController is destroyed (which usually happens when the activity hosting the NavController is finished). This means that the ViewModel will survive even if the Fragment is destroyed and recreated, such as when the user navigates away from the Fragment and then back to it, causing the Fragment to be popped off the back stack and then added back to it.

When you are using the Navigation component along with NavHostFragment, it uses a slightly different lifecycle scope for the ViewModel than a standalone Fragment would. In other words, even though your Fragment is being recreated, the associated ViewModel is not.

(See also "Get started with the Navigation component")

This is because NavHostFragment maintains its own ViewModelStore that is scoped to the NavController's lifecycle. This ViewModelStore is used to store ViewModels for all destinations (fragments) that are a part of the NavController.

(See also "Communication of Fragments in the Fragment, using Navigation Component, MVVM and Koin" by Piotr Prus)

When you are calling ViewModelProvider(this).get(DashboardViewModel::class.java), the ViewModel is being retrieved from the ViewModelStore associated with the NavController, not the individual fragment. This means that the ViewModel will persist as long as the NavController is alive, even if individual fragments are destroyed and recreated.

This design allows data to be persisted across configuration changes and navigation events, which is a common pattern in Android development.

You can observe this behavior by checking the ID of the ViewModel object:

Log.i("CHEOK", "DashboardViewModel constructed. ID: ${System.identityHashCode(this)}")

You will see that the ID remains the same across different instances of the Fragment, indicating that it is the same ViewModel instance.


If you want a ViewModel that is scoped to the NavController's lifecycle and survives configuration changes and navigation between Fragments, you can use navGraphViewModels:

class DashboardFragment : Fragment() {

    private val dashboardViewModel: DashboardViewModel by navGraphViewModels(R.id.nav_graph) { ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!) }

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

        // Use the ViewModel
    }
}

Here, R.id.nav_graph is the ID of your navigation graph. This ViewModel will be shared between all Fragments in the navigation graph and will be destroyed when the last Fragment in the navigation graph is popped off the back stack.

(See also "What is the difference between navGraphViewModels and activityViewModels?")

But then, the original behavior you observed will persist. The DashboardViewModel will not be re-constructed each time you navigate back to the DashboardFragment.

The navGraphViewModels delegate method creates or retrieves a ViewModel that is scoped to a navigation graph. This means the ViewModel will be shared between all fragments in the navigation graph and will survive configuration changes and navigation between fragments. The ViewModel will only be cleared (and thus ready for re-construction) when the last fragment in the navigation graph is popped off the back stack.

If you want the ViewModel to be scoped to the Fragment's lifecycle instead of the NavController's lifecycle, you would have to use the ViewModelProvider with the Fragment's viewLifecycleOwner:

val dashboardViewModel = ViewModelProvider(viewLifecycleOwner).get(DashboardViewModel::class.java)

Note: as noted in "ViewModelProviders is deprecated in 1.1.0", early in 2020, Google had deprecated the ViewModelProviders class, in version 2.2.0 of the AndroidX lifecycle library.

In the context of the AndroidX libraries, the equivalent would be:

dashboardViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!)).get(DashboardViewModel::class.java)

With this referring to the Fragment itself. ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!) is the Factory that is used to create the ViewModel. This factory requires the Application as a parameter, and it is retrieved from the Fragment's associated `Activity

英文:

To expand on Arda Kazancı's answer, the back stack is a record of the navigation history within a NavController.
Each time you navigate to a Fragment, that Fragment is added to the back stack.
When you navigate back, the current Fragment is popped off the back stack, and the previous Fragment is recreated.

However, if you are scoping your ViewModel to the NavController's lifecycle, the ViewModel will not be recreated along with the Fragment; instead, the existing ViewModel instance will be reused.

When we talk about scoping a ViewModel to a Fragment's lifecycle or to a NavController's lifecycle, we are talking about when the ViewModel is created and destroyed.

  • If the ViewModel is scoped to the Fragment's lifecycle (using viewLifecycleOwner), the ViewModel will be created when the Fragment's view is created and destroyed when the Fragment's view is destroyed. This is true whether the Fragment is destroyed because it was popped off the back stack or because of a configuration change like a screen rotation.

  • If the ViewModel is scoped to the NavController's lifecycle (the default behavior when using ViewModelProvider(this) in a Fragment), the ViewModel will be created the first time the Fragment is launched and will not be destroyed until the NavController is destroyed (which usually happens when the activity hosting the NavController is finished). This means that the ViewModel will survive even if the Fragment is destroyed and recreated, such as when the user navigates away from the Fragment and then back to it, causing the Fragment to be popped off the back stack and then added back to it.

When you are using the Navigation component along with NavHostFragment, it uses a slightly different lifecycle scope for the ViewModel than a standalone Fragment would. In other words, even though your Fragment is being recreated, the associated ViewModel is not.

(See also "Get started with the Navigation component")

This is because NavHostFragment maintains its own ViewModelStore that is scoped to the NavController's lifecycle. This ViewModelStore is used to store ViewModels for all destinations (fragments) that are a part of the NavController.

(See also "Communication of Fragments in the Fragment, using Navigation Component, MVVM and Koin" by Piotr Prus)

When you are calling ViewModelProvider(this).get(DashboardViewModel::class.java), the ViewModel is being retrieved from the ViewModelStore associated with the NavController, not the individual fragment. This means that the ViewModel will persist as long as the NavController is alive, even if individual fragments are destroyed and recreated.

This design allows data to be persisted across configuration changes and navigation events, which is a common pattern in Android development.

You can observe this behavior by checking the ID of the ViewModel object:

Log.i("CHEOK", "DashboardViewModel constructed. ID: ${System.identityHashCode(this)}")

You will see that the ID remains the same across different instances of the Fragment, indicating that it is the same ViewModel instance.


If you want a ViewModel that is scoped to the NavController's lifecycle and survives configuration changes and navigation between Fragments, you can use navGraphViewModels:

class DashboardFragment : Fragment() {

    private val dashboardViewModel: DashboardViewModel by navGraphViewModels(R.id.nav_graph) { ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!) }

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

        // Use the ViewModel
    }
}

Here, R.id.nav_graph is the ID of your navigation graph. This ViewModel will be shared between all Fragments in the navigation graph and will be destroyed when the last Fragment in the navigation graph is popped off the back stack.

(See also "What is the difference between navGraphViewModels and activityViewModels?")

But then, the original behavior you observed will persist. The DashboardViewModel will not be re-constructed each time you navigate back to the DashboardFragment.

The navGraphViewModels delegate method creates or retrieves a ViewModel that is scoped to a navigation graph. This means the ViewModel will be shared between all fragments in the navigation graph and will survive configuration changes and navigation between fragments. The ViewModel will only be cleared (and thus ready for re-construction) when the last fragment in the navigation graph is popped off the back stack.


If you want the ViewModel to be scoped to the Fragment's lifecycle instead of the NavController's lifecycle, you would have to use the ViewModelProvider with the Fragment's viewLifecycleOwner:

val dashboardViewModel = ViewModelProvider(viewLifecycleOwner).get(DashboardViewModel::class.java)

Note: as noted in "ViewModelProviders is deprecated in 1.1.0", early in 2020, Google had deprecated the ViewModelProviders class, in version 2.2.0 of the AndroidX lifecycle library.

In the context of the AndroidX libraries, the equivalent would be:

dashboardViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!)).get(DashboardViewModel::class.java)

With this referring to the Fragment itself. ViewModelProvider.AndroidViewModelFactory.getInstance(activity?.application!!) is the Factory that is used to create the ViewModel. This factory requires the Application as a parameter, and it is retrieved from the Fragment's associated Activity.

Those codes will give you a ViewModel that is scoped to the Fragment's lifecycle and will be recreated every time the Fragment is recreated.
However, keep in mind that this might not be the desired behavior, especially if you want to maintain state across navigation events.

Illustration: "How Android ViewModel works under the hood to survive to configuration change" by Torcheux Frédéric.

答案2

得分: 0

因为“Dashboard Fragment”位于返回堆栈中。它至少已创建一次。它包括了多个返回堆栈的概念。

在某些情况下,您的应用程序可能需要支持多个返回堆栈。一个常见的例子是如果您的应用程序使用底部导航栏。FragmentManager允许您使用**saveBackStack()restoreBackStack()**方法来支持多个返回堆栈。这些方法让您通过保存一个返回堆栈并恢复另一个来在返回堆栈之间进行切换。

英文:

Because Dashboard Fragment is located in back stack. It has been created at least once. It includes the concept of Multiple BackStacks.

In some cases, your app might need to support multiple back stacks. A common example is if your app uses a bottom navigation bar. FragmentManager lets you support multiple back stacks with the saveBackStack() and restoreBackStack() methods. These methods let you swap between back stacks by saving one back stack and restoring a different one.

huangapple
  • 本文由 发表于 2023年6月8日 16:37:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76430031.html
匿名

发表评论

匿名网友

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

确定