Empty state of NavHostFragment of Android Navigation Component - android

Background
In app I have couple of NavHostFragments that are displayed in different parts of screen:
defaultNavHost is used for switching between tabs
inside one of fragments of defaultNavHost, I have another navHost for BottomSheets
There is a need for another navHost that could display fragments on top of everything else (above tabs, above both of other navHosts). Imagine a tablet layout, that displays a fragment on top of another fragment, just by covering 50% of it.
Issue
A secondary NavHostFragment, as declated below, works as expected when setting nav graph to it.
<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="false" />
I set the graph and start destination programmatically:
val navController = findNavController(R.id.fragmentContainerView)
val graph = navController.navInflater.inflate(R.navigation.navigation_additional)
graph.setStartDestination(R.id.navigation_additional_destination_5)
navController.graph = graph
What would be a correct approach to hide navController or navGraph? I want it to dissapear when I do navController.popBackStack(), but it does not work. If I just set fragmentContainerView to gone then fragment will still remain in that NavHostFragment and dissapear animation will not happen. What could be the correct approach to do this? Should the startDestination point to an empty fragment? Or is there a different architectural approach I should be using?

Related

Changing theme at runtime from fragment in android kotlin

I have created an activity. The activity's layout contains FragmentContainerView element.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/main_navigation_graph" />
I am showing a fragment into this container using the following code,
val navHostFragment = activity?.supportFragmentManager?.findFragmentById(R.id.nav_host_container) as NavHostFragment
val navController = navHostFragment.navController
navController.navigate(destinationID)
Here i am showing a fragment named as HomeFragment. The home fragment layout contains a linear layout. 50% of space is given to ViewPager2 and 50% is given to a group of 4 buttons, both these layouts are separated vertically. Currently there are multiple items in my viewpager2 and i am looking to change the Ui of these 4 buttons according to the item in the viewpager2. The meaning of changing Ui is to change/switch the entire theme of the fragment or activity at runtime. I have been checking solutions on stack where answers are given to change the theme using styles in OnCreateView before super is called. However it is quite complicated to change the style as i have only one fragment loaded in ContainerView. Hence my question is, can we apply a style to the layout and just change the style of the entire layout on runtime easily or is there any better solution to this ?

How to use the Navigation Graph without loading a fragment by default?

I need to use the navigation component and have it already set up. The navigation graph requires a startDestination which loads that fragment by default upon creation. However, I have a fragment which I only want to show after an event is triggered within the existing screen. How could I leave the fragment container empty until I'm ready to navigate to it?
in your xml file declare your container as empty navigation host
<fragment
android:id="#+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
then in your activity you can set your graph progmatically
val navHost = NavHostFragment()
fragmentManager.beginTransaction()
.replace(R.id.container, navHost)
.commit()
val graph = navHost.navController.navInflater.inflate(R.navigation.my_navigation)
graph.startDestination = R.id.my_fragment
navHost.navController.graph = graph
you can check this link

Android Navigation: Access NavHostFragment from same Fragment

