如何在使用导航组件进行页面导航时保存片段状态

43
我正在尝试使用Android架构组件创建单个活动应用程序。我有一个Fragment A,其中包含一些文本字段,当用户按下按钮时,我会导航到Fragment B,在那里他上传和编辑一些图像,然后应用程序使用以下代码导航回A: findNavController().navigate(R.id.action_from_B_to_A, dataBundle) 当导航回来时,B会使用dataBundle将一些数据传递给A。问题在于,所有文本字段都会重置,因为Fragment A基本上是从头开始重新创建的。我在某个地方读到过,Google的开发人员建议您可以将视图保存在var中而不是每次填充它们。所以我试着这样做:
private var savedViewInstance: View? = null

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return if (savedViewInstance != null) {
        savedViewInstance
    } else {
        savedViewInstance =
                inflater.inflate(R.layout.fragment_professional_details, container, false)
        savedViewInstance
    }
}

但是这样不起作用,当返回到 A 页面时所有文本字段都会重置。我做错了什么?如何正确处理这种情况?

Translated text:

但这并不起作用,在返回A页面时所有文本框都会重置。我做错了什么?如何正确处理这种情况?


https://dev59.com/1FMI5IYBdhLWcg3wcbFs#66262120 - Erfan Eghterafi
8个回答

15

我会逐一回答你的问题。

但是这样做不起作用,当返回到A页面时所有文本框都会重置。我做错了什么?

从FragmentB中,当用户完成工作并且应用程序调用下面的方法返回FragmentA。

findNavController().navigate(R.id.action_from_B_to_A, dataBundle)

你原本期望应用程序会将用户带回到FragmentA,但实际结果是创建了一个新的FragmentA并将其放置在返回栈的顶部。现在返回栈的情况如下所示。
FragmentA (new instance)
FragmentB
FragmentA (old instance)

因为它是 FragmentA 的全新实例,所以你会看到所有的文本字段都被重置了。

如何正确处理这种情况?

你想启动一个 Fragment,然后从该 Fragment 接收结果,这类似于 Activity 的 startActivityForResult 方法。

Android Dev Summit 2019 - Architecture Components 中,2:43 的时候,有一个问题是针对 Android 开发者的。

我们是否可以为 Navigation Controller 创建像 startFragmentForResult 这样的方法?

答案是他们正在开发这个功能,并且将来会提供此功能。

回到你的问题,这是我的解决方案。

步骤1:创建一个名为 SharedViewModel 的类

class SharedViewModel : ViewModel() {

    // This is the data bundle from fragment B to A
    val bundleFromFragmentBToFragmentA = MutableLiveData<Bundle>()
}

步骤二:将以下代码添加到 FragmentA 中

private lateinit var viewModel: SharedViewModel

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

    viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class.java)
    viewModel.bundleFromFragmentBToFragmentA.observe(viewLifecycleOwner, Observer {
        // This will execute when fragment B set data for `bundleFromFragmentBToFragmentA`
        // TODO: Write your logic here to handle data sent from FragmentB
        val message = it.getString("ARGUMENT_MESSAGE", "")
        Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
    })
}

第三步:将以下代码添加到FragmentB中

// 1. Declare this as class's variable
private lateinit var viewModel: SharedViewModel

// 2. Use the following code when you want to return FragmentA           
// findNavController().navigate(R.id.action_from_B_to_A) // Do not use this one

// Set data for `bundleFromFragmentBToFragmentA`
val data = Bundle().apply { putString("ARGUMENT_MESSAGE", "Hello from FragmentB") }
viewModel.bundleFromFragmentBToFragmentA.value = data

// Pop itself from back stack to return FragmentA
requireActivity().onBackPressed()

1
嘿,我按照你说的做了,但我的片段仍然无法保存视图状态。我需要保留我在问题中发布的代码吗? - Amol Borkar
@AmolBorkar,你可以展示一下fragment_professional_details.xml文件和对应的fragment类吗? - Son Truong
我已经重新添加了问题中的代码,现在它可以正常工作了,谢谢! - Amol Borkar
不错的解决方法,但我真的希望他们能够修复它。 - apex39
1
最后,我们现在可以使用startFragmentForResult了!)) - Husniddin Muhammad Amin
@SonTruong 你好,我遇到了同样的问题(在片段之间不发送数据),但是我有一个底部导航栏,并且通过导航组件的帮助,在片段之间进行切换。我认为新实例一直在被创建,所以我无法恢复我的片段状态。如何从堆栈中弹出我的片段以恢复状态?我不懂Java,请帮忙。 - Mohammed

