Android Navigation Architecture Component - Programmatically - android

Looking for examples or anything similar that takes Swift (iOS) code like this:
let navController = UINavigationController(rootViewController: initialView)
and sets it up in Kotlin, via the new Navigation component. I've referenced the following examples, but it's not making complete sense to me:
val myNavHostController: NavHostFragment = nav_host_fragment as NavHostFragment
val inflater = myNavHostController.navController.navInflator
val graph = inflater.inflate(R.layout.nav_graph)
myNavHostController.navController.graph = graph
and
val finalHost = NavHostFragment.create(R.navigation.example_graph)
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit()
It appears the Android examples I'm coming across, still require an Activity/Fragment to already be established in the XML file created by the navigation component ... but, what if I want this functionality to be dynamic? What if I need to set the 'host' activity for the nav component based on data passed in? I am in need of the ability to do this all via code, which is what the Swift line is doing (setting the 'initialView' UIViewController, as the 'host', which has the navigation controller embedded in it). None of it is done via storyboarding, which seems to be what Android wants me to do regardless...
I am certain the issue is me not fully understanding how this works in Android, which is what I really would like to learn.

Related

Navigation component, popping back to another modules Destination

I have a multi module project that uses Navigation Component to navigate between Fragments. This means that in order to get to another module, I have to launch a DeepLink using findNavController().navigate(NavDeepLinkRequest, NavOptions). When it comes time to log out of the application, I need to pop the back stack inclusive to a Destination in another module that is not visible to that module. How do I achieve this?
I had exactly the same issue and I believe Google doesn't support it by default using Navigation Component (which is sad). However I managed to do it in a bit hacky way (but it works) using old friend getIdentifier.
In your Fragment you may have a navigation request like this:
val navigateToOtherModuleRequest = NavDeepLinkRequest.Builder
.fromUri("yourapp://othermoduledestination".toUri())
.build()
Then get the resource id you need to pop-up to using getIdentifier() (it can be Fragment id or in my case nav graph id):
val homeNavGraphResourceId = resources.getIdentifier(
"home_nav_graph",
"id",
requireContext().packageName
)
Define navigationOptions like this:
val navigationOptions = NavOptions.Builder()
.setPopUpTo(
destinationId = homeNavGraphResourceId,
inclusive = true
)
.build()
And navigate using
findNavController().navigate(
navigateToOtherModuleRequest,
navigationOptions
)
Hope this will help!

Is the navigation graph obsolete when I use Jetpack Compose Navigation?

Before I use a navigation graph in my Android Studio project, just like the article says.
I use an xml file located in res\navigation folder to include all my different destinations.
At present, I'm learning Jetpack Compose Navigation by the article.
Code A is from the official sample project mentioned in the above article.
It seems that Jetpack Compose Navigation use Code A and other codes to navigate, and I can't find any XML file in res\navigation folder.
1: Is the navigation graph obsoleted when I use Jetpack Compose Navigation ?
2: Don't I need to use the navigation graph located in res\navigation folder again when I use Jetpack Compose Navigation?
Code A
#Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = Overview.name,
modifier = modifier
) {
composable(Overview.name) {
OverviewBody(
onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
onClickSeeAllBills = { navController.navigate(Bills.name) },
onAccountClick = { name ->
navigateToSingleAccount(navController, name)
},
)
}
composable(Accounts.name) {
AccountsBody(accounts = UserData.accounts) { name ->
navigateToSingleAccount(navController = navController, accountName = name)
}
}
composable(Bills.name) {
BillsBody(bills = UserData.bills)
}
...
}
}
Navigation has always had three ways of building a NavGraph object:
Manually, by using the NavGraph constructor itself. While this serves as the basis for all other methods listed here, you aren't meant to use these APIs directly.
Building the graph via Navigation XML. This is a way of building a graph at compile time using the Navigator Editor tooling and the Safe Args plugin. This method only supports Navigation with Fragments.
Using the Navigation Kotlin DSL. This provides a type-safe way of building a navigation graph programmatically, at runtime by using a Kotlin DSL. This method supports both Navigation with Fragments and Navigation Compose.
As Compose is a way of programmatically building your UI, Navigation only supports the programmatic version of building the graph - via that Kotlin DSL that is exactly what that trailing lambda of NavHost provides you: it is that same NavGraphBuilder scope that allows you to call navigation(...) {} to build a nested graph or composable to add a new destination to your graph.
All of the concepts are the same because the underlying NavGraph you construct ends up being the exact same set of objects at runtime, no matter how you actually build the graph.

How to transfer data from one Fragment to another?