Currently I have the following scenario: a user must sign in to use the app. This means I've used 2 nav_graphs, a main graph for everything and then a nested home graph for the views after you have signed in.
After signing in, a bottom navigation bar should appear to change tabs in the home graph.
I have the following home_fragment.xml:
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<fragment
android:id="#+id/home_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/home_graph"
app:defaultNavHost="true"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
app:menu="#menu/navigation_items"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And I want to be able to change tabs in the bottom navigation view, so I setup this logic in HomeFragement.kt using bottomNavigationView.setOnNavigationItemSelectedListener.
Unfortunetly, when I try to fetch the home_nav_host_fragment in home_fragment.xml I cannot because homeNavController = findNavController() in the fragment can only find the main_nav_host_fragment that's in the main activity.
I want findNavController() to return the home_nav_host_fragment instead but because this method only searches for parent NavControllers and not ones on the same level it cannot find the one I want.
Is there a better structure that will provide a solution to this issue? Thanks
This isn't the correct approach. Instead, you should listen for navigation events and hide the BottomNavigationView when you're on the login graph:
navController.addOnDestinationChangedListener { _, destination, _ ->
if(destination.parent!!.id == R.id.login_graph) {
toolbar.visibility = View.GONE
bottomNavigationView.visibility = View.GONE
} else {
toolbar.visibility = View.VISIBLE
bottomNavigationView.visibility = View.VISIBLE
}
}
Then, you can use just a single graph, following the best practices for user login.
Better use popUpTo combined with popUpToInclusive to create a one-way navigation action - in order to use a single one graph, instead of two disconnected graphs. This only should be applied to the first 1-2 fragments in the graph, so that eg. the back button cannot navigate back to the splash-screen or to the login-screen, because these actions do not count towards "the expected behavior".
<fragment
android:id="#+id/splashFragment"
android:name="com.acme.fragment.SplashFragment"
tools:layout="#layout/fragment_splash"
android:label="fragment_splash">
<action
android:id="#+id/action_splashFragment_to_loginFragment"
app:destination="#id/loginFragment"
app:popUpTo="#id/splashFragment"
app:popUpToInclusive="true"/>
</fragment>

How to use with CollapsingLayout with Navigation Advanced Example

I am trying to integrate Collapsing layout with Navigation Advanced example
What I tried?
Added Collapsing bar layout to main_actvity.xml
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
Modified setupBottomNavigationBar() in MainActivity.kt
private fun setupBottomNavigationBar(){
...
controller.observe(this, Observer { navController ->
mainBinding.collapsingToolbarLayout.setupWithNavController(mainBinding.toolbar, navController)
//setupActionBarWithNavController(navController)
})
...
}
Current issue:
Now I have two ActionBars instead of one. Top one have app name as the title, one below shows nothing initially but when navigated to an sub destination only a grey back arrow is shown, no destination label is shown on the actionbar
Anyone who understand this multiple backstack implementation, please help me to get things work with CollapsingToolbarLayout
Update:
Got rid of one action bar as #Manoj suggested in a comment, Now need to fix not appearing of titles in the actionbar
Update 2:
Although the destination labels(titles) are not shown when navigated to sub destinations, but back button is shown.
if the title is not displayed (and that increasing the appbar layout height "works"), just set the isTitleEnabled of the collapsingToolbarLayout to false.
It should fix the problem
I finally figured out why the toolbar title was not shown reason was I have not set enough height for appbar layout, I was using wrap_content so collapsing toolbar layout was covering the toolbar title. Solution was to set appbar layout height to value larger than 64dp. When I increased the appbar layout height, it looks unusually tall. (Forgive me for my lack of understanding of how collapsing toolbar works)
But this was not my intention, I wanted to enable collapsing toolbar for some specific fragments, I was using single activity concepts as navigation architecture component recommends.
As I read in following answers
Having two toolbars and making one transparent when doing fragment transactions.
Having separate toolbar for each fragment
IMHO Both of these are not good solutions if you are using navigation architecture component, there is no value of using navigation architecture component, if you need to manage fragment transactions or toolbars manually.
So for now I have stop using collapsing toolbar.

Android JetPack navigation with multiple stack

