Android JetPack navigation with multiple stack - android

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();

Related

Single Activity login flow Android

I am currently working on a school project that displays your timetable, grades, subjects and so on. To use any functionality in the Android application you have to be logged in. Here comes my problem:
When the users starts the application for the first time, they should see a login fragment. Once the login is completed they will be presented with a setup screen where they can choose color themes for grades and other user specific things. Only then should they be presented with the actual main fragment. On the second start the user should directly see the main fragment
The main fragment is a FragmentContainerView and a BottomNavigationBar that hosts 5 other fragments. In each of those subfragments you can click on items. Then a different fragment should be presented that shows some more details. These fragments however should overlap the bottom navigation so that you have to navigate back before you can choose a different fragment in the bottom navigation bar.
As far as I can tell I need nested FragmentContainerViews. The MainActivity should be
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
The MainActivity should host the LoginFragment, SetupFragment and a MainFragment. In the MainFragment should be like this
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="#+id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
I would like to achieve all of this with a navigation graph. Do I have to use one graph or two for each FragmentContainerView? Then there is also the problem with the detail Fragments. If for example I have a HomeFragment as a child of the MainFragment which is a child of the MainActivity and in the HomeFragment the user clicks on for example a button to the SettingsFragment, this fragment should be displayed as a child of the MainActivity. How would I get this working? I've already had a look at this question but don't really understand how to implement it.
How to setup Multiple nested FragmentContainerViews with respective navigation graphs?
Could somebody maybe create a very simple implementation of this. I especially need help with the nested FragmentContainerViews, the BottomNavigationBar and the NavigationGraph. I've also come across the problem that the bottom navigation bar doesn't respond anymore.
Thanks for your help in advance. Please let me know if you need any more details.
1.way
val homeFragment = HomeFragment()
val listFragment = ListFragment()
val profileFragment = ProfileFragment()
nav_view.setOnItemSelectedListener {
when (it) {
R.id.homeFragment -> setCurrentFragment(homeFragment)
R.id.listFragment -> setCurrentFragment(listFragment)
R.id.profileFragment ->
setCurrentFragment(profileFragment)
}
return#setOnItemSelectedListener
}
private fun setCurrentFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().apply {
replace(R.id.nav_host_fragment_activity_main, fragment)
addToBackStack(null) //If you delete this, press the back button, it will not return to the previous fragment.
commit()
}
2.way
Navigation with
https://www.youtube.com/watch?v=DI0NIk-7cz8&t=527s&ab_channel=Stevdza-San

Architecture component - List to detail and back

I have a list with entries and I want to be able to navigate to the detail of an entry and back and keep the data of the list. The list shall keep all the items that it had before and maybe even the scroll position.
I am using the navigation architecture component. For this I have 3 Fragments: Search - Result - Detail. I have one navigation graph navigation that is used in the single activity for this app NavigationActivity:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.features.NavigationActivity">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/navigation"/>
</LinearLayout>
I've setup all actions and calling them like so:
val action = SearchResultFragmentDirections.actionSearchResultFragmentToDetailFragment(item)
findNavController().navigate(action)
I tried to use the popTo in the navigaion graph but that did not change a thing. Does somebody know a solution to keep the states of the fragments with this scenario? Do I need another navigation graph?
//Edit:
I populate my list in a viewModel call in onCreate. This gets only called once in my flow, so the fragment is not destroyed. I can save the list items I retrieve in a local variable inside the fragment and repopulate my list by that in onResume, but this does not seem like the correct way.

How to handle multiple NavHosts/NavControllers?

