Issues while changing Back button behavior with Navigation Drawer (Android/Kotlin) - android

Building my first custom app, I would like to implement something that seemed easy at first but of course it is not. I have an activity with some fragments, which I can navigate to with a Navigation Drawer:
res/menu/navdrawer_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/matchFragmentUI"
android:icon="#drawable/ic_match"
android:title="#string/matches_string" />
<item
android:id="#+id/statFragmentUI"
android:icon="#drawable/ic_stat"
android:title="#string/stats_string" />
<item
android:id="#+id/clubFragmentUI"
android:icon="#drawable/ic_club"
android:title="#string/club_string" />
<item
android:id="#+id/agendaFragmentUI"
android:icon="#drawable/ic_agenda"
android:title="#string/agenda_string" />
<item
android:id="#+id/settingFragmentUI"
android:icon="#drawable/ic_settings"
android:title="#string/settings_string" />
<item
android:id="#+id/profileFragmentUI"
android:icon="#drawable/ic_person_black_24dp"
android:title="#string/profile_text" />
</menu>
res/layout/activity_content.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.drawerlayout.widget.DrawerLayout
android:id="#+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".content.ContentActivity">
<fragment
android:id="#+id/contentNavHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/navigation_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
android:id="#+id/navView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="#menu/navdrawer_menu"
app:headerLayout="#layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
I would like to change the back button behavior to prevent users from leaving accidentally the app like this :
/* Override behavior of back button */
override fun onBackPressed() {
/* Case we are not on the home screen */
if(/* not on home screen */) /* go to home screen */
else {
/* Otherwise we return to the access activity */
/* Dialog window to prevent accidental leaving */
val backAlertDialog = android.app.AlertDialog.Builder(this)
backAlertDialog.setTitle("Log out")
backAlertDialog.setMessage("Are you sure you want to leave?")
/* buttons YES/NO actions */
backAlertDialog.setPositiveButton("No") { dialog, which -> dialog.cancel() }
backAlertDialog.setNegativeButton("Yes") { dialog, which ->
val accessIntent = Intent(this, AccessActivity::class.java)
accessIntent.flags = FLAG_ACTIVITY_CLEAR_TOP // Clear activity backstack
startActivity(accessIntent) // Go back to access activity
finish() // Finish this activity
}
backAlertDialog.show()
}
}
My problem is checking if we are or not displaying the HomeFragment. At first, I tried with this :
if (supportFragmentManager.backstackEntryCount > 0)
But the issue here is that navigating with the navigation drawer doesn't add anything to the backstack (the backstackEntryCount stays at 0) so it won't work.
That's why I'm now trying to use NavigationView.OnNavigationItemSelectedListener interface, to add something in the backstack when an item is selected and also keep track of what is the current displayed fragment like :
/* Keep track of what the current fragment is like */
private var fragment : Fragment? = null
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val itemId = item.itemId
val fm = this.supportFragmentManager
/* Clear stack */
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
/* Add a case for each item in the drawer */
/* For each case, always add (push) a HomeFragment and then the fragment we navigate to */
/* Then, going back will always return to Home because we cleared backstack before */
when(itemId) {
R.id.profileFragmentUI -> {
fragment = ProfileFragmentUI()
}
R.id.agendaFragmentUI -> {
fragment = AgendaFragmentUI()
}
R.id.clubFragmentUI -> {
fragment = ClubFragmentUI()
}
R.id.statFragmentUI -> {
fragment = StatFragmentUI()
}
R.id.matchFragmentUI -> {
fragment = MatchFragmentUI()
}
R.id.settingFragmentUI -> {
fragment = SettingFragmentUI()
}
}
/* Replace current fragment by the selected fragment in backstack and add home for back support */
if (fragment != null) {
fm.beginTransaction()
.add(HomeFragmentUI(), "HomeFragmentUI")
.addToBackStack("HomeFragmentUI")
.replace(R.id.contentNavHostFragment, fragment!!)
.commit()
}
return true
}
Also, here are my onCreate() and onSupportNavigateUp() methods in the activity code :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* Bind layout elements at compile time to be faster at runtime */
val binding = DataBindingUtil.setContentView<ActivityContentBinding>(this, R.layout.activity_content)
val contentDrawerLayout = binding.drawerLayout
val player = intent.getParcelableExtra("player")
val navigationView = binding.navView
navigationView.setNavigationItemSelectedListener { item -> onNavigationItemSelected(item) }
/* Setup the navigation for fragments */
val navController = this.findNavController(R.id.contentNavHostFragment)
val fragmentManager = this.supportFragmentManager
/* Setup navigation bar, with drawer menu */
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
NavigationUI.setupWithNavController(binding.navView, navController)
fragment = HomeFragmentUI() /* To make back button work at start of navigation */
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.contentNavHostFragment)
/* Replace up button by drawer button on title screen */
return NavigationUI.navigateUp(navController, drawerLayout)
}
Now, I want to do my check like this :
if (fragment is HomeFragmentUI)
/* display dialog */
else
/* navigate back to home */
The problem now is that when I select an item in my drawer, I navigate to the good fragment, BUT the attribute fragment is still a HomeFragmentUI instance. I tried to put some logs in
onNavigationItemSelected method but they never show up.
I have no idea of why the method is never called since I defined the listener in onCreate.
Here are some links I used until now :
https://www.youtube.com/watch?v=I47Zy6JtNIU
https://www.youtube.com/watch?v=y9KYKYJJ4GY
https://guides.codepath.com/android/Fragment-Navigation-Drawer
Thank you in advance.

Related

Android Bottom navigation with splash

