How can I keep data when fragment is replaced? - android

I use the navigation component to do various screen transitions.
Pass the title data from A fragment to B fragment at the same time as the screen is switched. (using safe args)
In fragment B, set the data received from A.
And to keep the title data even when the screen is switched, I set it in LiveData in the ViewModel.
But if you go back from fragment B to fragment C,
B's title is missing.
Some say that because this is a replace() method, a new fragment is created every time the screen is switched.
How can I keep the data even when I switch screens in the Navigation Component?
Note: All screen transitions used findNavController.navigate()!
fragment A
startBtn?.setOnClickListener { v ->
title = BodyPartCustomView.getTitle()
action = BodyPartDialogFragmentDirections.actionBodyPartDialogToWrite(title)
findNavController()?.navigate(action)
}
fragment B
class WriteRoutineFragment : Fragment() {
private var _binding: FragmentWritingRoutineBinding? = null
private val binding get() = _binding!!
private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
private val args : WriteRoutineFragmentArgs by navArgs() // When the screen changes, it is changed to the default value set in <argument> of nav_graph
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.setValue(args) // set Data to LiveData
viewModel.title.observe(viewLifecycleOwner) { titleData ->
// UI UPDATE
binding.title.text = titleData
}
}
UPDATED Navigation Graph.xml
<?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/calendar">
<!-- fragment A -->
<dialog
android:id="#+id/bodyPartDialog"
android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
android:label="BodyPartDialogFragment"
tools:layout="#layout/fragment_body_part_dialog">
<action
android:id="#+id/action_bodyPartDialog_to_write"
app:destination="#id/write"/>
</dialog>
<!-- fragment B -->
<fragment
android:id="#+id/write"
android:name="com.example.writeweight.fragment.WriteRoutineFragment"
android:label="WritingRoutineFragment"
tools:layout="#layout/fragment_writing_routine">
<action
android:id="#+id/action_write_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="title"
app:argType="string"
android:defaultValue="Unknown Title" />
</fragment>
<!-- fragment C -->
<fragment
android:id="#+id/workoutListTabFragment"
android:name="com.example.writeweight.fragment.WorkoutListTabFragment"
android:label="fragment_workout_list_tab"
tools:layout="#layout/fragment_workout_list_tab" >
<action
android:id="#+id/action_workoutListTabFragment_to_write"
app:destination="#id/write"
app:popUpTo="#id/write"
app:popUpToInclusive="true"/>
</fragment>
</navigation>
UPDATED ViewModel(
This is the view model for the B fragment.)
class WriteRoutineViewModel : ViewModel() {
private var _title: MutableLiveData<String> = MutableLiveData()
val title: LiveData<String> = _title
fun setValue(_data: WritingRoutineFragmentArgs) {
_title.value = _data.title
}
}
Error
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.writeweight, PID: 25505
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:52)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
at com.example.writeweight.fragment.WriteRoutineFragment.getArgs(Unknown Source:4)
at com.example.writeweight.fragment.WriteRoutineFragment.onViewCreated(WriteRoutineFragment.kt:58)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8512)
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: java.lang.IllegalArgumentException: Required argument "title" is missing and does not have an android:defaultValue
at com.example.writeweight.fragment.WriteRoutineFragmentArgs$Companion.fromBundle(WriteRoutineFragmentArgs.kt:26)
at com.example.writeweight.fragment.WriteRoutineFragmentArgs.fromBundle(Unknown Source:2)

A single ViewModel can also be used for multiple fragments. Fragments are obviously showing inside an activity. The ViewModel in the activity can be passed to each fragment, that has the reference from the title. It is the solution if you want to solve using ViewModel.
Otherwise you can try the savedInstance method for solving this issue. Here is a thread about it.

Following from my comment:
I would make the title argument nullable and pass null as the default value. Then in viewModel.setValue(), ignore it if it's null instead of passing it to the LiveData.
The ViewModel's setValue() function should look like:
fun setValue(_data: WritingRoutineFragmentArgs) {
_data.title?.let { _title.value = it }
}
so the value is only passed along to the LiveData if it is not the default (null).
Your xml for the value should mark the default as #null and needs to have nullable="true". Your stack trace looks like there was a problem with how you specified the default or making it nullable.
<argument
android:name="title"
app:argType="string"
app:nullable="true"
android:defaultValue="#null" />
IMO, for proper separation of concerns, the ViewModel should not have any awareness of navigation arguments. The setValue function should take a String parameter, and you should decide in the fragment whether to update the ViewModel. Like this:
// In ViewModel
fun setNewTitle(title: String) {
_title.value = title
}
// in Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
args.title?.let { viewModel.setNewTitle(it) } // set Data to LiveData
viewModel.title.observe(viewLifecycleOwner) { titleData ->
// UI UPDATE
binding.title.text = titleData
}
}

