I'm recently struggling a lot with some complex logic for deeplink & navigation management with Jetpack Compose Navigation. My actual problem is that when I handle my deeplinks and call navController.navigate(uri) the navigation is working properly but the startDestination screen from the graph is never added to the stack. So its created as:
Actual: Deeplink arrives > Deeplink Screen
Expected: Deeplink arrives > Open Home > Deeplink Screen
My Home screen is the startDestination of the graph where Deeplink Screen belongs to.
Some important notes:
I can't use navController.handleDeeplink because im doing my own deeplink management and this one doesn't work well with dynamic links(firebase) or external web uris. For this reason I always use navController.navigate(uri)
This solution is a workaround but I dont want to use it because im looking for a generic solution to work with nested graphs. Not a huge when clause. It's weird that compose don't support a synthetic stack and this is the "best" workaround. So possibly im missing something.
I don't want to copy/paste and re-implement the NavController. This is a solution aswell but a disgusting one.
Im attaching the code where I call my navigate method with the navOptions. But its more a theory question about Jetpack compose or what I could be missing from the library:
LaunchedEffect(pendingDeeplinkUri.value ) { // Avoid multiple executions using the intent as key
pendingDeeplinkUri.value?.let { uri ->
Timber.d("[DeepLinkManagement] LaunchedEffect pendingDeeplink $uri")
if (navController.graph.hasDeepLink(uri)) { //Check that deeplink exist
//find it in the graph
val match = navController.graph.matchDeepLink(NavDeepLinkRequest.Builder.fromUri(uri).build())
val destination = match!!.destination
navController.navigate(uri,
navOptions {
val changingGraphs = navController.graph.hierarchy.none { it == destination.parent }
Timber.d("[DeepLinkManagement] Changing graphs $changingGraphs")
if (changingGraphs) {
// If we are navigating to a 'sibling' graph (one that isn't part
// of the current destination's hierarchy), then we need to saveState
// to ensure that each graph has its own saved state that users can
// return to
Timber.d("[DeepLinkManagement] Parent ${destination.parent}")
popUpTo(destination.parent?.findStartDestination()?.id ?: 0) {
saveState = true
inclusive = true
}
// Note we specifically don't call restoreState = true
// as our deep link should support multiple instances of the
// same graph in a row
}
}
)
}
}
}
Hope that anyone can give me a hand with this. I'm possibly missing something obvious or maybe is something not supported at the moment. So any information or idea is totally welcome.
Thanks!
Related
I'm developing an app which is made of SingleActivity&Fragments.
The project flow is below.
Project flow
The navgraph is below.
Navigation flow
RegisterPhotoFragment has a photo button. If i click the button, it show the CameraOrGalleryBottomSheetDialog by using navigate(actionid).
In the CameraOrGalleryBottomSheetDialog, I can take a photo or get a image url from gallery.
For the process, I'm using setFragmentResultListener in RegisterPhotoFragment and setFragmentResult in CameraOrGalleryBottomSheetDialog.
But the problem is, I can't get the image uri from CameraOrGalleryBottomSheetDialog in RegisterPhotoFragment and I realized that RegisterPhotoFragment is just made by using ViewPager2 in BeginningFragment not navigate(anctionid). So, i didn't use navigate(actionid) to go to the RegisterPhotoFragment.
Therefore, I changed the navigation flow like below.
Second Navigation Flow
I restarted my app and it happened a crash.
java.lang.IllegalArgumentException: Navigation action/destination mypackage:id/action_registerPhotoFragment_to_cameraOrGalleryBottomSheetDialog cannot be found from the current destination Destination(mypackage:id/beginningFragment) label=BeginningFragment class=,mypackage.presentation.ui.beginning.BeginningFragment
because i added the RegisterPhotoFragment res/navigation/navGraph but i didn't use navigate(actionid). If i use naviagte(actionid), I can only see the screen of RegisterPhotoFragment. So, i didn't use it.
Finally, I decided to make a RegisterPhotoNavHostFragment and it has only FragmentContainerView and the Navigation Flow is changed again like below.
Sadly, I thought it works but it didn't. the same crash occured.
How can i make it? is this idea wrong?
Okay, I solved this problem. One thing that i missed was that i have to create the RegisterPhotoNavHostFragment instead of RegisterPhotoFragment when i create the FragmentStateAdapter for ViewPager2 in BeginningFragment.
before
private fun getFragmentsForViewPager2() = arrayListOf(
RegisterPhotoFragment.getNewInstance(),
RegisterNicknameFragment.getNewInstance(),
RegisterGenderFragment.getNewInstance()
)
after
private fun getFragmentsForViewPager2() = arrayListOf(
RegisterPhotoNavHostFragment.getNewInstance(),
RegisterNicknameFragment.getNewInstance(),
RegisterGenderFragment.getNewInstance()
)
the idea works well.
For tests I use Espresso and Barista
I have a test in which I need to open another screen by pressing a button. How can I check if this screen opens? Did the screen I need open?
Can I somehow check the chain of screens? To understand that the screens open in the order I need?
If someone throws links to good tutorials on UI tests in Android, I will be very grateful.
An easy solution would be to just check for an element of the new screen to be shown like this:
onView(withId(R.id.id_of_element_in_your_new_screen)).check(matches(isDisplayed()))
If you really want to check out for the current activity that is shown, you could try something like this:
Gather the current activity via InstrumentationRegistry and check for the activity in stage RESUMED.
fun getTopActivity(): Activity? {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.iterator().hasNext()) {
resumedActivities.iterator().next()?.let {
activity = it
}
}
}
return activity
}
You could then check this in a test like this:
#Test
fun checkForActivity() {
val currentActivity = getTopActivity()
assertTrue(currentActivity?.javaClass == YourActivityToCheckAgainst::class.java)
}
I personally use intended(hasComponent(YourActivityToCheckAgainst::class.java.name)), which checks if the last intent was done with a desired activity, set as its component.
I also wrote an extensive Android UI testing tutorial using Espresso + Barista libraries.
We have an app that utilises Deeplinks. We also use the Android Navigation component.
Currently we configure our Deeplinks in out navigation.xml file and this works fine but we now have the requirement to be able to add another Deeplink at build time based on a set Environment Variable.
I have attempted setting String resources in the build.gradle and referenceing these in the navigation.xml.
I have also attempted setting a placeholder in the navigation.xml but cannot replace it as it has already been parsed as a URI.
I have also attempted setting direct intent filters in the Manifest with placeholders, this will work but we lose the nice routing from the navigation component.
Currently we configure our Deeplinks in out navigation.xml file in the following form:
<deepLink
android:autoVerify="true"
app:uri="foo.bar.baz/pull/{quxArg}/{quuxArg}" />
We now have the requirement to be able to create an additional Deeplink at build time based on a set Envar.
Example:
DEEPLINK_ENVAR = "replacement.com"
Build.gradle:
manifestPlaceholders = [deeplink:DEEPLINK_ENVAR]
navigation.xml:
<deepLink
android:autoVerify="true"
app:uri="${deeplink}/pull/{quxArg}/{quuxArg}" />
Please note the above does not work.
If this was just an intent-filter in the Manifest we could use Manifest placeholders to achieve this task and set them in the app.gradle. However Deeplinks set in navigation.xml are parsed as URIs and destroy any placeholders before they can be replaced.
Has anyone attempted anything similar? I am trying to avoid having to run a pre-build script to template the navigation file directly.
Desired outcome:
I am looking to be able to add an additional deeplink (4 actually to different destinations) at build time whilst making use of Android Navigation component.
Not sure if I completely understand but...
You should be able to add several deepLinks to a single action.
If you require it to redirect to a different fragment, you could try have a "deepLinkTokenCheckFragment" or something, which receives the deepLink, then extracts the information from it, and can redirect the user to the page that you want them to go to.
I have an application that does something like this
private fun extractAction() {
if (ACTION_VIEW == parent.intent.action) {
// Collect information to know where to redirect here.....
val actionType = parent.intent.data
?.toString()
?.substringBefore('?')
?.substringAfterLast('/')
action = get information or token from the url here //?.substringBefore('?') ?.substringAfterLast('/')
when (action) {
"change_password" -> go to change password screen
"change email" -> go to change email screen
"go to other" -> go to other screen
}
}
}
This is just an idea of how I did it.
In the same way, instead of checking some token, you could check the build or whatever you need to compare it to.
NavDestination:
public final void addDeepLink (String uriPattern)
Add a deep link to this destination. Matching Uris sent to NavController.handleDeepLink(Intent) or NavController.navigate(Uri) will trigger navigating to this destination.
https://developer.android.com/reference/androidx/navigation/NavDestination.html#addDeepLink(java.lang.String)
This sounds like it could help you.
I have not tested it myself.
In my application I have created my own Loading indicator with the help of Page class in xamarin.forms, and I use 'PushModalAsync' api to show Loading Indicator wherever needed like below
var loadingindicator=new LoadingIndicator();
MyApp.CurrentAppInstance.MainPage.Navigation.PushModalAsync(loadingindicator,false);
Everything works as expected and It is looking fine and I also managed to make it look like AndHUD by tweeking the alpha for the controls in that page until I hit an issue,
The issue is that, every time I show and hide the loadingindicator page 'OnAppearing()' getting called on the current top page on view stack.
I can fix this by introducing one additional functionality on all pages where I am using my LoadingIndicator, But I feel there might be some other cleaner way to solve this issue.
Can you guys suggest me if there is any cleaner approach available to solve this issue?
(I target this solution mainly for android and I want to achieve it through common code)
If I have understand the problem, I think there are some way to add Loading indicator.
Use ActivityIndicator
Use aritchie/userdialogs's Loading
using (this.Dialogs.Loading("Test Loading"))
await Task.Delay(3000);
Create a PopUp With Rg.Plugins.Popup
// Use these methods in PopupNavigation globally or Navigation in your pages
// Open new PopupPage
Task PushAsync(PopupPage page, bool animate = true) // Navigation.PushPopupAsync
// Hide last PopupPage
Task PopAsync(bool animate = true) // Navigation.PopPopupAsync
// Hide all PopupPage with animations
Task PopAllAsync(bool animate = true) // Navigation.PopAllPopupAsync
// Remove one popup page in stack
Task RemovePageAsync(PopupPage page, bool animate = true) // Navigation.RemovePopupPageAsync
I'm using AppGyver Steroids and Supersonic to build an app and I'm having some issues navigating between views programmatically.
Based on the docs, you navigate between views like this:
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.push(view_obj);
However, when I inspect things via the Chrome DevTools, it appears that a second duplicate view is created i.e. If I navigate away from the index page and then navigate back, I now have two index pages, instead of what [I think] should be one. It also doesn't close the previous view I was on.
How can I prevent this from happening and simply move to the existing view, instead of duplicating views? How do I close a view after I have navigated away from it?
Thanks.
The problem you're encountering is that you're creating a new supersonic.ui.View("main#index") every time you navigate. On top of this, I think you want to return to the same view when you navigate back to a view for the second time, i.e. you want the view to remain in memory even if it has been removed from the navigation stack with pop() (rather than pushing a new instance of that view). For this, you need to preload or "start()" the view, as described in the docs here.
I implemented my own helper function to make this easier; here is my code:
start = function(dest, isModal) {
var viewId=dest,
view=new supersonic.ui.View({
location: dest,
id: viewId
});
view.isStarted().then(function(started) {
if (started) {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
} else {
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
Use it like start('module#view');. As a bonus, you can pass true as the second argument and it gets pushed as a modal instead.
It checks if you've already started a view - if so, it just pushes that view back onto the stack. If not, it start()s (i.e. preloads) it, then pushes it. This ensures that the view stays in memory (with any user input that has been modified) even when you pop() it from the stack.
You have to imagine that the layer stack is actually a stack in the Computer Science sense. You can only add and remove views at the top of the stack. The consequence of this is that complex navigations such as A > B > C > D > B are difficult/hacky to do (in this case, you'd have to pop() D and C in succession to get back to B).
Views will close if you pop() them, as long as you didn't start() them. If you did, and you pop() them, they remain in memory. To kill that view, you have to call stop() on it, as described in the docs I linked above.
try
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.replace(view_obj);
And take a look at supersonic.ui.layers.pop();
Thanks to LeedsEbooks for helping me get my head around this challenge. I was able to find a solution. Here is the code:
var start = function(route_str, isModal) {
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view_id_str = match_obj[2],
view_location_str = route_str,
view = new supersonic.ui.View({
location: view_location_str,
id: view_id_str
});
view.isStarted().then(function(started) {
if (started)
{
if (isModal)
{
supersonic.ui.modal.show(view);
}
else {
supersonic.ui.layers.push(view);
}
}
else
{
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal)
{
supersonic.ui.modal.show(view);
}
else
{
supersonic.ui.layers.push(view);
}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
You must ensure that your route has the format module#view as defined in the documentation.
PLEASE NOTE
There seems to some problem with the supersonic ui method for starting views. If you run the following code:
supersonic.ui.views.start("myapp#first-view");
supersonic.ui.views.find("first-view").then( function(startedView) {
console.log(startedView);
});
You'll notice that your view id and location are identical. This seems to be wrong as the id should be first-view and location should be myapp#first-view.
So I decided to not use the AppGyver methods and create my own preload method instead, which I run from the controller attached to my home view (this ensures that all the views I want to preload are handled when the app loads). Here is the function to do this:
var preload = function(route_str)
{
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view = new supersonic.ui.View({
location: route_str,
id: match_obj[2]
});
view.start();
};
By doing this, I'm sure that the view will get loaded with the right location and id, and that when I use my start() function later, I won't have any problems.
You'll want to make sure that your structure.coffee file doesn't have any preload instructions so as not to create duplicate views that you'll have problems with later.
Finally, I have a view that is 2 levels in that is a form that posts data via AJAX operation. I wanted the view to go back to the previous view when the AJAX operation was complete. Using my earlier function resulted in the push() being rejected. It would be nice if AppGyver Supersonic could intelligently detect that pushing to a previous view should default to a layers.pop operation, but you don't always get what you want. Anyway, I managed to solve this using supersonic.ui.layers.pop(), which simply does what the Back button would have done.
Everything working as intended now.