I'm having a problem when dealing with multiple NavHosts. This issue is very similar to the one asked here. I think the solution for this question would help me as well, but it's a post from 2017 and it still has no answer. Android Developers Documentation doesn't help and searching through the web shows absolutely nothing that could possibly help.
So basically I have one Activity and two Fragments. Let's call them
FruitsActivity, FruitListFragment, FruitDetailFragment, where FruitsActivity has no relevant code and its xml layout is composed by a <fragment> tag, serving as NavHost, like that:
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/fruits_nav_graph"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
The FruitListFragment is the startDestination of my NavGraph, it handles a list of fruits that will come from the server.
The FruitDetailFragment shows details about the Fruit selected in the list displayed by FruitListFragment.
So far we have Activity -> ListFragment -> DetailFragment.
Now I need to add one more Fragment, called GalleryFragment. It's a simple fragment that displays many pictures of the Fruit selected and it's called by FruitDetailFragment when clicking a button.
The problem here is: In Portrait mode I simply use findNavController().navigate(...) and I navigate through the Fragments like I want. but when I'm using a Tablet in Landscape mode, I'm using that Master Detail Flow to display List and Details on the same screen. There is an example of how it works here, and I want the GalleryFragment to replace the FruitDetailFragment, sharing the screen with the list of fruits, but so far I could only manage to make it replace the "main navigation" flow, occupying the entire screen and sending the FruitListFragment to the Back Stack.
I already tried to play around with findNavController() method, but no matter from where I call it I can only get the same NavController all the time, and it always navigates in the same linear way.
I tried to implement my own NavHost, but I get and error "class file for androidx.navigation.NavHost not found".
This is the xml of my FruitsActivity:
<?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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/listing_nav_graph"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</layout>
This is the xml of the FruitListActivity in Landscape mode (in portrait mode there is just the RecyclerView):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvFruits"
android:layout_weight="3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"/>
<FrameLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/sideFragmentContainer"
android:name="fruits.example.FruitDetailFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
</RelativeLayout>
</layout>
And now I want to call the GalleryFragment and make it replace just the <fragment> of id 'sideFragmentContainer' instead of the whole screen (instead of replacing the <fragment> of id fragmentContainer in the Activity's xml).
I didn't find any explanations of how to handle multiple NavHosts or <fragment> inside <fragment>.
So based on that, is it possible to use Navigation Architecture and display a Fragment inside another Fragment? Do I need multiple NavHosts for that, or is there another way?
As suggested by jsmyth886, this blog post pointed me to the right direction.
The trick was to findFragmentById() to get the NavHost directly from the fragment container (in this case, the one sharing the screen with the rest of the Master Detail screen). This allowed me to access the correct NavController and navigate as expected.
It's important to create a second NavGraph too.
So a quick step-by-step:
Create the main NavGraph to make all the usual navigation (how it would work without the Master Detail Flow);
Create a secondary NavGraph containing only the possible Destinations that the Master Detail fragment will access. No Actions connecting then, just the Destinations.
In the main <fragment> container, set the attributes like that:
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/main_nav_graph"
app:defaultNavHost="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
The app:defaultNavHost="true" is important. Then the Master Detail layout will look like that:
<?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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvFruits"
android:layout_weight="3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"/>
<FrameLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/sideFragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/secondary_nav_graph"
app:defaultNavHost="false"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
</RelativeLayout>
</layout>
Again, the attribute app:defaultNavGraph is important, set it to false here.
In the code part, you should have a boolean flag to verify if your app is running on a Tablet or not (the link provided in the beginning of the answer explains how to do it). In my case, I have it as a MutableLiveData inside my ViewModel, like that I can observe it and change layouts accordingly.
If is not tablet (i.e. follows the normal navigation flow), simply call findNavController().navigate(R.id.your_action_id_from_detail_to_some_other_fragment). The main navigation will happen using the main NavController;
If is tablet using the Master Detail Flow, you must find the correct NavHost and NavController by finding the <fragment> that contains it, like that:
val navHostFragment = childFragmentManager.findFragmentById(R.id. sideFragmentContainer) as NavHostFragment
And finally you can navigate to the Fragment that you want to appear dividing the screen with the rest of the Master Detail screen by calling navHostFragment.navController.navigate(R.id.id_of_the_destination). Notice that here we don't call Actions, we call the Destination directly.
That's it, simpler than what I thought. Thank you Lara Martín for the blog post and jsmyth886 for pointing me to the right direction!

Fitting second fragment to an activity

In my Activity I ad a fragment dynamically which occupies a dynamic porportion of the screen (you can slide it up or down to cover the entire screen or just the bottom ~250dp).
I now want to dynamically add another fragment to the same activity but I need this new fragment to fit next to the first one. Meaning you can slide the first one still up and down but the second will then either stay behind the first or move to make room.
Google tells me to use the same fragment manager and just add a fragment but the same fragment manager has already done a commit (with the adding of the first).
Is there any way to achieve this? Thank you!
CODE:
In my activity onCreate I added a fragment called buildingFragment (blue)
fragmentTransaction = getFragmentManager().beginTransaction().add(R.id.fragment_container, buildingFragment, "BUILDINGPAGE");
fragmentTransaction.hide(buildingFragment);
fragmentTransaction.commit();
The buildingFragment animates itself in onCreateView so that most of its content is out of screen (bottom direction):
// ANIMATE THE BUILDING PAGE TO PEAK FROM BOTTOM
ObjectAnimator anim = ObjectAnimator.ofFloat(buildingPageView, "y", USEABLE_HEIGHT, USEABLE_HEIGHT-TOOLBAR_HEIGHT-VIRT_NAV_BAR);
anim.setDuration(500);
anim.start();
Now this BuildingFragment works and it can be slided up or down.
With a push of a button I want to add another fargment (red) to the activity but I need it to take the rest of the space on the screen that the BuildingFragment (blue) is currently not occupying.
No when I slide the BuildingFragment (blue) I need the second fragment (red) either to stay behind the Building Fragment or just make room by moving up or down. Either way is acceptable.
Here is an illustration:
Second question:
If I DONT want to replace fragments but add a new one, how can I bring the older fragment on top of the new one?
Check this lib its may be help you that you want achieve.
<com.sothree.slidinguppanel.SlidingUpPanelLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:sothree="http://schemas.android.com/apk/res-auto"
android:id="#+id/sliding_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
sothree:umanoPanelHeight="68dp"
sothree:umanoShadowHeight="4dp"
sothree:umanoParallaxOffset="100dp"
sothree:umanoDragView="#+id/dragView"
sothree:umanoOverlay="true"
sothree:umanoScrollableView="#+id/list">
<!-- MAIN CONTENT -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--LOAD FIRST FRAGMENT-->
</FrameLayout>
<!-- SLIDING LAYOUT -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--LOAD SECOND FRAGMENT-->
</FrameLayout>
</com.sothree.slidinguppanel.SlidingUpPanelLayout>
You can do commit as many times as you wish on a FragmentManager, BTW there is only one instance of FragmentManager that getFragmentManager() returns on each call. You can add a Fragment to any view any no of times.
So even if you have added a fragment, you can add same Fragment again to a view.
It is nothing but another instance of Fragment that will work as a isolated entity.

ViewPager with setOffscreenPageLimit hiding my initial Activity title

I'm using a standard ViewPager inside an Activity. Very basic stuff:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="#layout/toolbar" />
<android.support.design.widget.TabLayout
android:id="#+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/new_teal"/>
<android.support.v4.view.ViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Now, in my acitivity's code, I have an OnPageChangeListener which I'm using to switch the title in the toolbar whenever the viewpager's views are paged through.
I'm also calling setTitle(...) to set the title for the initial screen.
The issue with all of this is that I'm setting setOffscreenPageLimit(...), and when I do, that initial call to setTitle(...) gets overridden or hidden or something. The title just disappears!!!
If I remove setOffscreenPageLimit(...), then the entire thing misbehaves, as expected.
Any ideas???
For a effective solution we need to see your fragment's code but if you do not set a offScreenPageLimit to a view pager it create 1 previous and one next fragment of your current fragment. All these three fragments gets created and calls default fragment lifecycle methods such as onCreate, onViewCreated and etc.
If you are changing your title in one of your fragments which will be created when you set offScreenPageLimit to 2 or more, you can get this behave.
For example let's say your current Fragment is C and your view pager contains a b C d e and you change title in a or e fragments or other fragments in second or more position.
I hope this'll help you.

Categories

Resources