Related

pass arguments to fragments inside Bottom Navigation using navArgs

From Android Studio, I selected Bottom Navigation Activity project. I can compile and successfully run the generated app without any problem. However, I wanted the fragments under the BottomNavigationView to receive arguments, so I added 2 arguments, email and jwt to the starting destination fragment. So here's how my mobile_navigation.xml looks like now:
<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/mobile_navigation"
app:startDestination="#+id/navigation_home">
<fragment
android:id="#+id/navigation_home"
android:name="com.example.testbottomnavigation.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home">
<argument
android:name="email"
app:argType="string" />
<argument
android:name="jwt"
app:argType="string" />
</fragment>
After that I added these codes to my HomeFragment
private val myArgs by navArgs<HomeFragmentArgs>()
override fun onCreateView(){
val email = myArgs.email
val jwt = myArgs.jwt
}
and tried to run the app. The app will the crash with the error
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.testbottomnavigation/com.example.testbottomnavigation.MainActivity}: android.view.InflateException: Binary XML file line #33 in com.example.testbottomnavigation:layout/activity_main: Binary XML file line #33 in com.example.testbottomnavigation:layout/activity_main: Error inflating class fragment
...
at com.example.testbottomnavigation.MainActivity.onCreate(MainActivity.kt:19)
...
Caused by: java.lang.IllegalArgumentException: Required argument "email" is missing and does not have an android:defaultValue
Basically the app will crash at the line binding = ActivityMainBinding.inflate(layoutInflater) under my MainActivity below
Here's my MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navView: BottomNavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_activity_main)
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications))
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
}
So looking at the code above, where and how should I set the arguments that I needed to pass to HomeFragment?
I think you just need to add a default value:
<argument
android:name="email"
app:argType="string"
android:defaultValue="abc#gmail.com"/>
You didnt tranfer any values,so you should use defultvalue on argument of navigation
And
You can see below link for transfer data and navigate
enter link description here

Why can't I get null as an argument when switching screens in navigation?

I use navigation and SafeArgs to switch screens and pass data.
When data is selected on screen A, data is transmitted at the same time as screen switching.
However, there are times when screen A switches to screen B without any action.
I decided to send null in this case.
But I keep getting the following error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.lightweight, PID: 9555
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:54)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:35)
at com.example.lightweight.fragment.WriteRoutineFragment.getArgs(WriteRoutineFragment.kt:20)
at com.example.lightweight.fragment.WriteRoutineFragment.onViewCreated(WriteRoutineFragment.kt:44)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2985)
navigation.xml
<fragment
android:id="#+id/writeRoutine"
android:name="com.example.lightweight.fragment.WriteRoutineFragment"
android:label="fragment_write_routine"
tools:layout="#layout/fragment_write_routine" >
<action
android:id="#+id/action_writeRoutineFragment_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="workout"
app:argType="string"
app:nullable="true" />
</fragment>
In Adapter
inner class ViewHolder(itemView: View, context: Context) : RecyclerView.ViewHolder(itemView) {
private val tv: TextView = itemView.findViewById(R.id.workout)
init {
tv.setOnClickListener { view ->
val workout = tv.text.toString()
val action: NavDirections = WorkoutListTabFragmentDirections.actionWorkoutListTabToWriteRoutine(workout)
view.findNavController().navigate(action)
}
}
fun bind(item: String) {
tv.text = item
}
}
what's the reason?
You should add defaultValue of the workaround argument. Here because you have set app:nullable="true", you should set android:defaultValue="#null".
Modify the way you create workaround argument as follows:
<argument
android:name="workout"
app:argType="string"
android:defaultValue="#null"
app:nullable="true" />
More information: Supported argument types - Navigation

How to bind viewmodel to SupportMapFragment

