Android Advanced Navigation deeplinking between graphs - android

I have been using Android Advanced Navigation for a while. In my current project, I have three navigation graphs. The problem is, some of the fragments in one graph should be reached from another graph. In order to solve this, I've made deep links.
For example, in graph A I've included graph B and then used a deep link from graph B to reach that particular fragment. The problem is when I am in graph B and now I want to jump back to graph A I can't. As graph A is not included in graph B, the current navigation controller can't find the destination. If I include graph A in graph B, another problem occurs. Android Studio can't build the project as it is having a circular import problem (graph B tries to import graph A but graph B is already included in graph A etc.) and I really don't know what else to do.
I've tried creating one huge navigation graph which contains all three subgraphs but I couldn't make it work with this Android Advanced Navigation. Is there any more efficient way?
Edit to add code:
BottomNavigationView extension:
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent)
&& selectedItemId != navHostFragment.navController.graph.id) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
Navigation graphs:
<?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/home_nav_graph"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="lu.thebiggame.fragments.home.HomeFragment"
android:label="home_fragment"
tools:layout="#layout/home_fragment" >
<action
android:id="#+id/homeToGameResults"
app:destination="#id/gameResultsFragment" />
<action
android:id="#+id/homeToPlayLottoGames"
app:destination="#id/playLottoGamesFragment" />
</fragment>
<fragment
android:id="#+id/checkoutFragment"
tools:layout="#layout/checkout_fragment"
android:name="lu.thebiggame.fragments.checkout.CheckoutFragment"
android:label="CheckoutFragment" />
<fragment
android:id="#+id/gameResultsFragment"
android:name="lu.thebiggame.fragments.gameresults.GameResultsFragment"
android:label="game_results_fragment"
tools:layout="#layout/game_results_fragment">
<deepLink
android:id="#+id/gameResultsFragmentDeepLink"
app:uri="app://home/game-results-fragment" />
<action
android:id="#+id/gameResultsToPlayLottoGames"
app:destination="#id/playLottoGamesFragment" />
</fragment>
<fragment
android:id="#+id/congratulationsFragment"
android:name="lu.thebiggame.fragments.congratulations.CongratulationsFragment"
android:label="congratulations_fragment"
tools:layout="#layout/congratulations_fragment" >
<action
android:id="#+id/congratulationsToClaim"
app:destination="#id/claimFragment" />
<deepLink
android:id="#+id/congratulationsFragmentDeepLink"
app:uri="app://home/congratulations-fragment" />
</fragment>
<fragment
android:id="#+id/claimFragment"
tools:layout="#layout/claim_fragment"
android:name="lu.thebiggame.fragments.claim.ClaimFragment"
android:label="ClaimFragment" />
<include app:graph="#navigation/profile_nav_graph"/>
<fragment
android:id="#+id/aboutGameFragment"
android:name="lu.thebiggame.fragments.page.PageFragment"
android:label="page_fragment"
tools:layout="#layout/page_fragment">
<deepLink
android:id="#+id/aboutGameFragmentDeepLink"
app:uri="app://home/about-game-fragment" />
</fragment>
<fragment
android:id="#+id/playLottoGamesFragment"
android:name="lu.thebiggame.fragments.play.lottogames.PlayLottoGamesFragment"
tools:layout="#layout/play_lotto_games_fragment"
android:label="PlayLottoGamesFragment" >
<action
android:id="#+id/playLottoGamesToCheckout"
app:destination="#id/checkoutFragment" />
<deepLink
android:id="#+id/playLottoGamesDeepLink"
app:uri="app://home/play-lotto-games-fragment" />
<action
android:id="#+id/playLottoGamesToGameResults"
app:destination="#id/gameResultsFragment" />
</fragment>
</navigation>
<?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/games_nav_graph"
app:startDestination="#id/gamesFragment">
<fragment
android:id="#+id/gamesFragment"
android:name="lu.thebiggame.fragments.games.GamesFragment"
android:label="games_fragment"
tools:layout="#layout/games_fragment" />
<include app:graph="#navigation/home_nav_graph"/>
</navigation>
<?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/profile_nav_graph"
app:startDestination="#id/profileFragment">
<fragment
android:id="#+id/profileFragment"
android:name="lu.thebiggame.fragments.profile.ProfileFragment"
android:label="profile_fragment"
tools:layout="#layout/profile_fragment" >
<action
android:id="#+id/profileToResults"
app:destination="#id/resultsFragment" />
<action
android:id="#+id/profileToHelp"
app:destination="#id/helpFragment" />
<action
android:id="#+id/profileToPersonal"
app:destination="#id/personalFragment" />
<action
android:id="#+id/profileToChangePassword"
app:destination="#id/changePasswordFragment" />
<action
android:id="#+id/profileToPage"
app:destination="#id/pageFragment" />
</fragment>
<fragment
android:id="#+id/resultsFragment"
android:name="lu.thebiggame.fragments.results.ResultsFragment"
android:label="results_fragment"
tools:layout="#layout/results_fragment" >
<argument
android:name="startingTab"
app:argType="integer"
android:defaultValue="0" />
<deepLink
android:id="#+id/resultsFragmentDeepLink"
android:autoVerify="true"
app:uri="app://profile/results-fragment" />
<action
android:id="#+id/resultsToClaim"
app:destination="#id/claimFragmentResults" />
</fragment>
<fragment
android:id="#+id/helpFragment"
android:name="lu.thebiggame.fragments.help.HelpFragment"
android:label="help_fragment"
tools:layout="#layout/help_fragment" >
<action
android:id="#+id/helpToPage"
app:destination="#id/pageFragment" />
<action
android:id="#+id/helpToContact"
app:destination="#id/contactUsFragment" />
<action
android:id="#+id/helpToFaq"
app:destination="#id/faqFragment" />
</fragment>
<fragment
android:id="#+id/personalFragment"
android:name="lu.thebiggame.fragments.personal.PersonalFragment"
android:label="personal_fragment"
tools:layout="#layout/personal_fragment" />
<fragment
android:id="#+id/changePasswordFragment"
android:name="lu.thebiggame.fragments.changepassword.ChangePasswordFragment"
android:label="change_password_fragment"
tools:layout="#layout/change_password_fragment" />
<fragment
android:id="#+id/pageFragment"
android:name="lu.thebiggame.fragments.page.PageFragment"
android:label="page_fragment"
tools:layout="#layout/page_fragment" />
<fragment
android:id="#+id/contactUsFragment"
android:name="lu.thebiggame.fragments.contactus.ContactUsFragment"
android:label="contact_us_fragment"
tools:layout="#layout/contact_us_fragment" />
<fragment
android:id="#+id/faqFragment"
android:name="lu.thebiggame.fragments.faqfragment.FaqFragment"
android:label="faq_fragment"
tools:layout="#layout/faq_fragment" />
<fragment
android:id="#+id/claimFragmentResults"
tools:layout="#layout/claim_fragment"
android:name="lu.thebiggame.fragments.claim.ClaimFragment"
android:label="ClaimFragment" />
</navigation>
Initializing bottom navigation:
private fun setupBottomNavigationBar() {
activityMainBottomNav.itemIconTintList = null
val navGraphIds = listOf(
R.navigation.home_nav_graph,
R.navigation.games_nav_graph,
R.navigation.profile_nav_graph
)
val controller = activityMainBottomNav.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
currentNavController = controller
}

