I use navigation component and I faced a pretty interesting problem with fragments: whenever I open fragment B from fragment A and then go back to fragment A, fragment A's view state is lost. I mean the view is being created again. The same thing happens if I use FragmentTransaction instead of navigation component. When I detach the fragment and then attach again, the view is being created again. Is there any possible way I can prevent the view from being destroyed or is there any way I can save the state of the view?
UPDATE
I wrote a simple code to demonstrate this behavior:
activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/navHostView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.navHostView) as NavHostFragment?
val navController = navHostFragment!!.navController
navController.setGraph(R.navigation.navigation_main)
}
}
The navigation itself:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation_main"
app:startDestination="#id/fragmentA">
<fragment
android:id="#+id/fragmentA"
android:name="com.sever.fragmentviewtest.FragmentA"
android:label="FragmentA"
tools:layout="#layout/fragment_a">
<action
android:id="#+id/action_fragmentA_to_fragmentB"
app:destination="#id/fragmentB" />
</fragment>
<fragment
android:id="#+id/fragmentB"
android:name="com.sever.fragmentviewtest.FragmentB"
android:label="FragmentB"
tools:layout="#layout/fragment_b" />
</navigation>
Fragment A
class FragmentA : Fragment() {
private var shouldSetText = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
retainInstance = true
val view = inflater.inflate(R.layout.fragment_a, container, false)
if (shouldSetText) { // Will be called only once
view.findViewById<TextView>(R.id.tvTest).text = "Some text"
shouldSetText = false
}
view.rootView.setOnClickListener {
findNavController().navigate(R.id.action_fragmentA_to_fragmentB)
}
return view
}
}
Fragment B
class FragmentB : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
view.rootView.setOnClickListener {
findNavController().navigateUp()
}
return view
}
}
Important
The FragmentA's onCreateView is being called whenever I navigate back from FragmentB. And if I made some changes to FragmentA's views (like set text to a TextView) - they are lost after popping back stack from FragmentB. I tried to use navigateUp - the behavior is the same. And also retainInstance have not helped me to solve the issue.
Related
I have an app that uses NavigationComponents among with ViewPager 2.
I'd like to use ViewPager for switching between fragments. I've made my ActivityMain as FragmentContainerView, and I'd like to have ViewPager implemented in one of my fragments.
The problem is, the ViewPager doesn't work at all. It doesn't change fragments, don't know why. What should I change in the code?
ActivityMain
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
First fragment
class BlankFragment : Fragment() {
private var mPag: ViewPager2? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_blank, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mPag = view.findViewById(R.id.pager123)
val adapter = PagerAdapter(this)
val list = mutableListOf<Fragment>()
list.add(BlankFragment())
list.add(BlankFragment2())
mPag?.adapter = adapter
}
}
.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment"
android:orientation="vertical">
<TextView
android:id="#+id/test123"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="111111111" />
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager123"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#a1a1"/>
</LinearLayout>
Second fragment
class BlankFragment2 : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank2, container, false)
}
}
.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment2">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="222222222222222" />
</FrameLayout>
Nav graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/blankFragment">
<fragment
android:id="#+id/blankFragment"
android:name="com.example.myapplication.BlankFragment"
android:label="fragment_blank"
tools:layout="#layout/fragment_blank" />
<fragment
android:id="#+id/blankFragment2"
android:name="com.example.myapplication.BlankFragment2"
android:label="fragment_blank2"
tools:layout="#layout/fragment_blank2" />
</navigation>
Pager adapter
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
val mFragments = mutableListOf<Fragment>()
override fun getItemCount(): Int {
return mFragments.size
}
override fun createFragment(position: Int): Fragment {
when (position){
0 -> return BlankFragment()
1 -> return BlankFragment2()
}
return mFragments[position]
}
}
What you are essentially missing is what I tried to explain in the previous question.
You need to use Navigation component and ViewPager2 as two separate entities.
There is also issue with your PagerAdapter. Fragment adapters are not similar to other Adapters you might have experience with(mFragments).
They shouldn't hold a reference to those fragments. There are two main Fragment adapters(more here).
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position){
0 -> BlankFragment1()
1 -> BlankFragment2()
else -> throw IllegalArgumentException("Out of fragments, will depend on getItemCount")
}
}
}
You need to have Fragments that are solely in the Navigation component(e.g. FirstFragment & SecondFragment) and Fragments that belong to the ViewPager2(e.g. BlankFragment1 & BlankFragment2)
<?xml version="1.0" encoding="utf-8"?>
<navigation android:id="#+id/nav_graph"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="#id/FirstFragment">
<fragment
android:id="#+id/FirstFragment"
android:name="com.example.myapplication.FirstFragment"
android:label="#string/first_fragment_label"
tools:layout="#layout/fragment_first" >
<action
android:id="#+id/action_FirstFragment_to_SecondFragment"
app:destination="#id/SecondFragment" />
</fragment>
<fragment
android:id="#+id/SecondFragment"
android:name="com.example.myapplication.SecondFragment"
android:label="#string/second_fragment_label"
tools:layout="#layout/fragment_second" />
</navigation>
Fragments for ViewPager2:
class BlankFragment1: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_blank_1, container, false)
}
}
class BlankFragment2: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_blank_2, container, false)
}
}
The FirstFragment is going to host the ViewPager2 that will allow you to swipe between BlankFragment1 and BlankFragment2.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_first, container, false)
view.findViewById<ViewPager2>(R.id.vp2)?.let { viewPager2 ->
val pagerAdapter = PagerAdapter(this)
viewPager2.adapter = pagerAdapter
}
return view
}
Now the only thing you need to do in any of the fragments to use Navigation component is to simply findNavControler and navigate to destination you want.
class BlankFragment1: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_blank_1, container, false)
view.findViewById<MaterialButton>(R.id.blank_1_button).setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
return view
}
}
This is my FragmentTwo fragment class code.
class FragmentTwo : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
// Inflate the layout for this fragment
val binding : FragmentTwoBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_two, container, false)
var args = FragmentTwoArgs.fromBundle(arguments)
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater?.inflate(R.menu.overflow_menu,menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.onNavDestinationSelected(item!!,findNavController())
|| super.onOptionsItemSelected(item)
}
}
This is my FragmentOne fragment class code:
class FragmentOne : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_one, container, false)
val binding: FragmentOneBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_one, container, false)
binding.clickable = this
return binding.root
}
fun onClicking() {
//Toast.makeText(activity, "You clicked me.", Toast.LENGTH_SHORT).show()
//findNavController().navigate(R.id.action_fragmentOne_to_fragmentTwo)
findNavController().navigate(FragmentOneDirections.actionFragmentOneToFragmentTwo())
}
}
And this is my Navigation xml code.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation"
app:startDestination="#id/fragmentOne">
<fragment
android:id="#+id/fragmentOne"
android:name="com.example.fragmentpractise1.FragmentOne"
android:label="fragment_one"
tools:layout="#layout/fragment_one" >
<action
android:id="#+id/action_fragmentOne_to_fragmentTwo"
app:destination="#id/fragmentTwo" />
<argument
android:name="numViews"
app:argType="integer"
android:defaultValue="18" />
</fragment>
<fragment
android:id="#+id/fragmentTwo"
android:name="com.example.fragmentpractise1.FragmentTwo"
android:label="fragment_two"
tools:layout="#layout/fragment_two" />
<fragment
android:id="#+id/aboutFragment"
android:name="com.example.fragmentpractise1.AboutFragment"
android:label="fragment_about"
tools:layout="#layout/fragment_about" />
</navigation>
Now I am getting an error in FragmentTwo class code as FragmentTwoArgs class is not generated while assigning it to args variable. I am using Nav safe args and defined argument value through Nav graph in FragmentOne.
Any help would be appreciated.
You're using wrong Fragment to declare the arguments. If you want FragmentTwo to have arguments you should use the fragment in the navigation xml:
<fragment
android:id="#+id/fragmentTwo"
android:name="com.example.fragmentpractise1.FragmentTwo"
android:label="fragment_two"
tools:layout="#layout/fragment_two">
<argument
android:name="numViews"
app:argType="integer"
android:defaultValue="18" />
</fragment>
You might also want to use the lazy delegate navArgs():
private val args: FragmentTwoArgs by navArgs()
FragmentTwo fragment class code:
class FragmentTwo : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
// Inflate the layout for this fragment
val binding : FragmentTwoBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_two, container, false)
var args = FragmentTwoArgs.fromBundle(arguments)
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater?.inflate(R.menu.overflow_menu,menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.onNavDestinationSelected(item!!,findNavController())
|| super.onOptionsItemSelected(item)
}
}
FragmentOne fragment class code:
class FragmentOne : Fragment() {
var nameValue = "Abhas"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_one, container, false)
val binding: FragmentOneBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_one, container, false)
binding.clickable = this
binding.button.setOnClickListener {
findNavController().navigate(FragmentOneDirections.actionFragmentOneToFragmentTwo())
}
return binding.root
}
}
navigation xml code :
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation"
app:startDestination="#id/fragmentOne">
<fragment
android:id="#+id/fragmentOne"
android:name="com.example.fragmentpractise1.FragmentOne"
android:label="fragment_one"
tools:layout="#layout/fragment_one" >
<action
android:id="#+id/action_fragmentOne_to_fragmentTwo"
app:destination="#id/fragmentTwo" />
</fragment>
<fragment
android:id="#+id/fragmentTwo"
android:name="com.example.fragmentpractise1.FragmentTwo"
android:label="fragment_two"
tools:layout="#layout/fragment_two" >
<argument
android:name="nameValue"
app:argType="string" />
</fragment>
<fragment
android:id="#+id/aboutFragment"
android:name="com.example.fragmentpractise1.AboutFragment"
android:label="fragment_about"
tools:layout="#layout/fragment_about" />
</navigation>
Now when I am setting args variable in FragmentTwo class, arguments showing error in fromBundle(arguments). I have tried giving argument in setOnclicklistener in FragmentOne while navigating but it's not asking for any kind of value in constructor. I am unable to understand why
arguments in fromBundle(arguments) of FragmentTwo class showing error.
Looks like you forget to declare argument inside the action(fragment one).
And you don't send anything from fragment_one.
You should add argument to the action inside the fragment_one in navigation xml :
<action
android:id="#+id/action_fragmentOne_to_fragmentTwo"
app:destination="#id/fragmentTwo">
<argument
android:name="nameValue"
app:argType="string"
android:defaultValue="default" />
</action>
then re-build the app - another navigation action method will be generated with a string argument.
FragmentOneDirections.actionFragmentOneToFragmentTwo(nameValue : String)
So you should put the value to this method in fragment one.
You can find detailed documentation by the link.
I cannot understand what is the reason for the error java.lang.NullPointerException: null cannot be cast to non-null type androidx.navigation.fragment.NavHostFragment
This is my host fragment:
class HostFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_host, container, false)
// error here
val navHostFragment = requireActivity().supportFragmentManager.findFragmentById(R.id.hostFragment) as NavHostFragment
//
val navController = navHostFragment.navController
root.host_text_view.setOnClickListener {
Log.d("Tag", "clicked")
navController.navigate(R.id.action_hostFragment_to_secondFragment)
}
return root
}
}
I want to navigate to SecondFragment by clicking on the textView. And back.
And nav graph .xml file:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/hostFragment">
<fragment
android:id="#+id/hostFragment"
android:name="com.example.test.HostFragment"
android:label="fragment_host"
tools:layout="#layout/fragment_host" >
<action
android:id="#+id/action_hostFragment_to_secondFragment"
app:destination="#id/secondFragment" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.example.test.SecondFragment"
android:label="fragment_second"
tools:layout="#layout/fragment_second" >
<action
android:id="#+id/action_secondFragment_to_hostFragment"
app:destination="#id/hostFragment" />
</fragment>
</navigation>
The Navigation graph has its final form like:
best way to use Navigation Graph is after the view is created, since some view takes time to create so u might get this exception as your view is still in processing or creating the view.
so to avoid this , u can use OnViewCreated() as
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
view.imgNumbrLookUp.setOnClickListener {
navController.navigate(R.id.action_homeFragment_to_webActivity)
}
}
You don't need to find your navHostFragment to navigate
val root = inflater.inflate(R.layout.fragment_host, container, false)
root.host_text_view.setOnClickListener {
Log.d("Tag", "clicked")
if (findNavController().currentDestination?.id == hostFragment)
findNavController().navigate(R.id.action_hostFragment_to_secondFragment)
}
You can check for Kotlin methods to navigate
https://developer.android.com/guide/navigation/navigation-navigate
While creating a very simple sample app, I couldn't wrap my head around why my app is closing when I press the hardware back button on my emulator.
I have 1 mainActivity and 2 fragments.
When I am on the NavigationFragment and press back, the app closes instead of going back to IntermediateFragment.
MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
toolbar.setTitle(R.string.app_name)
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.exampleapplication.MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
/>
<fragment
android:id="#+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/main_nav"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
navigation_graph:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/main_nav"
app:startDestination="#+id/intermediateFragment">
<fragment
android:id="#+id/intermediateFragment"
android:name="com.exampleapplication.IntermediateFragment">
<action
android:id="#+id/action_intermediate_to_navigation"
app:destination="#+id/navigationFragment"
/>
</fragment>
<fragment
android:id="#+id/navigationFragment"
android:name="com.exampleapplication.NavigationFragment"
/>
</navigation>
IntermediateFragment:
class IntermediateFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_intermediate, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_next_fragment.setOnClickListener {
findNavController().navigate(R.id.action_intermediate_to_navigation)
}
}
}
NavigationFragment:
class NavigationFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_navigation, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_first_library.setOnClickListener {
findNavController().setGraph(R.navigation.first_library_nav)
}
btn_download_pdf.setOnClickListener {
findNavController().setGraph(R.navigation.download_pdf_nav)
}
}
}
Any ideas?
You're missing one line on your <fragment>:
app:defaultNavHost="true"
As per the Navigation Getting Started guide:
The app:defaultNavHost="true" attribute ensures that your NavHostFragment intercepts the system Back button.
Since you don't set that, Navigtion does not intercept the back button and hence, you only get the default activity behavior (which is closing your activity).