I am writing an application on Kotlin (Android Studio), using jetpack.navigation architecture.
There are two fragments: The first contains a list with class instances, which I display in the RecyclerView, the second for EditText (I fill in the client data). I also use Livedata and ViewModel.
The problem is that when I go to the second fragment, fill in the data and confirm, I go to the 1st fragment. As I understand it, the following lines destroy the old Fragment1, and create a new one. the list on the first fragment is reset to zero (although the list is saved when you rotate the screen and minimize the application).
val client = Clients(id,name,secondName,thirdName, address, creditCard, bankNum)
val action = Fragment2Directions.actionFragment2ToFragment1(client)
findNavController().navigate(action)
I could not find how to solve problem using the navigation component. I will be very grateful.
To pass data between two fragments with jetpack navigation you have to use Safe Args
pass an argument section like
<fragment android:id="#+id/myFragment" >
<argument
android:name="myArg"
app:argType="integer"
android:defaultValue="0" />
</fragment>
add classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" in top level gradle file
and add the plugin apply plugin: "androidx.navigation.safeargs.kotlin"
now send the value like so
override fun onClick(v: View) {
val amountTv: EditText = view!!.findViewById(R.id.editTextAmount)
val amount = amountTv.text.toString().toInt()
val action = SpecifyAmountFragmentDirections.confirmationAction(amount)
v.findNavController().navigate(action)
}
and receive it as
val args: ConfirmationFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tv: TextView = view.findViewById(R.id.textViewAmount)
val amount = args.amount
tv.text = amount.toString()
}
However safeargs works only for primitive types so you have to deconstruct and reconstruct if you're trying to pass Objects
As you are using ViewModels, I recommend that you use a shared ViewModel. The way it works is that multiple Fragments within the same Activity have access to the same ViewModel instance.
There is an example on Android Developers that fits your use case exactly. It shows how to use a shared ViewModel to do this using the master-detail navigation pattern and LiveData. I recommend you take a look at it.
Why not to use Safe Args here: You can try using Safe Args to achieve what you are trying to achieve, but I strongly recommend against it: You would have to deal with somehow using Safe Args to pass your Client objects between the Fragments back and forth (which means either sending each field individually or bundling) and you would have to manually update your LiveData objects - which defeats the purpose of LiveData. Using a shared ViewModel, you do not have to worry about any of that. No sending data back and forth, no taking your Client objects apart or bundling, no manual updating of LiveData objects - you simply access the same LiveData instance from both Fragments through your ViewModel.

How to add conditions to BottomNavigationView + Navigation components?

I'm trying to set up conditional navigation for my Fragments using the Navigation components and a BottomNavigationView.
Current setup (without conditions):
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
NavigationUI.setupWithNavController(bottom_navigation, navController)
General navigation is working fine, but I want to restrict user interaction with the bottom navigation based on conditions. If the condition is not met at the time of clicknig the menu item, this should only result in showing a Toast instead of navigating to the next fragment.
I already looked up this but the solution mentioned there involves navigating to the next fragment first and then check the conditions - but I want to avoid this.
Thank you very much.
Expose LiveData that will stream which #Ids are disabled.
class MainViewModel{
val disableNavigation = MutableLiveData<#Ids Int>()
fun invalidateNavigation() {
val canNavigate = ....
if(!canNavigate){
disableNavigation.value = R.id.bottom_nav_item_x
}
}
}
Then just observe this:
mainViewModel.observe(viewLifecycleOwner){
//disable the id here
//re-enable the rest of the items
}

How can I create dynamic/conditional navigation with Jetpack Navigation?

I've come across an interesting problem with trying to accomplish dynamic or conditional navigation with the Jetpack Navigation library.
The goal I have in mind is to be able to continue using the nav_graph.xml to manage the overall navigation graph, but simultaneously allow for conditional navigation based on some factors.
I have included some code below that shows where my solution is headed. The problem is that it inherently requires a lot of maintenance for future conditional logic to work.
I really want the navigateToDashboard function in the example to be able to be executed with either no parameters, or parameters that rarely change. For instance, instead of passing NavDirections, maybe passing some identifier that let's the navigateToDashboard function know which NavDirections to return.
Code for the class managing the conditional logic.
class DynamicNavImpl(private val featureFlagService: FeatureFlagService) : DynamicNav {
override fun navigateToDashboard(navDirectionsMap: Map<Int, NavDirections>): NavDirections {
val destinationIdRes = if (featureFlagService.isDashboardV2Enabled()) {
R.id.dashboardV2Fragment
} else {
R.id.dashboardFragment
}
return navDirectionsMap[destinationIdRes] ?: handleNavDirectionsException(destinationIdRes)
}
private fun handleNavDirectionsException(destinationIdRes: Int): Nothing {
throw IllegalStateException("Destination $destinationIdRes does not have an accompanying set of NavDirections. Are you sure you added NavDirections for it?")
}
}
Call site examples
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboard()),
Pair(R.id.dashboardV2Fragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboardV2())
)
)
)
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, EmailLoginFragmentDirections.emailLoginToDashboard()),
Pair(R.id.dashboardV2Fragment, EmailLoginFragmentDirections.emailLoginToDashboardV2())
)
)
)
Looking at the call site, you could see how this could be problematic. If I ever want to add a new potential destination, let's say dashboardV3Fragment, then I'd have to go to each call site and add another Pair.
This almost defeats the purpose of having the DynamicNavImpl class. So this is where I am stuck. I want to be able to encapsulate the various variables involved in deciding what destination to go to, but it seems with how NavDirections are implemented, I'm not able to.
I went between a few different approaches, and I landed on something that still doesn't feel ideal, but works for my use case.
I completely abandoned the idea of using a central dynamic navigation manager. Instead, I decided on having a "redirect" or "container" Fragment that decides what Fragment to show.
So here's the new code inside of the DashboardRedirectFragment
childFragmentManager.beginTransaction().replace(
R.id.dashboard_placeholder,
if (featureFlagService.isDashboardV2Enabled()) {
DashboardV2Fragment.newInstance()
} else {
DashboardFragment.newInstance()
}
).commit()
The way I'm using this is by registering a new destination in my nav graph called dashboardRedirectFragment, and anything in the graph that needs access to the dashboard use the dashboardRedirectFragment destination.
This fully encapsulates the dynamic navigation logic in the redirect Fragment, and allows me to continue using my nav graph as expected.

Categories

Resources