42

I'm trying to create a single activity app using android architecture components. I have a fragment A which has some textfields, when user pushes a button I navigate to fragment B where he uploads and edits some images after that app navigates back to A using code like this:

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

When navigating back B passes some data to A using dataBundle. The problem with this is all the textfields reset because Fragment A is basically recreated from scratch. I read somewhere that a developer from google suggests that you can just save view in a var instead of inflating it everytime. So tried doing that:

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
    }
}

But this does not work, all the textfields reset when navigating back to A. What am I doing wrong? What is proper way of handling cases like this?

Amol Borkar
  • 2,321
  • 7
  • 32
  • 63

8 Answers8

14

I will answer your questions one by one.

But this does not work, all the textfields reset when navigating back to A. What am I doing wrong?

From FragmentB, when users finish their work and the app call the below method to return FragmentA.

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

You expected that the app will bring users back to FragmentA, but the actual result is a new FragmentA is created and put on the top of the back stack. Now the back stack will be like this.

FragmentA (new instance)
FragmentB
FragmentA (old instance)

That why you see all textfields reset, because it is a totally new instance of FragmentA.

What is proper way of handling cases like this?

You want to start a fragment, then receive result form that fragment, it seems like startActivityForResult method of Activity.

In Android Dev Summit 2019 - Architecture Components, at 2:43, there is a question for Android developers.

Can we have something like startFragmentForResult for the Navigation Controller?

The answer is they are working on it, and this feature will be available in future.

Back to your problem, here is my solution.

Step 1: Create a class called SharedViewModel

class SharedViewModel : ViewModel() {

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

Step 2: Add these lines of code to 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()
    })
}

Step 3: Add these lines of code to 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()
Son Truong
  • 13,661
  • 5
  • 32
  • 58
  • 1
    Hey, I'm doing exactly as you've said but my fragment still doesn't save view state. Do i need to keep the code i posted in my question? – Amol Borkar Jan 10 '20 at 19:11
  • @AmolBorkar Can you show fragment_professional_details.xml file and the fragment class as well. – Son Truong Jan 11 '20 at 00:42
  • I've added the code in the question back, it works fine now, Thanks! – Amol Borkar Jan 11 '20 at 12:47
  • Nice workaround, but I really wait until they will fix it – apex39 Feb 26 '21 at 16:00
  • 1
    And finally we can use startFragmentForResult now! )) – Husniddin Muhammad Amin Apr 01 '21 at 20:28
  • @SonTruong Hi, I am facing the same problem (I do not send data between fragments) but I have a bottom navigation bar and with help on navigation component I switch between fragments. I think new instance keeps getting created so I cannot restore my fragments state. How can I pop my fragment from stack to restore state? I do not understand Java so please help – Mohammed Jun 09 '21 at 09:16
10

Just a little thing, if your views don't have id, state will not be kept!

A view needs an ID to retain its state. This ID must be unique within the fragment and its view hierarchy. Views without an ID cannot retain their state.

Have a look at google official docs View state

Ali Zarei
  • 3,523
  • 5
  • 33
  • 44
  • 3
    Care to elaborate? – obey Dec 10 '20 at 20:00
  • @obey this happened in my case – Ali Zarei Dec 11 '20 at 09:39
  • This worked for me for EditText!, TextView keep loosing its state. – Moti Bartov Jan 21 '21 at 11:49
  • 1
    it did not work for me. i set id to every view i have in my fragment layout but still it looks like fragment is making new view/state. i'm using navigation component by the way if it gives you any clue whats really going on. – StackOverflower Feb 01 '23 at 06:29
  • This answer solved my issue, I've been debugging the problem all day. I've been losing my checkbox check state when I'm navigating back to a fragment. I changed the ID of the view to a unique one and my state is saved. THANK YOU. – Ahmad Hamwi Apr 26 '23 at 16:44
9

If you want to keep scroll view's position after popping back, make sure you put ID to views such as NestedScrollView even if it's not needed on the code level:

android:id="@+id/some_id"

This way when the Fragment returned from the backstack, scroll positions are maintained.

Jim Ovejera
  • 739
  • 6
  • 10
8

In frag A, I create two global variables

private var mRootView: ViewGroup? = null
private var mIsFirstLoad = false

In onCreateView() of frag A, I write

_binding = FragmentDashBoardBinding.inflate(inflater, container, false)

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

In onViewCreated() of frag A, I check the value of "mIsFirstLoad"

if(mIsFirstLoad) {
    initAdapter()
    getMovies()
} else {
    //Continue with previously initialised variables. e.g. adapter   
}
Vaibhav
  • 101
  • 1
  • 2
0

You can use base fragment, but it's just workaround. Actually, navigation component is still buggy. Here is an issue on GitHub. Example:

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
    }
}

Code snipped from: This project

Kuvonchbek Yakubov
  • 558
  • 1
  • 6
  • 16
0

Check this link of Google document.

If your action is fragmentA -> fragmentB -> fragmentC. then from fragmentC back to fragmentA, you want to remove fragmentC, fragmentB and keep fragmentA's state.

So you should:

  1. add an action from fragmentC to fragmentA
  2. set the action that "popUpTo" be fragmentA and popUpToInclusive be true.

enter image description here

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Ho Seok
  • 57
  • 1
0

I found an alternative i simply cleared all the backstack and go to privious destination with saveState true, this worked for me

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

if you do inclusive also true in popBackStack() method then your app will crash ( when you want to go to other destination ) with error "action cannot be found from the current destination NavGraph"

-1

You need create ViewModels scoped to a Navigation Graph.

Check this nice guide and you can keep your navigation component. Its very easy to implement and works for me!! https://medium.com/sprinthub/a-step-by-step-guide-on-how-to-use-nav-graph-scoped-viewmodels-cf82de4545ed

Andy
  • 751
  • 1
  • 12
  • 25