I'm implementing this bottom navigation pattern but with a splash fragment.
My issue is when I navigate throw different fragments with bottom menu and I press to go back, I don't go back to the home fragment, instead of this, I return to the previous fragment.
For example, I have fragments A-B-C:
Now I'm on fragment A and I press to go to B.
Then I press to go to C (from B).
Then I press to go back.
The result is I'm getting back to B, not to fragment A (what I really want!).
(If I set in the navigation graph app:startDestination -> fragment A - instead of login fragment - everything goes well).
Here is my graph:
<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/main_navigation"
app:startDestination="#id/splashFragment">
<fragment
android:id="#+id/splashFragment"
android:name="application.SplashFragment"
tools:layout="#layout/fragment_splash">
<action
android:id="#+id/action_splashFragment_to_fragmentA"
app:destination="#id/fragmentA"
app:popUpTo="#id/main_navigation"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/fragmentA"
android:name="application.fragmentA"
android:label="#string/fragmentA"
tools:layout="#layout/fragmentA" />
<fragment
android:id="#+id/fragmentB"
android:name="application.fragmentB"
android:label="fragmentB"
tools:layout="#layout/fragmentB" />
<fragment
android:id="#+id/fragmentC"
android:name="application.fragmentC"
android:label="#string/fragmentC"
tools:layout="#layout/fragmentC" />
And here my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Toolbar & Navigation
setSupportActionBar(toolbar)
navController = findNavController(R.id.nav_host)
// AppBarConfiguration with the correct top-level destinations
appBarConfiguration = AppBarConfiguration(
topLevelDestinationIds = setOf(
R.id.fragmentA,
R.id.fragmentB,
R.id.fragmentC
)
)
// This allows NavigationUI to decide what label to show in the action bar
// By using appBarConfig, it will also determine whether to
// show the up arrow or drawer menu icon
setupActionBarWithNavController(navController, appBarConfiguration)
// Set destinations to left and bottom navigation
bottom_navigation.setupWithNavController(navController)
// Set visibility for NavigationView & Toolbar
visibilityNavElements()
}
// Allows NavigationUI to support proper up navigation or the drawer layout
// drawer menu, depending on the situation
override fun onSupportNavigateUp() = navController.navigateUp(appBarConfiguration)
private fun visibilityNavElements() {
findNavController(R.id.nav_host).addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.splashFragment -> {
toolbar.visibility = View.GONE
bottom_navigation.visibility = View.GONE
}
else -> {
toolbar.visibility = View.VISIBLE
bottom_navigation.visibility = View.VISIBLE
}
}
}
}
}
Thanks!
You can handle OnbackPress in any fragment like below and is the recommended way . You can use it in Fragment C and when back is pressed you can navigate to Fragment A
in Java
OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
in Kotlin
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Handle the back button event
}

Stop fragment refresh in bottom nav using navhost