In my android application I created map fragment with MapsFragment class
class MapsFragment : Fragment() {
val callback = OnMapReadyCallback { googleMap ->
val sydney = LatLng(-34.0, 151.0)
googleMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
private val viewModel: MapViewModel by lazy {
ViewModelProvider(this).get(MapViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMapsBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
mapFragment?.getMapAsync(callback)
}
}
and fragment_maps.xml layout file
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="com.mobile.viewmodels.MapViewModel" />
</data>
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
</layout>
I use
buildFeatures {
dataBinding true
}
option for creating binding. Then when I try bind my MapViewModel in line
val binding = FragmentMapsBinding.inflate(inflater)
I get an error
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.mobile, PID: 331
java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:1201)
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:719)
at com.mobile.databinding.FragmentMapsBindingImpl.<init>(FragmentMapsBindingImpl.java:25)
at com.mobile.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:58)
at androidx.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:130)
at androidx.databinding.ViewDataBinding.inflateInternal(ViewDataBinding.java:1368)
at com.mobile.databinding.FragmentMapsBinding.inflate(FragmentMapsBinding.java:68)
at com.mobile.databinding.FragmentMapsBinding.inflate(FragmentMapsBinding.java:54)
at com.mobile.ui.MapsFragment.onCreateView(MapsFragment.kt:45)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2600)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:881)
at androidx.fragment.app.FragmentManagerImpl.addAddedFragments(FragmentManagerImpl.java:2100)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1874)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1830)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
I/Process: Sending signal. PID: 331 SIG: 9
How to bind viewModel class properly? Is it possible?
Sorry, I might be late to answer this question but as they say, "Better Late than never."
So for the past two days, I have been trying to do the same thing, i.e., using DataBinding and Maps Fragment together.
I tried using both the methods, i.e., DataBindingUtil and as you did FragmentMapsBinding.
So, there were many errors I can across while trying to implement Data Binding, for starters, there was no DataBinding class generated, which is usually not a problem, except in this case. So, the workaround was to force the compiler to generate DataBinding class by introducing a variable, in your case viewModel in the XML file.
Now the data binding class is finally generated but we get this funny error, saying
Index Out of Bound
Now looking at the last error line which was generated
java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
at androidx.databinding.ViewDataBinding.mapBindings(ViewDataBinding.java:1201)
and inspecting the code further inside the file ViewDataBinding.java, we see this.
if (isRoot && tag != null && tag.startsWith("layout")) {
final int underscoreIndex = tag.lastIndexOf('_');
if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
final int index = parseTagInt(tag, underscoreIndex + 1);
//ERROR IS GENERATED HERE
if (bindings[index] == null) {
bindings[index] = view;
}
indexInIncludes = includes == null ? -1 : index;
isBound = true;
} else {
indexInIncludes = -1;
}
}
Not what I think the problem was is that the bindings file which we had told the complier to forcefully generate doesn't really have any Views in it and the bindings index.
Data Binding Class -
No. of Views = 0
Index = 0
Now trying to access it gives an error.
BUT WAIT A MINUTE, one might say there is a view present, i.e., fragment. What about this?
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
After going through a few of the android docs, I think Fragment is not a View.
So, the workaround I would suggest is using either of the two methods.
replace fragment with androidx.fragment.app.FragmentContainerView -> This will remove the error and as the documentation says
FragmentContainerView is a customized Layout designed specifically for Fragments. It extends FrameLayout, so it can reliably handle Fragment Transactions, and it also has additional features to coordinate with fragment behavior.
have another view in your XML file, an example would be.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MapsFragment" />
</androidx.constraintlayout.widget.ConstraintLayout>
Or you can use both, that's up to you.
Hope this answers your question. Have a great day.
UPDATE :
As per Android Developer Docs:
Fragment
kotlin.Any
↳ androidx.fragment.app.Fragment
Fragment Container View
kotlin.Any
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.FrameLayout
↳ androidx.fragment.app.FragmentContainerView
It is evident that fragment does not have View as a SuperClass. Thus, this error is thrown.
The above 2 methods are the best ways to avoid this error since in DataBinding it requires at least 1 View.
Either change fragment tag with androidx.fragment.app.FragmentContainerView
or wrap around fragment with another View or ViewGroup like LinearLayout, FrameLayout, etc.

How to pass arguments to a fragment using bottom navigation view and Android Navigation component?