10

小提示:如果您的视图没有ID,状态将无法保留!

视图需要一个ID来保持其状态。此ID必须在片段及其视图层次结构中是唯一的。没有ID的视图无法保持其状态。

请查看Google官方文档视图状态


3
可以详细解释一下吗? - obey
@遵守,这在我的情况下发生了。 - Ali Zarei
这对于EditText有效,但TextView会不断失去其状态。 - Moti Bartov
1
它对我没用。我在片段布局中为每个视图设置了ID,但仍然看起来像片段正在创建新的视图/状态。顺便说一下,我正在使用导航组件,如果这能给你任何线索,说明实际情况是什么。 - StackOverflower
这个答案解决了我的问题,我一整天都在调试这个问题。当我返回到一个Fragment时,我一直失去复选框的选中状态。我将视图的ID更改为唯一的一个,我的状态得到了保存。谢谢你。 - Ahmad Hamwi

9
如果您想在返回时保持滚动视图的位置,请确保为视图添加ID,例如NestedScrollView,即使在代码级别上不需要它。
android:id="@+id/some_id"

这样,当从回退栈返回Fragment时,滚动位置将保持不变。

非常适合保存滚动状态。 - Sagar G.
我已经为片段中的每个视图设置了“id”,但不起作用。我有什么遗漏吗? - StackOverflower
我需要提到ID必须是唯一的。我有一个ID,但状态仍然丢失。这是因为在同一UI中存在我的ID的副本。 - Ahmad Hamwi

8
在 frag A 中,我创建了两个全局变量。
private var mRootView: ViewGroup? = null
private var mIsFirstLoad = false

在A片段的onCreateView()方法中,我编写了以下代码:
_binding = FragmentDashBoardBinding.inflate(inflater, container, false)

    if (mRootView == null) {
        mRootView = _binding?.root
        mIsFirstLoad = true
    } else {
        mIsFirstLoad = false
    }
    return mRootView

在 A Fragment 的 onViewCreated() 方法中,我检查了 "mIsFirstLoad" 的值。
if(mIsFirstLoad) {
    initAdapter()
    getMovies()
} else {
    //Continue with previously initialised variables. e.g. adapter   
}

0

我找到了一种替代方法,只需清除所有的后退栈并使用saveState true返回到上一个目标,这对我起作用了

fragmentOtpBinding.verifyOtpButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Navigation.findNavController(view).popBackStack(R.id.destinationFragmentId,false,true);
        }
    });

如果在popBackStack()方法中也设置inclusive为true,那么当您想要转到其他目的地时,您的应用程序将崩溃,并显示错误消息“无法从当前目标NavGraph找到操作”。这与编程有关。

如果用户点击他/她设备的返回按钮,那么会发生什么? - StackOverflower

0

请查看谷歌文档的链接

如果您的操作是从fragmentA到fragmentB再到fragmentC,然后从fragmentC返回到fragmentA,您想要删除fragmentC和fragmentB并保留fragmentA的状态。

所以您应该:

  1. 添加一个从fragmentC到fragmentA的操作
  2. 将操作设置为“popUpTo”为fragmentA,并将popUpToInclusive设置为true。

enter image description here


0

你可以使用基础片段,但这只是一种解决方法。实际上,导航组件仍然存在缺陷。这里有一个在 GitHub 上的问题

class SearchFragment : BaseBottomTabFragment() {

    private var _binding: FragmentSearchBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentSearchBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.buttonDynamicTitleNavigate.setOnClickListener {
            navigateWithAction(
                SearchFragmentDirections.actionSearchFragmentToDynamicTitleFragment(
                    binding.editTextTitle.text.toString()
                )
            )
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

代码片段来自:这个项目


1
确实有漏洞。 - Akash Gorai
对我来说,它起作用了。 - Андрей Шпилевой

-1

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