This problem has been asked a few times now, but we are in 2020 now, did anyone find a good usable solution to this yet?
I want to be able to navigate using the bottom navigation control without refreshing the fragment each time they are selected. Here is what I have currently:
navigation/main.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/main"
app:startDestination="#id/home">
<fragment
android:id="#+id/home"
android:name="com.org.ftech.fragment.HomeFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/news"
android:name="com.org.ftech.fragment.NewsFragment"
android:label="News"
tools:layout="#layout/fragment_news"/>
<fragment
android:id="#+id/markets"
android:name="com.org.ftech.fragment.MarketsFragment"
android:label="Markets"
tools:layout="#layout/fragment_markets"/>
<fragment
android:id="#+id/explore"
android:name="com.org.ftech.ExploreFragment"
android:label="Explore"
tools:layout="#layout/fragment_explore"/>
</navigation>
activity_mail.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="#color/nav"
app:itemTextColor="#color/nav"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/main">
</com.google.android.material.bottomnavigation.BottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
app:menu="#menu/main"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="#+id/navigationView"
android:layout_gravity="start">
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private var drawerLayout: DrawerLayout? = null
private var navigationView: NavigationView? = null
private var bottomNavigationView: BottomNavigationView? = null
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer_layout)
navigationView = findViewById(R.id.navigationView)
bottomNavigationView = findViewById(R.id.bottomNavigationView)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(setOf(R.id.markets, R.id.explore, R.id.news, R.id.home), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
findViewById<NavigationView>(R.id.navigationView)
.setupWithNavController(navController)
findViewById<BottomNavigationView>(R.id.bottomNavigationView)
.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
startActivity(Intent(applicationContext, SearchableActivity::class.java))
}
return super.onOptionsItemSelected(item)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.options_menu, menu)
return super.onCreateOptionsMenu(menu)
}
}
In the fragment I am making a few calls to my services to fetch the data in onCreateView, when resuming the fragment I am assuming those calls will not longer be executed and the state of the fragment should be preserved.
Try this:
public class MainActivity extends AppCompatActivity {
final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();
}
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return true;
case R.id.navigation_dashboard:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
return true;
case R.id.navigation_notifications:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
return true;
}
return false;
}
};
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}
Or You can follow Google's recommended solution: Google Link
The simple solution to stop refreshing on multiple clicks on the same navigation item could be
binding.navView.setOnNavigationItemSelectedListener { item ->
if(item.itemId != binding.navView.selectedItemId)
NavigationUI.onNavDestinationSelected(item, navController)
true
}
where binding.navView is the reference for BottomNavigationView using Android Data Binding.
Kotlin 2020 Google's Recommended Solution
Many of these solutions call the Fragment constructor in the Main Activity. However, following Google's recommended pattern, this is not needed.
Setup Navigation Graph Tabs
Firstly create a navigation graph xml for each of your tabs under the res/navigation directory.
Filename: tab0.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/tab0"
app:startDestination="#id/fragmentA"
tools:ignore="UnusedNavigation">
<fragment
android:id="#+id/fragmentA"
android:label="#string/fragment_A_title"
android:name="com.app.subdomain.fragA"
>
</fragment>
</navigation>
Repeat the above template for your other tabs. Important all fragments and the navigation graph has an id (e.g. #+id/tab0, #+id/fragmentA).
Setup Bottom Navigation View
Ensure the navigation ids are the same as the ones specified on the bottom menu xml.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="#string/fragment_A_title"
android:id="#+id/tab0"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_B_title"
android:id="#+id/tab1"
android:icon="#drawable/ic_baseline_add_alert_24"/>
<item android:title="#string/fragment_C_title"
android:id="#+id/tab2"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_D_title"
android:id="#+id/tab3"
android:icon="#drawable/ic_baseline_more_horiz_24"/>
</menu>
Setup Activity Main XML
Ensure FragmentContainerView is being used and not <fragment and do not set the app:navGraph attribute. This will set later in code
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/main_toolbar"
/>
Main Activity XML
Copy over the following Code into your main activity Kotlin file and call setupBottomNavigationBar within OnCreateView. Ensure you navGraphIds use R.navigation.whatever and not R.id.whatever
private lateinit var currentNavController: LiveData<NavController>
private fun setupBottomNavigationBar() {
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val navGraphIds = listOf(R.navigation.tab0, R.navigation.tab1, R.navigation.tab2, R.navigation.tab3)
val controller = bottomNavigationView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.fragmentContainerView,
intent = intent
)
controller.observe(this, { navController ->
val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
val appBarConfiguration = AppBarConfiguration(navGraphIds.toSet())
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
setSupportActionBar(toolbar)
})
currentNavController = controller
}
override fun onSupportNavigateUp(): Boolean {
return currentNavController?.value?.navigateUp() ?: false
}
Copy NavigationExtensions.kt File
Copy the following file to your codebase
[EDIT] The above link is broken. Found it in a forked repo
Source
Google's Solution
If you are using Jetpack, the easiest way to solve this is using ViewModel
You have to save all valuable data and not make unnecessary database loads or network calls everytime you go to a fragment from another.
UI controllers such as activities and fragments are primarily intended to display UI data, react to user actions, or handle operating system communication, such as permission requests.
Here is when we use ViewModels
ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.
So if the fragment is recreated, all your data will be there instantly instead of make another call to database or network. Its important to know that if the activity or fragment that holds the ViewModel is reacreated, you will receive the same ViewModel instance created before.
But in this case you have to specify the ViewModel to have activity scope instead of fragment scope, independently if you are using a shared ViewModel for all the fragments, or a different ViewModel for every fragment.
Here is a little example using LiveData too:
//Using KTX
val model by activityViewModels<MyViewModel>()
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
//Not using KTX
val model by lazy {ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]}
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
And that's it! Google is actively working on multiple back stack support for bottom tab Navigation and claim that it'll arrive on Navigation 2.4.0 as said here and on this issue tracker if you want and/or your problem is more related to multiple back stack, you can check out those links
Remember fragments still be recreated, usually you don't change component behavior, instead, you adapt your data to them!
I leave you some useful links:
ViewModel Overview Android Developers
How to communicate between fragments and activities with ViewModels - on Medium
Restoring UI State using ViewModels - on Medium
Quick tip, if you just want to prevent loading the already selected fragment just override setOnNavigationItemReselectedListener and do nothing, but this won't save the fragment states
binding.navBar.setOnNavigationItemReselectedListener { }
You use the old version, you just use version 2.4.0-alpha05 or above.
This answer is updated in 2021.
androidx.navigation:navigation-runtime-ktx:2.4.0-alpha05
androidx.navigation:navigation-fragment-ktx:2.4.0-alpha05
androidx.navigation:navigation-ui-ktx:2.4.0-alpha05
If you use NavigationUI.setupWithNavController(), the NavOptions are defined for you with NavigationUI.onNavDestinationSelected(). These options include launchSingleTop and, if the menu item is not secondary, a popUpTo the root of the graph.
The problem is, that launchSingleTop still replaces the top fragment with a new one. To resolve this issue, you'd have to create your own setupWithNavController() and onNavDestinationSelected() functions. In onNavDestinationSelected() you'd just adapt the NavOptions to your needs.
If you are using navigation component,In addition to this answer From version:'2.4.0-alpha01' it has inbuilt support for multiple back stacks.So no navigation extension is needed
refer to this link for more details. https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
Try something like this
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
And
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> {
fragmentManager.beginTransaction().hide(active).show(homeFragment).commit()
active = homeFragment
return#OnNavigationItemSelectedListener true
}
R.id.news -> {
fragmentManager.beginTransaction().hide(active).show(newsFragment).commit()
active = newsFragment
return#OnNavigationItemSelectedListener true
}
R.id.markets -> {
fragmentManager.beginTransaction().hide(active).show(marketsFragment).commit()
active = marketsFragment
return#OnNavigationItemSelectedListener true
}
R.id.explore -> {
fragmentManager.beginTransaction().hide(active).show(exploreFragment).commit()
active = exploreFragment
return#OnNavigationItemSelectedListener true
}
}
false
}
create a class:
#Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml
class KeepStateNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
var initialNavigate = false
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
} else {
initialNavigate = true
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
val className = destination.className
fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commitNow()
return if (initialNavigate) {
destination
} else {
null
}
}
}
Use keep_state_fragment instead of fragment in nav_graph
In Activity:
val navController = findNavController(R.id.nav_host_fragment)
// get fragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
// setup custom navigator
val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
navController.navigatorProvider += navigator
// set navigation graph
navController.setGraph(R.navigation.nav_graph)
bottom_navigation.setupWithNavController(navController)
Use this snippet:
private fun attachFragment(fragmentTag: String) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
supportFragmentManager.findFragmentByTag(fragmentTag)?.let {
if (supportFragmentManager.backStackEntryCount == 0) return
val currentFragmentTag = supportFragmentManager.getBackStackEntryAt(supportFragmentManager.backStackEntryCount - 1).name
(supportFragmentManager.findFragmentByTag(currentFragmentTag) as? FragmentBase)?.let { curFrag ->
fragmentTransaction.hide(curFrag)
}
fragmentTransaction.show(it)
} ?: run {
when (fragmentTag) {
FragmentHome.TAG -> FragmentBase.newInstance<FragmentHome>()
FragmentAccount.TAG -> FragmentBase.newInstance<FragmentAccount>()
else -> null
}?.let {
fragmentTransaction.add(R.id.container, it, fragmentTag)
fragmentTransaction.addToBackStack(fragmentTag)
}
}
fragmentTransaction.commit()
}
You can use this pass the tag of specific fragment that you want to show now, using method attachFragment(FragmentHome.TAG)
Hi friend, it's new solution:
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
if (item.getItemId() != binding.navView.getSelectedItemId())
NavigationUI.onNavDestinationSelected(item, navController);
return true;
}
});
I was looking for the best way to handle this and finally i came out with this simple idea : deactivate the MenuItem currently selected.
This way, you cannot click twice on it and therefore reloading the fragment is prevented.
Don't forget to Enable it back when you go to another fragment through your navHost.
The mechanic is based on the NavHostFragment which receives your BottomNavigationView from within a fragment/activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navHostFragment =
childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navView: BottomNavigationView = view.findViewById(R.id.nav_view)
val navController = navHostFragment.navController
//Here you link the NavHostFragment's navController to your
//bottomMenu
navView.setupWithNavController(navController)
//Add a listener monitoring the destination changes
navController.addOnDestinationChangedListener(object : NavController.OnDestinationChangedListener{
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
/* Disable the selected item and re-enable the others */
for( item in navView.menu.iterator()){
item.isEnabled = item.itemId != navView.selectedItemId
}
}
})
}
Hoping it might help
Lenzy
Try this: For ChipNavigationBar
private void Bottom_navigation() {
final Fragment fragment1 = new home_fragment();
final Fragment fragment2 = new bottom_nav1_Bookmark_Fragment();
final Fragment fragment3 = new bottom_nav1_Search_Fragment();
final FragmentManager fm = getSupportFragmentManager();
ChipNavigationBar chipNavigationBar = findViewById(R.id.chipNavigation);
chipNavigationBar.setItemSelected(R.id.home, true);
fm.beginTransaction().add(R.id.frame, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.frame, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.frame,fragment1, "1").commit();
chipNavigationBar.setOnItemSelectedListener(new ChipNavigationBar.OnItemSelectedListener() {
#Override
public void onItemSelected(int i) {
Fragment active = fragment1;
Fragment fragment = null;
switch (i) {
case R.id.home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
chipNavigationBar.animate().translationY(0);
break;
case R.id.tajbi:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
break;
case R.id.more_App:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
break;
}
}
});
}
You can resolve this issue by using this solution.
First, declare fragments that are used in the bottom navigation view.
val fragment1: Fragment = HomeFragment()
val fragment2: Fragment = ProfileFragment()
val fragment3: Fragment = SettingsFragment()
val fm: FragmentManager = supportFragmentManager
var active = fragment1
Now you need to setup the bottom navigation like this
private fun setUpBottomNavigation() {
fm.beginTransaction().add(R.id.mainHostFragment, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.mainHostFragment, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.mainHostFragment,fragment1, "1").commit();
val navigation = findViewById<View>(R.id.bottomNavigationView) as BottomNavigationView
navigation.setOnItemSelectedListener(mOnNavigationItemSelectedListener)
}
private val mOnNavigationItemSelectedListener =
NavigationBarView.OnItemSelectedListener { item ->
when (item.itemId) {
R.id.homeFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment1).commit()
active = fragment1
}
R.id.assessmentListFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment2).commit()
active = fragment2
}
R.id.settingsFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment3).commit()
active = fragment3
}
}
false
}