As nobody bothered to answer me, here is what I did. All the fragments that can appear in all three of my navigation graphs I moved to another graph and named it a shared graph. Furthemore, in order to navigate to this graph I created standard actions to it, but before navigating I made sure to change starting position of shared graph.
fun navigateToNestedGraph(navDir: NavDirections, destination: Int, navOps: NavOptions? = null){
val view = (this as Fragment).requireView()
val navController = Navigation.findNavController(view)
val graph = navController.graph.findNode(R.id.shared_nav_graph)
if (graph is NavGraph){
graph.startDestination = destination
navController.navigate(navDir, navOps)
}
}
This is a function from a listener that my Fragments implement. That's it.

Related

BottomNavigationView. Navigation to destination with deeplink cause to add destination fragment also in first tab

Let's say we have a bottom navigation view with 4 tabs, we have a deeplink for a fragment of 4 tab, so when navigating from this deeplink to 4 tab - it works as it should, but when after that manually select the first tab, a fragment of 4 tab is added also in the first tab.
i have a nav graph with 4 fragments
in on create of my activity i set the bottom view with nav controller
navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
navController = navHostFragment?.navController
navController?.setGraph(R.navigation.nav_unauth_state)
navView.inflateMenu(getBottomNavViewMenu())
navController?.let {
navView.setupWithNavController(it)
}
in manifest for this activity i added <nav-graph android:value="#navigation/nav_unauth_state" />
in nav_unauth_state for frgament i set the deeplink
android:id="#+id/deepLink2"
app:uri="https://<my_secret_url>/{action}" />
i solved it my self with this solution
navView.setOnItemSelectedListener { menuItem ->
val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(false)
val graph = navController?.currentDestination?.parent
val destination = graph?.findNode(menuItem.itemId)
if (menuItem.order and Menu.CATEGORY_SECONDARY == 0) {
navController?.graph?.findStartDestination()?.id?.let {
builder.setPopUpTo(
it,
inclusive = false,
saveState = true
)
}
}
val options = builder.build()
destination?.id?.let { id -> navController.navigate(id, null, options) }
return#setOnItemSelectedListener true
}

