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.
Related
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)
}
}}
I have a single activity which has a bottom navigation view in order to open different fragments.
The top level/default fragments loads data from firebase and then i want to pass that data to different fragments when user switches to different fragment.
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/navigation"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.ankitrath.finderr.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/searchFragment"
android:name="com.ankitrath.finderr.SearchFragment"
android:label="fragment_search"
tools:layout="#layout/fragment_search" />
<fragment
android:id="#+id/requestFragment"
android:name="com.ankitrath.finderr.RequestFragment"
android:label="fragment_request"
tools:layout="#layout/fragment_request" />
<fragment
android:id="#+id/friendFragment"
android:name="com.ankitrath.finderr.FriendFragment"
android:label="fragment_friend"
tools:layout="#layout/fragment_friendd" />
</navigation>
My MainActivity's OnCreate has:
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
NavController navController = Navigation.findNavController(this, R.id.fragment);
NavigationUI.setupWithNavController(bottomNavigationView, navController);
When HomeFragment loads the data from firestore. I want to pass 2 values to the other fragments.
I did look up the docs but I couldn't understand.
The easiest solution for this is to just use Viewmodel instead of passing it between activity and the fragment. Then u pass the activity on the ViewModel so every fragment and the parent have the same ViewModel.
This is an example of how to do it val viewModel by activityViewModels<The ViewModel that u made>()
you can use VIEWHOLDER to share data between fragments or activities, here is an example
in your app gradle add this implementation
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
Create a viewHolder Class like that
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SharedDataViewModel: ViewModel() {
private val _selectedCycle = MutableLiveData<Cycle>()
val selectedCycle: LiveData<Cycle> = _selectedCycle
private val _isAdmin = MutableLiveData<Boolean>()
val isAdmin: LiveData<Boolean> = _isAdmin
fun getSelectedCycle():Cycle {
return _selectedCycle.value!!
}
fun setSelectedCycle(cycle: Cycle) {
_selectedCycle.value = cycle
}
fun getIsAdmin():Boolean {
return _isAdmin.value!!
}
fun setIsAdmin(value: Boolean) {
_isAdmin.value = value
}
}
and use it like that in every fragment or activity
private val sharedData: SharedDataViewModel by activityViewModels()
then set or get the new values
sharedData.getIsAdmin()
sharedData.setIsAdmin(true)
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.
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.