Android Navigation Component back button not working

I'm using the Navigation Component in android where I have set 6 fragments initially. The problem is when I added a new fragment (ProfileFragment).
When I navigate to this new fragment from the start destination, pressing the native back button does not pop the current fragment off. Instead, it just stays to the fragment I'm in - the back button does nothing.
Here's my navigation.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/dashboard_navigation"
app:startDestination="#id/dashboardFragment"
>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.devssocial.localodge.ui.dashboard.ui.DashboardFragment"
android:label="DashboardFragment"
>
<action
android:id="#+id/action_dashboardFragment_to_newPostFragment"
app:destination="#id/newPostFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_mediaViewer"
app:destination="#id/mediaViewer"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_postDetailFragment"
app:destination="#id/postDetailFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
====================== HERE'S THE PROFILE ACTION ====================
<action
android:id="#+id/action_dashboardFragment_to_profileFragment"
app:destination="#id/profileFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
=====================================================================
</fragment>
<fragment
android:id="#+id/profileFragment"
android:name="com.devssocial.localodge.ui.profile.ui.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile"
/>
</navigation>
In the image above, the highlighted arrow (in the left) is the navigation action I'm having troubles with.
In my Fragment code, I'm navigating as follows:
findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)
The other navigation actions are working as intended. But for some reason, this newly added fragment does not behave as intended.
There are no logs showing when I navigate to ProfileFragment and when I press the back button.
Am I missing something? or is there anything wrong with my action/fragment configurations?
EDIT:
I do not do anything in ProfileFragment. Here's the code for it:
class ProfileFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_profile, container, false)
}
}
And my activity xml containing the nav host:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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">
<fragment
android:id="#+id/dashboard_navigation"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="#navigation/dashboard_navigation"
app:defaultNavHost="true"/>
</FrameLayout>
if you are using setupActionBarWithNavController in Navigation Component such as:
setupActionBarWithNavController(findNavController(R.id.fragment))
then also override and config this methods in your main activity:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
My MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(findNavController(R.id.fragment))
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
For anyone using LiveData in a previous Fragment which is a Home Fragment, whenever you go back to the previous Fragment by pressing back button the Fragment is starting to observe the data and because ViewModel survives this operation it immediately emits the last emitted value which in my case opens the Fragment from which I pressed the back button, that way it looks like the back button is not working the solution for this is using something that emits data only once. I used this :
class SingleLiveData<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean()
/**
* Adds the given observer to the observers list within the lifespan of the given
* owner. The events are dispatched on the main thread. If LiveData already has data
* set, it will be delivered to the observer.
*
* #param owner The LifecycleOwner which controls the observer
* #param observer The observer that will receive the events
* #see MutableLiveData.observe
*/
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
/**
* Sets the value. If there are active observers, the value will be dispatched to them.
*
* #param value The new value
* #see MutableLiveData.setValue
*/
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
This problem happened to me while using MutableLiveData to navigate between fragments and was observing the live data object at more than one fragment.
I solved it by observing the live data object one time only or by using SingleLiveEvent instead of MutableLiveData. So If you're having the same scenario here, try to observe the live data object one time only or use SingleLiveEvent.
You can use this following for the Activity
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
For the fragment, It will be needed requireActivity() along with Callback
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
requireActivity().onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
If you have a Button or something else to perform an action then you can use
this.findNavController().popBackStack()
You need to set the MutableLiveData to null once the navigation is done.
For example
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
fun printName(){
_name.value = "John"
}
fun navigationComplete(){
_name.value = null
}
Now say you are observing the "name" in your fragment and you are doing some navigation once the name is John then should be like that:
viewModel.name.observe(viewLifecycleOwner, Observer { name ->
when (name) {
"John" -> {
this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
viewModel.navigationComplete()
}
}
})
Now your back button will be working without a single problem.
Some data are almost used only once, like a Snackbar message or navigation event therefore you must tell set the value to null once done used.
The problem is that the value in _name remains true and it’s not possible to go back to previous fragment.
If you use Moxy or similar libs, checkout the strategy when you navigate from one fragment to second.
I had the same issue when strategy was AddToEndSingleStrategy.
You need change it to SkipStrategy.
interface ZonesListView : MvpView {
#StateStrategyType(SkipStrategy::class)
fun navigateToChannelsList(zoneId: String, zoneName: String)
}
Call onBackPressed in OnCreateView
private fun onBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(this) {
//Do something
}
}
For everyone who is using LiveData for setting navigation ids, there's no need to use SingleLiveEvent. You can just set the destinationId as null after you set its initial value.
For instance if you want to navigate from Fragment A to B.
ViewModel A:
val destinationId = MutableLiveData<Int>()
fun onNavigateToFragmentB(){
destinationId.value = R.id.fragmentB
destinationId.value = null
}
This will still trigger the Observer in the Fragment and will do the navigation.
Fragment A
viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
when (destinationId) {
R.id.fragmentB -> navigateTo(destinationId)
}
})
The Simplest Answer for your problem (If it has something to do with fragments - Bottom navigation) could be
To set defaultNavHost = "false"
From Official Documentation it says->
Let's say you have 3 fragments set for Bottom Navigation, then setting
"defaultNavHost = true" will make fragment A acts like a parent, so when user clicks on back button in fragment 3 , it comes to fragment 1 instead of closing the activity (Bottom Navigation as Example).
Your XML should look like this, if you wanna just press back and close the activity from any fragment you are in.
<fragment
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/bottom_nav"
app:defaultNavHost="false"
app:navGraph="#navigation/visit_summary_navigation" />
Set the MutableLiveData to false after navigation
Put this code in your ViewModel.kt
private val _eventNextFragment = MutableLiveData<Boolean>()
val eventNextFragment: LiveData<Boolean>
get() = _eventNextFragment
fun onNextFragment() {
_eventNextFragment.value = true
}
fun onNextFragmentComplete(){
_eventNextFragment.value = false
}
Let's say you want to navigate to another fragment, you'll call the onNextFragmentComplete method from the viewModel immediately after navigating action.
Put this code in your Fragment.kt
private fun nextFragment() {
val action = actionFirstFragmentToSecondFragment()
NavHostFragment.findNavController(this).navigate(action)
viewModel.onNextFragmentComplete()
}
I had faced the same issue due to the below "run blocking" code block. So don't use it if not necessary.