Is it possible to pass and access arguments in a fragment using a bottom navigation view and the Navigation component?
I'm using a one activity with many fragments approach where my top level fragment requires an argument(Usually done via the newInstance generated method). I've had a look at the Navigation component developer guide and the codelab but it only mentions using safeargs and adding argument tags in the destinations and actions.
Here's my navigation graph:
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="#id/homeFragment">
<fragment android:id="#+id/homeFragment"
android:name="uk.co.homeready.homeready.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home">
<!--Do I create an argument block here?-->
</fragment>
<fragment android:id="#+id/calculatorFragment"
android:name="uk.co.homeready.homeready.CalculatorFragment"
android:label="fragment_calculator"
tools:layout="#layout/fragment_calculator"/>
<fragment android:id="#+id/resourcesFragment"
android:name="uk.co.homeready.homeready.ResourcesFragment"
android:label="fragment_resources"
tools:layout="#layout/fragment_resources"/>
</navigation>
Bottom Navigation View menu:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/homeFragment"
android:icon="#drawable/ic_home_black_24dp"
android:title="#string/title_home"/>
<item
android:id="#+id/calculatorFragment"
android:icon="#drawable/ic_baseline_attach_money_24px"
android:title="#string/title_calculator"/>
<item
android:id="#+id/resourcesFragment"
android:icon="#drawable/ic_baseline_library_books_24px"
android:title="#string/title_resources"/>
</menu>
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
...
val navController = Navigation.findNavController(this,
R.id.nav_host_fragment)
bottom_navigation.setupWithNavController(navController)
....
}
activity_main.xml
<android.support.constraint.ConstraintLayout>
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintBottom_toTopOf="#id/bottom_navigation"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"/>
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation"
app:menu="#menu/bottom_navigation"/>
</android.support.constraint.ConstraintLayout>
HomeFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val argument = //TODO access argument here
...
}
If I understood you correctly, you want to pass arguments to destinations that is tied to menu items. Try to use 'OnDestinationChangedListener' inside your activity onCreate method, something like this:
navController.addOnDestinationChangedListener { controller, destination, arguments ->
when(destination.id) {
R.id.homeFragment -> {
val argument = NavArgument.Builder().setDefaultValue(6).build()
destination.addArgument("Argument", argument)
}
}
}
Update:
If you want that your start destination will receive default arguments the implementation should be different.
First, remove 'app:navGraph="#navigation/nav_graph"' from your 'NavHostFragment' xml tag.
Then, inside your activity onCreate you need to inflate the graph:
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.nav_graph)
Then add your arguments to graph(this arguments will be attached to start destination)
val navArgument1=NavArgument.Builder().setDefaultValue(1).build()
val navArgument2=NavArgument.Builder().setDefaultValue("Hello").build()
graph.addArgument("Key1",navArgument1)
graph.addArgument("Key2",navArgument2)
Then attach the graph to NavController:
navController.graph=graph
Now your first destination should receive the attached arguments.
The correct way to do this is indeed with an <argument> block on your destination.
<fragment android:id="#+id/homeFragment"
android:name="uk.co.homeready.homeready.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home">
<argument
android:name="Argument"
android:defaultValue="value"
/>
</fragment>
This will automatically populate the arguments of the Fragment with the default value without any additional code needed. As of Navigation 1.0.0-alpha09, this is true whether you use the Safe Args Gradle Plugin or not.
Default values was not usable for me, because I have dynamic menu items that could have multiple of the same destination with different arguments. (changed from server)
Implement BottomNavigationView.OnNavigationItemSelectedListener:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmentId = item.itemId
val arguments = argumentsByFragmentId[fragmentId] // custom mutableMapOf<Int, Bundle?>() with arguments
navController().navigate(fragmentId, arguments)
return true
}
To use that you will takeover the navigation, by replacing the listener. The order of calls here are important:
bottomNavigationView.setupWithNavController(navController)
bottomNavigationView.setOnNavigationItemSelectedListener(this)

How to navigate from nested Fragment to parent fragment using Jetpack Navigation?