How to prevent recreation of Fragments while navigating back and forth?

I have set of Fragments navigates inside activity. While I called findFragmentByTag() the fragments onCreateView() and onViewCreated() are called again and the data is reset to normal. how to prevent the recreation of fragment?
You can have look on the code of the advanced navigation in android samples
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
this example code you can find the full code here
https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample

Return the state of navigation controller (Android Navigation Component)

I'm using button navigation and NavController. All my fragments in same navigation container. When I switch between tabs I want to store travel stack, that allow to return on current tab state. I try to use NavController.saveState() and after restoreState(). But after calling this function nothing changed. How I can achieve this?
val bottomNavigationListener = object :
BottomNavigationView.OnNavigationItemSelectedListener {
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_statistic -> handleTabClick(item.itemId, R.id.statisticsFragment)
R.id.menu_tracker -> handleTabClick(item.itemId, R.id.trackerFragment)
R.id.menu_wiki -> handleTabClick(item.itemId, R.id.wikiFragment)
else -> return false
}
return true
}
}
private fun handleTabClick(tabId: Int, hostFragmentId: Int) {
if(currentTabId != tabId) {
if(navController.currentDestination?.id != hostFragmentId) {
destinationsHistory[currentTabId] = navController.saveState() ?: Bundle()
} else {
destinationsHistory[currentTabId] = Bundle()
}
val bundle = destinationsHistory[tabId] ?: Bundle()
if(!bundle.isEmpty) {
navController.restoreState(bundle)
} else {
navController.navigate(hostFragmentId)
}
} else {
destinationsHistory[tabId] = Bundle()
if (navController.currentDestination?.id != hostFragmentId) {
navController.navigate(hostFragmentId)
}
}
currentTabId = tabId
}
<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_graph"
app:startDestination="#id/wikiFragment">
<fragment android:id="#+id/statisticsFragment" android:name="com.sc.overhub.view.fragment.StatisticsFragment"
android:label="StatisticsFragment"/>
<fragment android:id="#+id/trackerFragment" android:name="com.sc.overhub.view.fragment.TrackerFragment"
android:label="TrackerFragment"/>
<fragment android:id="#+id/wikiFragment" android:name="com.sc.overhub.view.fragment.wiki.WikiFragment"
android:label="fragment_wiki" tools:layout="#layout/fragment_wiki">
<action android:id="#+id/action_wikiFragment_to_wikiHeroesListFragment"
app:destination="#id/wikiHeroesListFragment"/>
<action android:id="#+id/action_wikiFragment_to_mapsFragment" app:destination="#id/mapsFragment"/>
</fragment>
<fragment android:id="#+id/wikiHeroesListFragment"
android:name="com.sc.overhub.view.fragment.wiki.herosList.WikiHeroesListFragment"
android:label="WikiHeroesListFragment">
<action android:id="#+id/action_wikiHeroesListFragment_to_wikiHeroFragment"
app:destination="#id/wikiHeroFragment"/>
</fragment>
<fragment android:id="#+id/wikiHeroFragment"
android:name="com.sc.overhub.view.fragment.wiki.herosList.hero.WikiHeroFragment"
android:label="fragment_view_hero" tools:layout="#layout/fragment_wiki_hero"/>
<fragment android:id="#+id/mapsFragment" android:name="com.sc.overhub.view.fragment.wiki.maps.MapsFragment"
android:label="MapsFragment"/>

Android, how to replace initial fragment?

