Related
I am using 3 fragments with 1 activity for user profile features. I use navigation to move to fragments:
The problem is when I go from profileFragment to editProfileFragment, change user's data and then go back to profileFragment by navigation action, I cannot reach editProfileFragment again from profileFragment. I got this error:
Navigation action/destination xxx:id/action_profileFragment_to_editProfileFragment cannot be found from the current destination Destination(xxx:id/editProfileFragment)
I am trying to use MVVM architecture so my navigation goes like this - in fragment I observe LiveData:
navController = Navigation.findNavController(view)
viewModel.navigateTo.observe(viewLifecycleOwner, EventObserver {
navController.navigate(it)
})
viewModel 'navigateTo':
private val _navigateTo = MutableLiveData<Event<Int>>()
val navigateTo: LiveData<Event<Int>> = _navigateTo
and the navigation methods:
fun goBackToProfile(){
_navigateTo.value = Event(R.id.action_editProfileFragment_to_profileFragment)
}
fun editProfileButtonClick() {
_navigateTo.value = Event(R.id.action_profileFragment_to_editProfileFragment)
}
I also use Event wrapper class by Jose Alcerreca:
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
I don't know if error occurs because of Event wrapper or I do something wrong with navigation. I will appreciate any advices.
EDIT:
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/main_nav_graph"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="xxx.mainPackage.views.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/profileFragment"
android:name="xxx.mainPackage.views.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile" >
<action
android:id="#+id/action_profileFragment_to_editProfileFragment"
app:destination="#id/editProfileFragment" />
</fragment>
<fragment
android:id="#+id/editProfileFragment"
android:name="xxx.mainPackage.views.EditProfileFragment"
android:label="fragment_edit_profile"
tools:layout="#layout/fragment_edit_profile" >
<action
android:id="#+id/action_editProfileFragment_to_chooseImageFragment"
app:destination="#id/chooseImageFragment" />
<action
android:id="#+id/action_editProfileFragment_to_profileFragment"
app:destination="#id/profileFragment" />
</fragment>
<fragment
android:id="#+id/chooseImageFragment"
android:name="xxx.mainPackage.views.ChooseImageFragment"
android:label="ChooseImageFragment" >
<action
android:id="#+id/action_chooseImageFragment_to_editProfileFragment"
app:destination="#id/editProfileFragment" />
</fragment>
</navigation>
It looks to me like you're navigating the wrong way.
Firstly
You are moving from ProfileFragment to EditProfileFragment. Then you move from EditProfileFragment to ProfileFragment via action whose id is R.id.action_profileFragment_to_editProfileFragment.
That action I see you define from Fragment ProfileFragment, not EditFragment.
Make sure your LiveData navigateTo in EditProfileFragment trigger id is R.id.action_editProfileFragment_to_profileFragment when you want to open ProfileFragment from EditProfileFragment.
Secondly
When you return from EditProfileFragment , don't call navController.navigate() as it will keep your current fragment in the backstack and push your target fragment to the backstack. If you want to go back without keeping your current fragment in the backstack, call navController.popBackStack().
Error is very clear, there is no action_profileFragment_to_editProfileFragment defined on editProfileFragment. if you look at your navigation xml, you can see that action_profileFragment_to_editProfileFragment is defined for ProfileFragment.
But you are trying to use this navigation action from EditProfileFragment, which causes the error. so either update your navigation to include this action for EditProfileFragment or use some action which is already defined for EditProfileFragment.
It is reasonable that a Fragment might be used from several sub-graphs in the navigation hierarchy. In this case if the fragment depends on a view model provided by the parent Fragment the view model needs to be in a sub-graph scope that changes depending on its parent.
Kotlin provides a convenient way to get a graph scoped view model:
private val fvm: SoftenerViewModel by navGraphViewModels(R.id.navigation_graph_softener)
but this hard codes in the sub-graph id.
What is the best way to address this case?
One approach is the following, but given that the new extension function is not already in the library it raises the question if there is a better way?
By direct analogy with the Kotlin supplied extension function define the following extension:
/**
* Derived from [androidx.navigation.navGraphViewModels]
*/
#MainThread
inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
viewModelArgKey: String,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
val backStackEntry by lazy {
val id = arguments?.getInt(viewModelArgKey, 0)?:0
require(id != 0) {"Fragment argument $viewModelArgKey required."}
findNavController().getBackStackEntry(id)
}
val storeProducer: () -> ViewModelStore = {
backStackEntry.viewModelStore
}
return createViewModelLazy(VM::class, storeProducer, {
factoryProducer?.invoke() ?: backStackEntry.defaultViewModelProviderFactory
})
}
This differs only in that the argument is a String, and the name of the argument is what needs to be given. The laziness ensures that the fragment arguments are referenced at the right time.
It is used just like the existing androidx library extension:
private val fvm: SoftenerViewModel by navGraphViewModels("view_model_id")
The only requirement is the navigation sub-graph defines the argument:
<?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/mobile_navigation"
app:startDestination="#+id/navigation_home"
>
<fragment
android:id="#+id/navigation_home"
android:name="com...HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home"
/>
<navigation
android:id="#+id/navigation_graph_softener"
android:label="Softener"
app:startDestination="#id/navigation_softener"
>
<fragment
android:id="#+id/navigation_softener"
android:name="com.hanafey.android.waterstats.ui.softener.SoftenerFragment"
android:label="#string/title_softener"
tools:layout="#layout/fragment_softener"
/>
<fragment
android:id="#+id/navigation_softener_history"
android:name="com...SoftenerHistoryFragment"
android:label="Softener History"
tools:layout="#layout/fragment_softener_history"
>
<argument
android:name="view_model_id"
app:argType="reference"
android:defaultValue="#id/navigation_graph_softener"
/>
</fragment>
</navigation>
</navigation>
I have a BottomNavigationView and I am using a NavController to switch menus and move the screen.
I would like to know how to keep a specific screen even when moving the menu freely.
Let me explain in more detail.
There are bottom menus A, B, C. And each has a Fragment screen.
All menus and screen transitions use NavHost, NavController, and nav_graph.
In menu C, a dialog is displayed through the button on the C screen.
The dialog is also linked to the nav_graph.
When an option is selected in the dialog, it moves to screen D.
D screen is the page where you write something.
This is important from now on.
The current menu is C, screen D is open, and something is being written.
However, if you go to another menu while writing and then return to C, screen C of the first menu C appears, not the screen you are writing.
Here, I want the screen I was writing to continue to be displayed even when I return from another menu.
Any good way?
For reference, since I am using the mvvm pattern and viewmodel, the data of the screen I am writing seems to be maintained.
Thank you.
nav_graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/calendar">
<!-- menu C screen -->
<fragment
android:id="#+id/write_home"
android:name="com.example.writeweight.fragment.WriteRoutineHomeFragment"
android:label="fragment_write_routine_home"
tools:layout="#layout/fragment_write_routine_home" >
<action
android:id="#+id/action_write_home_to_bodyPartDialog"
app:destination="#id/bodyPartDialog" />
</fragment>
<!-- dialog -->
<dialog
android:id="#+id/bodyPartDialog"
android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
android:label="BodyPartDialogFragment"
tools:layout="#layout/fragment_body_part_dialog">
<action
android:id="#+id/action_bodyPartDialog_to_write"
app:destination="#id/write"/>
</dialog>
<!-- screen D (write page) -->
<fragment
android:id="#+id/write"
android:name="com.example.writeweight.fragment.WriteRoutineFragment"
android:label="WritingRoutineFragment"
tools:layout="#layout/fragment_writing_routine">
<action
android:id="#+id/action_write_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="title"
app:argType="string"
app:nullable="true"
android:defaultValue="#null"/>
<argument
android:name="workout"
app:argType="string"
app:nullable="true"
android:defaultValue="#null"/>
</fragment>
</navigation>
There are two ways to fix this issue either use version_navigation 2.4.0-alpha01 which is the easiest way or use NavigationExtensions
to use NavigationExtensions you have to add this to your project NavigationExtensions and then in the navigation menu create separate navigation files for each tab.
In the main activity, layout replace fragment with FragmentContainerView and remove NavHostFragment.
In the Mainactivty
class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener {
private val viewModel by viewModels<MainViewModel>()
private var currentNavController: LiveData<NavController>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
setSupportActionBar(toolbar)
/*
appBarConfiguration = AppBarConfiguration(
// navController.graph,
setOf(
R.id.navigate_home, R.id.navigate_collection, R.id.navigate_profile
),
drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
bottomNavView.setupWithNavController(navController)
// make sure appbar/toolbar is not hidden upon fragment switch
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}
*/
// Add your tab fragments
val navGraphIds = listOf(R.navigation.home, R.navigation.albumlist, R.navigation.test)
val controller = bottomNavView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
// Whenever the selected controller changes, setup the action bar.
controller.observe(this, Observer { navController ->
setupActionBarWithNavController(navController)
// optional NavigationView for Drawer implementation
// navView.setupWithNavController(navController)
addOnDestinationChangedListener(navController)
})
currentNavController = controller
}
private fun addOnDestinationChangedListener(navController: NavController) {
// ensure only one listener is active
navController.removeOnDestinationChangedListener(this)
navController.addOnDestinationChangedListener(this)
}
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}}
What I have done:
I have created Navigation Drawer Activity, As updated new format of Navigation Drawer Activity, As per new Android architecture, I got it with Navigation Component structure.
The NavigationView code with NavController and NavigationUI is below which is opening fragment when I click on any navigation item.
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_profile, R.id.nav_privacy_policy,
R.id.nav_terms, R.id.nav_contact_us, R.id.nav_share, R.id.nav_send)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
This is for nav_host_fragment:
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
The navigation is happening using this navigation/mobile_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/mobile_navigation"
app:startDestination="#+id/nav_home">
<fragment
android:id="#+id/nav_home"
android:name="com.sohamerp.marsremedies.fragment.HomeFragment"
android:label="#string/menu_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/nav_profile"
android:name="com.sohamerp.marsremedies.fragment.ProfileFragment"
android:label="#string/menu_my_profile"
tools:layout="#layout/fragment_profile" />
<fragment
android:id="#+id/nav_privacy_policy"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy" />
<fragment
android:id="#+id/nav_terms"
android:name="com.sohamerp.marsremedies.fragment.TermsConditionFragment"
android:label="#string/menu_terms"
tools:layout="#layout/fragment_terms_condition" />
<fragment
android:id="#+id/nav_contact_us"
android:name="com.sohamerp.marsremedies.fragment.ContactUsFragment"
android:label="#string/menu_contact_us"
tools:layout="#layout/fragment_terms_condition" />
</navigation>
What I want to do:
Now I want to pass some values as a bundle (arguments) in Fragment when it's called.
Scenario: I have two fragments PrivacyPolicyFragment and TermsConditionsFragment, In both fragments, I am just opening links inside WebView accordingly. So When I click on the menu item of Privacy Policy, I will pass a link related to the same.
In this new structure navigation/mobile_navigation.xml opening fragments, How can I pass arguments?
Any help?
So I forgot to go through this link : Define Destination Arguments
But this answer helpful to all lazy peoples like me:
Add dependency in project level build.gradle:
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
Apply plugin in app level build.gradle:
apply plugin: "androidx.navigation.safeargs"
Using XML: predefined (static) value:
In xml file of navigation /navigation/mobile_navigation.xml declare argument tag as below or you can design through this link:
<fragment
android:id="#+id/nav_privacy_policy"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy" >
<argument
android:name="privacyPolicyLink"
app:argType="string"
android:defaultValue="http://sohamerp.com/avo/avo_privacy_policy.html"/>
</fragment>
<fragment
android:id="#+id/nav_terms"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_terms"
tools:layout="#layout/fragment_terms_condition" >
<argument
android:name="privacyPolicyLink"
app:argType="string"
android:defaultValue="http://sohamerp.com/avo/avo_privacy_policy.html"/>
</fragment>
Now you have to write code in your Fragment like:
if(getArguments() != null) {
// The getPrivacyPolicyLink() method will be created automatically.
String url = PrivacyPolicyFragmentArgs.fromBundle(getArguments()).getPrivacyPolicyLink();
}
Hope it will helps you others.
Simple and fast solution:
pass arguments between destinations
Bundle bundle = new Bundle();
bundle.putString("amount", amount);
Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle);
and receiving
TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(getArguments().getString("amount"));
In this scenario, you can use
private NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
// Create the Bundle to pass, you can put String, Integer, or serializable object
Bundle bundle = new Bundle();
bundle.putString("link","http://yourlink.com/policy");
bundle.putSerializable("USER", user); // Serializable Object
navController.navigate(R.id.nav_terms, bundle); // called fragment with agruments
In case of any help you can reply on it
To pass arguments to other Fragments/Destinations, use Safe Args which ensures type safety. Just like #bromden illustrated, Safe Args will generate a class for each fragment/destination where an action originates. You can then pass the arguments into the action that navigates to the Fragments.
In the receiving fragment, say PrivacyFragment if your code is in Kotlin, use by navArgs() property delegate to access the arguments. i.e.
val args: PrivacyFragmentArgs by navArgs()
To better understand this, visit Pass data between destinations
In newer version of Android Studio 3.2+, below dependency and plug-in need to add in both build.gradle file
Step-1
Add dependency in Project-Level build.gradle
dependencies {
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
}
Apply plugins in App-Level build.gradle
plugins {
id 'androidx.navigation.safeargs'
}
Step-2
In Navigation file, res/navigation/nav_graph.xml
Declare argument tag in any fragment or inner fragment with action tag
List item
Sample xml code
<fragment
android:id="#+id/nav_register"
android:name="com.pd.demo.ui.profile.RegisterFragment"
android:label="#string/title_register"
tools:layout="#layout/fragment_register">
<action
android:id="#+id/action_nav_register_to_nav_verify_otp"
app:destination="#id/nav_verify_otp">
<argument
android:name="mobile"
app:argType="string" />
<argument
android:name="password"
app:argType="string" />
</action>
</fragment>
Step-3
Below Kotlin code, pass argument to destination fragment
val bundle = bundleOf("mobile" to binding.etMobileNo.text.toString().trim())
Navigation.findNavController(binding.root).navigate(R.id.action_nav_register_to_nav_verify_otp, bundle)
Step-4
Below Kotlin code, get bundle argument from source fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mobileNo = arguments!!.getString("mobile").toString()
password = arguments!!.getString("password").toString()
}
This code will helps
You could implement NavigationView.OnNavigationItemSelectedListener
And do something like this:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
drawer_layout.closeDrawers()
if (item.itemId == nv_navigation_drawer_navigation_view.checkedItem?.itemId)
return false
Handler().postDelayed({
when (item.itemId) {
R.id.nav_privacy_policy -> {
val action = FragmentDirections.actionFragmentToPrivacyFragment("Policy link")
findNavController().navigate(action)
}
}
}, DRAWER_NAVIGATION_DELAY)
return true
}
And in xml you can add argument to the recieving fragment, in this case
<fragment
android:id="#+id/nav_privacy_policy"
android:name=".fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy">
<argument
android:name="policy"
app:argType="string" />
</fragment>
You can also pass serializable objects, enum values and arrays of primitive types.
For example:
enum class ObjectType : Serializable {
FIRST, SECOND
}
Then, add arguments to the xml
<fragment
android:id="#+id/nav_profile"
android:name="com.sohamerp.marsremedies.fragment.ProfileFragment"
android:label="#string/menu_my_profile"
tools:layout="#layout/fragment_profile" >
<argument
android:name="myObjectType"
android:defaultValue="SECOND"
app:argType="com.project.app.data.ObjectType" />
</fragment>
Note, that you should specify complete path!
Passing data from the start destination with NavController NavGraph navigate is straightforward. I use this to display order lines associated to an order header:
private void showRepositionLinesFragment(AppObjects.RepOrderHeader orderHeader) {
int number = orderHeader.getOrderNumber();
String orderNumber = String.format("%06d",number);
String createDate = orderHeader.getCreateDate();
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,
"Navigate to FragRepoLines with orderNumber: " + orderNumber,false);
NavController navController = NavHostFragment.findNavController(FragmentRepositionHeaders.this);
Bundle bundle = new Bundle();
bundle.putString(getString(R.string.arg_header_ordernumber),orderNumber);
bundle.putString(getString(R.string.arg_repheader_createdate), createDate);
navController.getGraph().findNode(R.id.FragRepoLines).setLabel(orderNumber + " " + createDate);
navController.navigate(R.id.action_FragRepoHeaders_to_FragRepoLines,bundle);
}
Getting data from the fragment that handles the order lines turned to be more complicated. Tried for hours with NavController getArguments().
In the end this is what worked for me.
In the start fragment:
NavController navController = NavHostFragment.findNavController(this);
// We use a String here, but any type that can be put in a Bundle is supported
MutableLiveData<String> liveData = navController.getCurrentBackStackEntry()
.getSavedStateHandle()
.getLiveData(getString(R.string.arg_header_ordernumber));
liveData.observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(String s) {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info, "+++++++++ liveData changed -> " + s, false);
}
});
In the destination fragment:
String arg = getString(R.string.arg_header_ordernumber);
NavController navController = NavHostFragment.findNavController(this);
NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
if (navBackStackEntry != null) {
SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
if (savedStateHandle != null) {
savedStateHandle.set(arg, "000000");
} else {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,"savedStateHandle == null",false);
}
} else {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,"navBackStackEntry == null",false);
}
Source: Interact programmatically with the Navigation component
I changed the navController.getPreviousBackStackEntry() for navController.getCurrentBackStackEntry()
I had the same issue but I´m still not able to pass the arguments using fragment directions. Since I need the value in several of my fragments I decided to use a companion object in my main activity. It´s probably not the best but it solves the problem:
class MainActivity : AppCompatActivity() {
companion object{
var myGlobalVar = "Example"
}
override fun onCreate(savedInstanceState: Bundle?) {....
Then I can access its value in all of my fragments by importing it:
import myAppPackage.MainActivity.Companion.myGlobalVar
I had to delete the argument from my navGraph but i can still access it in the background.
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(...)