How to set title in app bar with Navigation Architecture Component

I was trying out Navigation architecture component and is now having difficulties in setting the title. How do I set the title programmatically and also how it works?
To clear my question, let's have an example, where, I've set up a simple app with MainActivity hosting the navigation host controller, the MainFragment has a button and on clicking the button it goes to DetailFragment.
The same code from another question of multiple app bars on stack-overflow.
MainActivity
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Setting up a back button
NavController navController = Navigation.findNavController(this, R.id.nav_host);
NavigationUI.setupActionBarWithNavController(this, navController);
}
#Override
public boolean onSupportNavigateUp() {
return Navigation.findNavController(this, R.id.nav_host).navigateUp();
}
}
MainFragment
public class MainFragment extends Fragment {
public MainFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_main, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Button buttonOne = view.findViewById(R.id.button_one);
buttonOne.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.detailFragment));
}
}
DetailFragment
public class DetailFragment extends Fragment {
public DetailFragment() {
// Required empty public constructor
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_detail, container, false);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:animateLayoutChanges="true"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="#+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
android:layout_marginTop="?android:attr/actionBarSize"
app:defaultNavHost="true"
app:layout_anchor="#id/bottom_appbar"
app:layout_anchorGravity="top"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
app:navGraph="#navigation/mobile_navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bottom_appbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:layout_gravity="bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="#id/bottom_appbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
navigation.xml
<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/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.MainFragment"
android:label="fragment_main"
tools:layout="#layout/fragment_main" >
<action
android:id="#+id/toAccountFragment"
app:destination="#id/detailFragment" />
</fragment>
<fragment
android:id="#+id/detailFragment"
android:name="com.example.DetailFragment"
android:label="fragment_account"
tools:layout="#layout/fragment_detail" />
</navigation>
So when start my app, the title is "MainActivity". As usual it shows the MainFragment that contains the button to go to DetailFragment. In the DialogFragment I've set the title as:
getActivity().getSupportActionBar().setTitle("Detail");
First Problem: So clicking the button on the MainFragment to goto DetailFragment, it does go there and the title changes to "Detail". But on clicking the back button, the title changes to "fragment_main". So I added this line of code to MainFragment:
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// ...
//Showing the title
Navigation.findNavController(view)
.getCurrentDestination().setLabel("Hello");
}
Now the while returning back from DetailFragment to MainFragment the title changes to "Hello". But here comes the second problem, when I close the app and start again, the title changes back to "MainActivity" though it should be showing "Hello" instead, know?
Ok, then adding setTitle("Hello") in MainFrgment is not working too. For example, the activity starts and the title is "Hello", go to DetailsFragment and press the back button again, the title goes back to "fragment_main".
The only solution is to have both setTitle("Hello") along with Navigation.findNavController(view).getCurrentDestination().setLabel("Hello") in MainFragment.
So what is the proper way to show the title for fragments using Navigation Component?
It's actually because of:
android:label="fragment_main"
Which you have set in the xml.
So what is the proper way to show the title for Fragments using
Navigation Component?
setTitle() works at this point. But, because you set label for those Fragments, it might show the label again when recreating the Activity. The solution will probably be deleting android:label and then do your things with code:
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("your title");
Or:
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle("your subtitle");
In onCreateView().
Found a workaround:
interface TempToolbarTitleListener {
fun updateTitle(title: String)
}
class MainActivity : AppCompatActivity(), TempToolbarTitleListener {
...
override fun updateTitle(title: String) {
binding.toolbar.title = title
}
}
Then:
(activity as TempToolbarTitleListener).updateTitle("custom title")
Check this out too:Dynamic ActionBar title from a Fragment using AndroidX Navigation
As others are still participating in answering this question, let me answer my own question as APIs has changed since then.
First, remove android:label from the fragment/s that you wish to change the title of, from within navigation.xml (aka Navigation Graph),.
Now you can change the title from with the Fragment by calling
(requireActivity() as MainActivity).title = "My title"
But the preferred way you should be using is the API NavController.addOnDestinationChangedListener from within MainActivity. An Example:
NavController.OnDestinationChangedListener { controller, destination, arguments ->
// compare destination id
title = when (destination.id) {
R.id.someFragment -> "My title"
else -> "Default title"
}
// if (destination == R.id.someFragment) {
// title = "My title"
// } else {
// title = "Default Title"
// }
}
You can use this code in your fragment if you don't specify your app bar(default appbar)
(activity as MainActivity).supportActionBar?.title = "Your Custom Title"
Remember to delete the android:label attribute in your navigation graph
Happy code ^-^
From experience, NavController.addOnDestinationChangedListener
Seems to perform well. My example below on my MainActivity did the magic
navController.addOnDestinationChangedListener{ controller, destination, arguments ->
title = when (destination.id) {
R.id.navigation_home -> "My title"
R.id.navigation_task_start -> "My title2"
R.id.navigation_task_finish -> "My title3"
R.id.navigation_status -> "My title3"
R.id.navigation_settings -> "My title4"
else -> "Default title"
}
}
There is a much easier way to achieve this nowadays with Kotlin and androidx.navigation:navigation-ui-ktx:
import androidx.navigation.ui.setupActionBarWithNavController
class MainActivity : AppCompatActivity() {
private val navController: NavController
get() = findNavController(R.id.nav_host_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController) // <- the most important line
}
// Required by the docs for setupActionBarWithNavController(...)
override fun onSupportNavigateUp() = navController.navigateUp()
}
That's basically it. Don't forget to specify android:label in your nav graphs.
You can use the navigation graph xml file and set the label of the fragment to an argument.
Then, in your parent fragment, you can pass an argument using SafeArgs (please, follow the guide on https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args to set up SafeArgs) and provide a default value to avoid the title being null or empty.
<!--this is originating fragment-->
<fragment
android:id="#+id/fragmentA"
android:name=".ui.FragmentA"
tools:layout="#layout/fragment_a">
<action
android:id="#+id/fragmentBAction"
app:destination="#id/fragmentB" />
</fragment>
<!--set the fragment's title to a string passed as an argument-->
<!--this is a destination fragment (assuming you're navigating FragmentA to FragmentB)-->
<fragment
android:id="#+id/fragmentB"
android:name="ui.FragmentB"
android:label="{myTitle}"
tools:layout="#layout/fragment_b">
<argument
android:name="myTitle"
android:defaultValue="defaultTitle"
app:argType="string" />
</fragment>
In originating Fragment:
public void onClick(View view) {
String text = "text to pass as a title";
FragmentADirections.FragmentBAction action = FragmentADirections.fragmentBAction();
action.setMyTitle(text);
Navigation.findNavController(view).navigate(action);
}
FragmentADirections and FragmentBAction -
These classes are autogenerated from IDs in your nav_graph.xml file.
In 'your action ID + Action' type classes you can find auto-generated setter methods, which you can use to pass arguments
In your receiving destination, you call auto-generated class {your receiving fragment ID}+Args
FragmentBArgs.fromBundle(requireArguments()).getMyTitle();
Please refer to the official guide at
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args
NavigationUI.setupActionBarWithNavController(this, navController)
Don't forget to specify android:label for your fragments in your nav graphs.
To navigate back:
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
I've spent a couple of hours trying to do a very simple thing such as changing the bar title of a detail fragment when navigating from a particuler item on a master fragment. My daddy always said to me: K.I.S.S. Keep it simple son.
setTitle() crashed, interfaces are cumbersome, came up with a (very) simple solution in terms of lines of code, for the latest fragment navigation implementation.
In the master fragment resolve the NavController, get the NavGraph, find the destination Node, set the Title, and last but not least navigate to it:
//----------------------------------------------------------------------------------------------
private void navigateToDetail() {
NavController navController = NavHostFragment.findNavController(FragmentMaster.this);
navController.getGraph().findNode(R.id.FragmentDetail).setLabel("Master record detail");
navController.navigate(R.id.action_FragmentMaster_to_FragmentDetail,null);
}
The API may have changed since the question was asked, but now you may indicate a reference to a string in your app resources in the navigation graph label.
This is pretty helpful if you want a static title for your fragments.
delete detatil fragment's label in navigation graph xml file.
then pass the prefered title through arguments to destination fragment which needs title like so.
The First Fragment code - Start point
findNavController().navigate(
R.id.action_FirstFragment_to_descriptionFragment,
bundleOf(Pair("toolbar_title", "My Details Fragment Title"))
)
as you see I sent as pair in arguments bundle when navigating to Destination Fragment
so in your Destination Fragment get title from arguments in onCreate method like this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbarTitle = requireArguments().getString("toolbar_title", "")
}
then use it to change Main Activity's title in onCreateView method like this
requireActivity().toolbar.title = toolbarTitle
I would suggest to include AppBar in each screen.
To avoid code duplicates, create a helper, that builds AppBar, taking the title as a parameter. Then invoke the helper in each screen class.
Another approach could be this:
fun Fragment.setToolbarTitle(title: String) {
(activity as NavigationDrawerActivity).supportActionBar?.title = title
}
Update title with either label in navigation xml or exclude labels and set with requiresActivity().title Supports mixing the two ways for screens with and without dynamic titles. Works for me with a Compose UI toolbar and Tabs.
val titleLiveData = MutableLiveData<String>()
findNavController().addOnDestinationChangedListener { _, destination, _ ->
destination.label?.let {
titleLiveData.value = destination.label.toString()
}
}
(requireActivity() as AppCompatActivity).setSupportActionBar(object: Toolbar(requireContext()) {
override fun setTitle(title: CharSequence?) {
titleLiveData.value = title.toString()
}
})
titleLiveData.observe(viewLifecycleOwner, {
// Update your title
})
android:label="{title_action}"
<?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/FirstFragment">
<fragment
android:id="#+id/FirstFragment"
android:name="com.example.contact2.FirstFragment"
android:label="#string/first_fragment_label"
tools:layout="#layout/fragment_first">
<action
android:id="#+id/action_FirstFragment_to_SecondFragment"
app:destination="#id/SecondFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim"
app:popEnterAnim="#anim/nav_default_pop_enter_anim"
app:popExitAnim="#anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="#+id/SecondFragment"
android:name="com.example.contact2.SecondFragment"
android:label="{title_action}"
tools:layout="#layout/fragment_second">
<action
android:id="#+id/action_SecondFragment_to_FirstFragment"
app:destination="#id/FirstFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim"
app:popEnterAnim="#anim/nav_default_pop_enter_anim"
app:popExitAnim="#anim/nav_default_pop_exit_anim" />
<argument
android:name="title_action"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>
Anyway few of those answers I tried did not work for then I decided to do it the old Java way in Kotlin using interface
Created an interface as shown below.
interface OnTitleChangeListener {
fun onTitleChange(app_title: String)
}
Then made my activity to implement this interface as shown below.
class HomeActivity : AppCompatActivity(), OnTitleChangeListener {
override fun onTitleChange(app_title: String) {
title = app_title
}
}
How on my fragment's on activity attache I this this
override fun onAttach(context: Context) {
super.onAttach(context)
this.currentContext = context
(context as HomeActivity).onTitleChange("New Title For The app")
}
The simple solution:
Layout
androidx.coordinatorlayout.widget.CoordinatorLayout
com.google.android.material.appbar.AppBarLayout
com.google.android.material.appbar.MaterialToolbar
androidx.constraintlayout.widget.ConstraintLayout
com.google.android.material.bottomnavigation.BottomNavigationView
fragment
androidx.navigation.fragment.NavHostFragment
Activity
binding = DataBindingUtil.setContentView(this, layoutRes)
setSupportActionBar(binding.toolbar)
val controller = findNavController(R.id.nav_host)
val configuration = AppBarConfiguration(
setOf(
R.id.navigation_home,
R.id.navigation_dashboard,
R.id.navigation_notifications
)
)
setupActionBarWithNavController(controller, configuration)
navView.setupWithNavController(controller)
It could be helpful if you would like to change the title of Toolbar programmatically with low cohesion code between Activity and Fragment.
class DetailsFragment : Fragment() {
interface Callbacks {
fun updateTitle(title: String)
}
private var listener: Callbacks? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// keep activity as interface only
if (context is Callbacks) {
listener = context
}
}
override fun onDetach() {
// forget about activity
listener = null
super.onDetach()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_details, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listener?.updateTitle("Dynamic generated title")
}
}
class MainActivity : AppCompatActivity(), DetailsFragment.Callbacks {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun updateTitle(title: String) {
supportActionBar?.title = title
}
}
September 4, 2021
Using Kotlin in my case this is solution. use this code in MainActivity
val navController = this.findNavController(R.id.myNavHostFragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->
destination.label = when (destination.id) {
R.id.homeFragment -> resources.getString(R.string.app_name)
R.id.roomFloorTilesFragment -> resources.getString(R.string.room_floor_tiles)
R.id.roomWallTilesFragment -> resources.getString(R.string.room_wall_tiles)
R.id.ceilingRateFragment -> resources.getString(R.string.ceiling_rate)
R.id.areaConverterFragment -> resources.getString(R.string.area_converter)
R.id.unitConverterFragment -> resources.getString(R.string.unit_converter)
R.id.lenterFragment -> resources.getString(R.string.lenter_rate)
R.id.plotSizeFragment -> resources.getString(R.string.plot_size)
else -> resources.getString(R.string.app_name)
}
}
NavigationUI.setupActionBarWithNavController(...)
NavigationUI.setupWithNavController(...)