I create this fragment and set initial fragment FirstFragment, if I don't set this initial fragment, the app will crash.
<fragment
android:id="#+id/fragment"
android:name="com.app.FirstFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp" />
And then I use this function to replace the fragment, this works fine, every fragment can be replaced by other fragment except FirstFragment, this fragment keep displaying on screen. How can I replace this fragment?
enum class FragmentType {
SETTING, ACTIVITY, PROFILE, CONNECT, SCANNER
}
private fun changeFragmentTo(type: FragmentType) {
val transaction = supportFragmentManager.beginTransaction()
when(type) {
FragmentType.SETTING -> {
title = "SETTING"
val f = SettingFragment()
transaction.replace(R.id.fragment, f)
}
FragmentType.ACTIVITY -> {
title = "ACTIVITY"
val f = ActivityFragment()
transaction.replace(R.id.fragment, f)
}
FragmentType.PROFILE -> {
title = "PROFILE"
val f = ProfileFragment()
transaction.replace(R.id.fragment, f)
}
FragmentType.CONNECT -> {
title = "CONNECT"
val f = ConnectFragment()
transaction.replace(R.id.fragment, f)
}
FragmentType.SCANNER -> {
title = "SCANNER"
val f = ScannerFragment()
transaction.replace(R.id.fragment, f)
}
}
transaction.addToBackStack(null)
transaction.commit()
}
The problem of yours is inside the xml. Instead of using fagment tag, you should create a layout which will be a container for your fragment.
Change your xml like this:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
and reference your FrameLayout inside, I assume, activity via it's id R.id.fragment:
val fragment = SettingFragment()
transaction.replace(R.id.fragment_container, fragment)

How do I define default animations for Navigation Actions?