I have main navigation:
SplashFragment -> RegistrationFragment -> RootFragment
<fragment
android:id="#+id/splashFragment"
android:name="com.low6.low6.features.splash.SplashFragment"
android:label="Splash"
tools:layout="#layout/fragment_splash" >
<action
android:id="#+id/action_next"
app:clearTask="true"
app:destination="#id/registrationFragment" />
</fragment>
<fragment
android:id="#+id/registrationFragment"
android:name="com.low6.low6.features.registration.RegistrationFragment"
android:label="Register">
<action
android:id="#+id/action_next"
app:clearTask="true"
app:destination="#id/rootFragment" />
</fragment>
<fragment
android:id="#+id/rootFragment"
android:name="com.low6.low6.core.RootFragment"
android:label="#string/home"
tools:layout="#layout/fragment_root" />
And I have nested registration navigation:
RegistrationPersonalFragment -> RegistrationContactFragment -> RegistrationSecurityFragment
<fragment
android:id="#+id/registrationPersonalFragment"
android:name="com.low6.low6.features.registration.RegistrationPersonalFragment"
android:label="Register">
<action
android:id="#+id/action_next"
app:destination="#+id/registrationContactFragment" />
</fragment>
<fragment
android:id="#+id/registrationContactFragment"
android:name="com.low6.low6.features.registration.RegistrationContactFragment"
android:label="Register">
<action
android:id="#+id/action_next"
app:destination="#+id/registrationSecurityFragment" />
</fragment>
<fragment
android:id="#+id/registrationSecurityFragment"
android:name="com.low6.low6.features.registration.RegistrationSecurityFragment"
android:label="Register">
<action
android:id="#+id/action_next"
app:destination="#+id/rootFragment" />
</fragment>
How to redirect from the last nested RegistrationSecurityFragment to RootFragment using Jetpack Navigation component?
Currently
<action
android:id="#+id/action_next"
app:destination="#+id/rootFragment" />
And
navigateTo(R.id.action_next)
Gives me
java.lang.IllegalArgumentException: navigation destination com.xxx:id/rootFragment referenced from action com.xxx:id/action_next is unknown to this NavController
at androidx.navigation.NavController.navigate(NavController.java:691)
at androidx.navigation.NavController.navigate(NavController.java:648)
at androidx.navigation.NavController.navigate(NavController.java:634)
at com.xxx.core.BaseFragment.navigateTo(BaseFragment.kt:73)
at com.xxx.core.BaseFragment.navigateTo$default(BaseFragment.kt:66)
at com.xxx.features.registration.RegistrationSecurityFragment$epoxyController$1$$special$$inlined$button$lambda$1.onClick(RegistrationSecurityFragment.kt:106)
at android.view.View.performClick(View.java:6597)
at android.view.View.performClickInternal(View.java:6574)
at android.view.View.access$3100(View.java:778)
at android.view.View$PerformClick.run(View.java:25885)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
If you have more than one navigation graph, please make sure you're using the right navigation controller. Using Navigation.findNavController(view) in some cases you might need to get your root view to get the root's navigation. Hope, this'll help.
When you have nested NavControllers, findNavController() return only last. To get previous navControllers, you can traverse up using parentFragment property.
Extensions with this approach:
// find all nav controllers from closest to farest
fun Fragment.findAllNavControllers(): List<NavController> {
val navControllers = mutableListOf<NavController>()
var parent = parentFragment
while (parent != null) {
if (parent is NavHostFragment) {
navControllers.add(parent.navController)
}
parent = parent.parentFragment
}
return navControllers
}
// find one nav controller by fragment id
fun Fragment.findNavControllerById(#IdRes id: Int): NavController {
var parent = parentFragment
while (parent != null) {
if (parent is NavHostFragment && parent.id == id) {
return parent.navController
}
parent = parent.parentFragment
}
throw RuntimeException("NavController with specified id not found")
}
And usage:
findAllNavControllers()[2]
findNavControllerById(R.id.navHostFragment)
In your code, you can pass the resource ID of the global action to the navigate() method for each UI element.
your_button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Navigation.findNavController(view).navigate(R.id.main_fragment);
}
});
Thanks to #Vladimir answer I came up with this solution
val mainNavView = requireActivity().findViewById<View>(R.id.mainNavFragment)
Navigation.findNavController(mainNavView).navigate(R.id.action_next)
You can do it like this ;
in Child Fragment
// parentFragment = getParentFragment() for java
(parentFragment as MyParentFragment).myNavigationHandler(myArguments)
in Parent Fragment
fun myNavigationHandler(myArguments) {
Navigation.findNavController(binding.root)
.navigate(MyFragmentDirections.actionMyAction(myArguments))
}
findNavController().navigate(HostFragmentDirections.actionHostToOtherFragment())
HostFragment is just an example, which should be the name of host of your nested fragment.
For everyone struggling with navigating to somewhere from your nested fragment, this works.

Categories

Resources