I'm using Jetpack Navigation version 1.0.0-alpha04 with bottom navigation. It works but the navigation doesn't happen correctly. For example, if I have tab A and tab B and from tab A I go to Page C and from there I go to tab B and come back to tab A again, I will see root fragment in the tab A and not page C which does not what I expect.
I'm looking for a solution to have a different stack for each tab, so the state of each tab is reserved when I come back to it, Also I don't like to keep all this fragment in the memory since it has a bad effect on performance, Before jetpack navigation, I used this library https://github.com/ncapdevi/FragNav, That does exactly what, Now I'm looking for the same thing with jetpack navigation.
EDIT 2: Though still no first class support (as of writing this), Google has now updated their samples with an example of how they think this should be solved for now: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
The major reason is you only use one NavHostFragment to hold the whole back stack of the app.
The solution is that each tab should hold its own back stack.
In your main layout, wrap each tab fragment with a FrameLayout.
Each tab fragment is a NavHostFragment and contains its own navigation graph in order to make each tab fragment having its own back stack.
Add a BottomNavigationView.OnNavigationItemSelectedListener to BottomNavigtionView to handle the visibility of each FrameLayout.
This also takes care of your "...I don't like to keep all this fragment in memory...", because a Navigation with NavHostFragment by default uses fragmentTransaction.replace(), i.e. you will always only have as many fragments as you have NavHostFragments. The rest is just in the back stack of your navigation graph.
Edit: Google is working on a native implementation https://issuetracker.google.com/issues/80029773#comment25
More in detail
Let's say you have a BottomNavigationView with 2 menu choices, Dogs and Cats.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="#+id/dogMenu"
.../>
<item android:id="#+id/catMenu"
.../>
</menu>
Then you need 2 navigation graphs, say dog_navigation_graph.xml and cat_navigation_graph.xml.
The dog_navigation_graph might look like
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/dog_navigation_graph"
app:startDestination="#id/dogMenu">
</navigation>
and the corresponding for cat_navigation_graph.
In your activity_main.xml, add 2 NavHostFragments
<FrameLayout
android:id="#+id/frame_dog"
...>
<fragment
android:id="#+id/dog_navigation_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/dog_navigation_graph"
app:defaultNavHost="true"/>
</FrameLayout>
and underneath add the corresponding for your cat NavHostFragment. On your cat frame layout, set android:visibility="invisible"
Now, in your MainActivity's onCreateView you can
bottom_navigation_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.dogMenu -> showHostView(host = 0)
R.id.catMenu -> showHostView(host = 1)
}
return#setOnNavigationItemSelectedListener true
}
All that showHostView() is doing is toggling the visibility of your FrameLayouts that are wrapping the NavHostFragments. So make sure to save them in some way, e.g. in onCreateView
val hostViews = arrayListOf<FrameLayout>() // Member variable of MainActivity
hostViews.apply {
add(findViewById(R.id.frame_dog))
add(findViewById(R.id.frame_cat))
}
Now it's easy to toggle which hostViews should be visible and invisible.
The issue has been resolved by the Android team in the latest version 2.4.0-alpha01 multiple backstacks along with bottom navigation support is now possible without any workaround.
https://developer.android.com/jetpack/androidx/releases/navigation
First, I want to make an edit to #Algar's answer. The frame that you want to hide should have android:visibility="gone" instead of invisible. The reason for that in your main layout you would have something like this:
<LinearLayout 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"
android:orientation="vertical"
tools:context=".ui.activity.MainActivity">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar_base" />
<FrameLayout
android:id="#+id/frame_home"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
>
<fragment
android:id="#+id/home_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/home_nav" />
</FrameLayout>
<FrameLayout
android:id="#+id/frame_find"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:visibility="gone">
<fragment
android:id="#+id/find_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/find_nav" />
</FrameLayout>
...
</LinearLayout>
If you wrap your main in a LinearLayout, setting the frame to invisible still make that frame counts, so the BottomNavigation wont appear.
Second, you should create a NavHostFragment instance (ie: curNavHostFragment) to keep track of which NavHostFragment is being visible when a tab in BottomNavigation is clicked. Note: you may want to restore this curNavHostFragment when the activity is destroyed by configuration's changes. This is an example:
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//if this activity is restored from previous state,
//we will have the ItemId of botnav the has been selected
//so that we can set up nav controller accordingly
switch (bottomNav.getSelectedItemId()) {
case R.id.home_fragment:
curNavHostFragment = homeNavHostFragment;
...
break;
case R.id.find_products_fragment:
curNavHostFragment = findNavHostFragment;
...
break;
}
curNavController = curNavHostFragment.getNavController();

Categories

Resources