I'm using Android Studio 3.2 Canary 14 and The Navigation Architecture Component. With this you can define transition animations pretty much as you would when using Intents.
But the animations are set as properties of the actions in the navigation graph, like so:
<fragment
android:id="#+id/startScreenFragment"
android:name="com.example.startScreen.StartScreenFragment"
android:label="fragment_start_screen"
tools:layout="#layout/fragment_start_screen" >
<action
android:id="#+id/action_startScreenFragment_to_findAddressFragment"
app:destination="#id/findAddressFragment"
app:enterAnim="#animator/slide_in_right"
app:exitAnim="#animator/slide_out_left"
app:popEnterAnim="#animator/slide_in_left"
app:popExitAnim="#animator/slide_out_right"/>
</fragment>
This gets tedious to define for all actions in the graph!
Is there a way to define a set of animations as default, on actions?
I've had no luck using styles for this.
R.anim has the default animations defined (as final):
nav_default_enter_anim
nav_default_exit_anim
nav_default_pop_enter_anim
nav_default_pop_exit_anim
in order to change this behavior, you would have to use custom NavOptions,
because this is where those animation are being assigned to a NavAction.
one can assign these with the NavOptions.Builder:
protected NavOptions getNavOptions() {
NavOptions navOptions = new NavOptions.Builder()
.setEnterAnim(R.anim.default_enter_anim)
.setExitAnim(R.anim.default_exit_anim)
.setPopEnterAnim(R.anim.default_pop_enter_anim)
.setPopExitAnim(R.anim.default_pop_exit_anim)
.build();
return navOptions;
}
most likely one would need to create a DefaultNavFragment, which extends class androidx.navigation.fragment (the documentation there does not seem completed yet).
So you can pass these NavOptions to the NavHostFragment like this:
NavHostFragment.findNavController(this).navigate(R.id.your_action_id, null, getNavOptions());
alternatively... when looking at the attrs.xml of that package; these animations are style-able:
<resources>
<declare-styleable name="NavAction">
<attr name="enterAnim" format="reference"/>
<attr name="exitAnim" format="reference"/>
<attr name="popEnterAnim" format="reference"/>
<attr name="popExitAnim" format="reference"/>
...
</declare-styleable>
</resources>
this means, one can define the according styles - and define these, as part of the theme...
one can define them in styles.xml:
<style name="Theme.Default" parent="Theme.AppCompat.Light.NoActionBar">
<!-- these should be the correct ones -->
<item name="NavAction_enterAnim">#anim/default_enter_anim</item>
<item name="NavAction_exitAnim">#anim/default_exit_anim</item>
<item name="NavAction_popEnterAnim">#anim/default_pop_enter_anim</item>
<item name="NavAction_popExitAnim">#anim/default_pop_exit_anim</item>
</style>
One can also define the default animations in res/anim:
res/anim/nav_default_enter_anim.xml
res/anim/nav_default_exit_anim.xml
res/anim/nav_default_pop_enter_anim.xml
res/anim/nav_default_pop_exit_anim.xml
I found solution that requires extending NavHostFragment. It's similar to Link182 but less involved in code. Most often it will require to change all xml defaultNavHost fragments names from standard:
<fragment
app:defaultNavHost="true"
...
android:name="androidx.navigation.fragment.NavHostFragment"
to:
<fragment
app:defaultNavHost="true"
...
android:name="your.app.package.fragments.NavHostFragmentWithDefaultAnimations"
Code for NavHostFragmentWithDefaultAnimations:
package your.app.package.fragments
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.navigation.*
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.NavHostFragment
import your.app.package.R
// Those are navigation-ui (androidx.navigation.ui) defaults
// used in NavigationUI for NavigationView and BottomNavigationView.
// Set yours here
private val defaultNavOptions = navOptions {
anim {
enter = R.animator.nav_default_enter_anim
exit = R.animator.nav_default_exit_anim
popEnter = R.animator.nav_default_pop_enter_anim
popExit = R.animator.nav_default_pop_exit_anim
}
}
private val emptyNavOptions = navOptions {}
class NavHostFragmentWithDefaultAnimations : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
// this replaces FragmentNavigator
FragmentNavigatorWithDefaultAnimations(requireContext(), childFragmentManager, id)
)
}
}
/**
* Needs to replace FragmentNavigator and replacing is done with name in annotation.
* Navigation method will use defaults for fragments transitions animations.
*/
#Navigator.Name("fragment")
class FragmentNavigatorWithDefaultAnimations(
context: Context,
manager: FragmentManager,
containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
// this will try to fill in empty animations with defaults when no shared element transitions are set
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
val shouldUseTransitionsInstead = navigatorExtras != null
val navOptions = if (shouldUseTransitionsInstead) navOptions
else navOptions.fillEmptyAnimationsWithDefaults()
return super.navigate(destination, args, navOptions, navigatorExtras)
}
private fun NavOptions?.fillEmptyAnimationsWithDefaults(): NavOptions =
this?.copyNavOptionsWithDefaultAnimations() ?: defaultNavOptions
private fun NavOptions.copyNavOptionsWithDefaultAnimations(): NavOptions =
let { originalNavOptions ->
navOptions {
launchSingleTop = originalNavOptions.shouldLaunchSingleTop()
popUpTo(originalNavOptions.popUpTo) {
inclusive = originalNavOptions.isPopUpToInclusive
}
anim {
enter =
if (originalNavOptions.enterAnim == emptyNavOptions.enterAnim) defaultNavOptions.enterAnim
else originalNavOptions.enterAnim
exit =
if (originalNavOptions.exitAnim == emptyNavOptions.exitAnim) defaultNavOptions.exitAnim
else originalNavOptions.exitAnim
popEnter =
if (originalNavOptions.popEnterAnim == emptyNavOptions.popEnterAnim) defaultNavOptions.popEnterAnim
else originalNavOptions.popEnterAnim
popExit =
if (originalNavOptions.popExitAnim == emptyNavOptions.popExitAnim) defaultNavOptions.popExitAnim
else originalNavOptions.popExitAnim
}
}
}
}
You can change animations in nav graph xml or in code through passing navOptions.
To disable default animations pass navOptions with anim values of 0 or pass navigatorExtras (setting shared transitions).
Tested for version:
implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
For version 2.5.2
fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?)
has to be override as well.
Here's my solution, and it worked well in my app.
public void navigate(int resId, Bundle bundle) {
NavController navController = getNavController();
if (navController == null) return;
NavDestination currentNode;
NavBackStackEntry currentEntry = navController.getCurrentBackStackEntry();
if (currentEntry == null) currentNode = navController.getGraph();
else currentNode = currentEntry.getDestination();
final NavAction navAction = currentNode.getAction(resId);
final NavOptions navOptions;
if (navAction == null || navAction.getNavOptions() == null) navOptions = ExampleUtil.defaultNavOptions;
else if (navAction.getNavOptions().getEnterAnim() == -1
&& navAction.getNavOptions().getPopEnterAnim() == -1
&& navAction.getNavOptions().getExitAnim() == -1
&& navAction.getNavOptions().getPopExitAnim() == -1) {
navOptions = new NavOptions.Builder()
.setLaunchSingleTop(navAction.getNavOptions().shouldLaunchSingleTop())
.setPopUpTo(resId, navAction.getNavOptions().isPopUpToInclusive())
.setEnterAnim(ExampleUtil.defaultNavOptions.getEnterAnim())
.setExitAnim(ExampleUtil.defaultNavOptions.getExitAnim())
.setPopEnterAnim(ExampleUtil.defaultNavOptions.getPopEnterAnim())
.setPopExitAnim(ExampleUtil.defaultNavOptions.getPopExitAnim())
.build();
} else navOptions = navAction.getNavOptions();
navController.navigate(resId, bundle, navOptions);
}
I have created the extension and called it instead of invoking navigation wherever required.
fun NavController.navigateWithDefaultAnimation(directions: NavDirections) {
navigate(directions, navOptions {
anim {
enter = R.anim.anim_fragment_enter_transition
exit = R.anim.anim_fragment_exit_transition
popEnter = R.anim.anim_fragment_pop_enter_transition
popExit = R.anim.anim_fragment_pop_exit_transition
}
})
}
findNavController().navigateWithDefaultAnimation(HomeFragmentDirections.homeToProfile())
As said, R.anim has the default animations defined:
nav_default_enter_anim
nav_default_exit_anim
nav_default_pop_enter_anim
nav_default_pop_exit_anim
But you can easily override them.
Just create your own four anim resources with the same names in your app module (just to clarify, the id of one of them is your.package.name.R.anim.nav_default_enter_anim) and write what animation you'd like.
It is possible with custom androidx.navigation.fragment.Navigator.
I will demonstrate how to override fragment navigation. Here is our custom navigator. Pay attention to setAnimations() method
#Navigator.Name("fragment")
class MyAwesomeFragmentNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
): FragmentNavigator(context, manager, containerId) {
private val backStack by lazy {
this::class.java.superclass!!.getDeclaredField("mBackStack").let {
it.isAccessible = true
it.get(this) as ArrayDeque<Integer>
}
}
override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?): NavDestination? {
if (manager.isStateSaved) {
logi("Ignoring navigate() call: FragmentManager has already"
+ " saved its state")
return null
}
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = instantiateFragment(context, manager,
className, args)
frag.arguments = args
val ft = manager.beginTransaction()
navOptions?.let { setAnimations(it, ft) }
ft.replace(containerId, frag)
ft.setPrimaryNavigationFragment(frag)
#IdRes val destId = destination.id
val initialNavigation = backStack.isEmpty()
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& backStack.peekLast()?.toInt() == destId)
val isAdded: Boolean
isAdded = if (initialNavigation) {
true
} else if (isSingleTopReplacement) { // Single Top means we only want one
instance on the back stack
if (backStack.size > 1) { // If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
manager.popBackStack(
generateBackStackName(backStack.size, backStack.peekLast()!!.toInt()),
FragmentManager.POP_BACK_STACK_INCLUSIVE)
ft.addToBackStack(generateBackStackName(backStack.size, destId))
}
false
} else {
ft.addToBackStack(generateBackStackName(backStack.size + 1, destId))
true
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key!!, value!!)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
return if (isAdded) {
backStack.add(Integer(destId))
destination
} else {
null
}
}
private fun setAnimations(navOptions: NavOptions, transaction: FragmentTransaction) {
transaction.setCustomAnimations(
navOptions.enterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
navOptions.exitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out,
navOptions.popEnterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
navOptions.popExitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out
)
}
private fun generateBackStackName(backStackIndex: Int, destId: Int): String? {
return "$backStackIndex-$destId"
}
}
In the next step we have to add the navigator to NavController. Here is an example how to set it:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer)!!
with (findNavController(R.id.fragmentContainer)) {
navigatorProvider += MyAwesomeFragmentNavigator(this#BaseContainerActivity, navHostFragment.childFragmentManager, R.id.fragmentContainer)
setGraph(navGraphId)
}
}
And nothing special in xml :)
<LinearLayout 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"
android:orientation="vertical">
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</LinearLayout>
Now each fragment from graph will have alpha transitions

Categories

Resources