Is there a way to keep fragment alive when using BottomNavigationView with new NavController?

I'm trying to use the new navigation component. I use a BottomNavigationView with the navController : NavigationUI.setupWithNavController(bottomNavigation, navController)
But when I'm switching fragments, they are each time destroy/create even if they were previously used.
Is there a way to keep alive our main fragments link to our BottomNavigationView?
Try this.
Navigator
Create custom navigator.
#Navigator.Name("custom_fragment") // Use as custom tag at navigation.xml
class CustomNavigator(
private val context: Context,
private val manager: FragmentManager,
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?) {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = destination.createFragment(args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
}
}
NavHostFragment
Create custom NavHostFragment.
class CustomNavHostFragment: NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
}
}
navigation.xml
Use custom tag instead of fragment tag.
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="#+id/navigation"
app:startDestination="#id/navigation_first">
<custom_fragment
android:id="#+id/navigation_first"
android:name="com.example.sample.FirstFragment"
android:label="FirstFragment" />
<custom_fragment
android:id="#+id/navigation_second"
android:name="com.example.sample.SecondFragment"
android:label="SecondFragment" />
</navigation>
activity layout
Use CustomNavHostFragment instead of NavHostFragment.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/nav_host_fragment"
android:name="com.example.sample.CustomNavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Update
I created sample project. link
I don't create custom NavHostFragment. I use navController.navigatorProvider += navigator.
Update 19.05.2021 Multiple backstack
Since Jetpack Navigation 2.4.0-alpha01 we have it out of the box.
Check Google Navigation Adavanced Sample
Old answer:
Google samples link
Just copy NavigationExtensions to your application and configure by example. Works great.
After many hours of research I found solution. It was all the time right in front of us :) There is a function: popBackStack(destination, inclusive) which navigate to given destination if found in backStack. It returns Boolean, so we can navigate there manually if the controller won't find the fragment.
if(findNavController().popBackStack(R.id.settingsFragment, false)) {
Log.d(TAG, "SettingsFragment found in backStack")
} else {
Log.d(TAG, "SettingsFragment not found in backStack, navigate manually")
findNavController().navigate(R.id.settingsFragment)
}
If you have trouble passing arguments add:
fragment.arguments = args
in class KeepStateNavigator
If you are here just to maintain the exact RecyclerView scroll state while navigating between fragments using BottomNavigationView and NavController, then there is a simple approach that is to store the layoutManager state in onDestroyView and restore it on onCreateView
I used ActivityViewModel to store the state. If you are using a different approach make sure you store the state in the parent activity or anything which survives longer than the fragment itself.
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerview.adapter = MyAdapter()
activityViewModel.listStateParcel?.let { parcelable ->
recyclerview.layoutManager?.onRestoreInstanceState(parcelable)
activityViewModel.listStateParcel = null
}
}
override fun onDestroyView() {
val listState = planet_list?.layoutManager?.onSaveInstanceState()
listState?.let { activityViewModel.saveListState(it) }
super.onDestroyView()
}
ViewModel
var plantListStateParcel: Parcelable? = null
fun savePlanetListState(parcel: Parcelable) {
plantListStateParcel = parcel
}
I've used the link provided by #STAR_ZERO and it works fine. For those who having problem with the back button, you can handle it in the activity / nav host like this.
override fun onBackPressed() {
if(navController.currentDestination!!.id!=R.id.homeFragment){
navController.navigate(R.id.homeFragment)
}else{
super.onBackPressed()
}
}
Just check whether current destination is your root / home fragment (normally the first one in bottom navigation view), if not, just navigate back to the fragment, if yes, only exit the app or do whatever you want.
Btw, this solution need to work together with the solution link above provided by STAR_ZERO, using keep_state_fragment.
Super easy solution for custom general fragment navigation:
Step 1
Create a subclass of FragmentNavigator, overwrite instantiateFragment or navigate as you need. If we want fragment only create once, we can cache it here and return cached one at instantiateFragment method.
Step 2
Create a subclass of NavHostFragment, overwrite createFragmentNavigator or onCreateNavController, so that can inject our customed navigator(in step1).
Step 3
Replace layout xml FragmentContainerView tag attribute from android:name="com.example.learn1.navigation.TabNavHostFragment" to your customed navHostFragment(in step2).
In the latest Navigation component release - bottom navigation view will keep track of the latest fragment in stack.
Here is a sample:
https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample
Example code
In project build.gradle
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha01"
}
In app build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs'
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.4.0-alpha01"
}
Inside your activity - you can setup navigation with toolbar & bottom navigation view
val navHostFragment = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
//setup with bottom navigation view
binding.bottomNavigationView.setupWithNavController(navController)
//if you want to disable back icon in first page of the bottom navigation view
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.feedFragment,
R.id.favoriteFragment
)
).
//setup with toolbar back navigation
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
Now in your fragment, you can navigate to your second frgment & when you deselect/select the bottom navigation item - NavController will remember your last fragment from the stack.
Example: In your Custom adapter
adapter.setOnItemClickListener { item ->
findNavController().navigate(
R.id.action_Fragment1_to_Fragment2
)
}
Now, when you press back inside fragment 2, NavController will pop fragment 1 automatically.
https://developer.android.com/guide/navigation/navigation-navigate
Not available as of now.
As a workaround you could store all your fetched data into ViewModel and have that data readily available when you recreate the fragment. Make sure you get the ViewModel object using activity context.
You can use LiveData to make your data lifecycle-aware observable data holder.
The solution provided by #piotr-prus helped me, but I had to add some current destination check:
if (navController.currentDestination?.id == resId) {
return //do not navigate
}
without this check current destination is going to recreate if you mistakenly navigate to it, because it wouldn't be found in back stack.

Categories

Resources