Android Architecture Navigation - How to navigate to specific place in Nav Graph building back stack - back-stack

I have a linear wizard created with Android Arch Navigation and I would like to start it at specific location building back stack as with natural user navigation.
So I have tried this deep link building:
Bundle args = new Bundle();
args.putString("myarg", "From Widget");
new NavDeepLinkBuilder(context)
.setGraph(R.navigation.mobile_navigation)
.setDestination(R.id.step_fragment)
.setArguments(args)
.createPendingIntent().send();
The problem with this approach is that it doesn't build back stack correctly as user will be build navigating Step 1 -> Step 2 -> ... -> Step N
It only holds Step 1 -> Step N, i.e. start destination and target destination of navigation graph. This is not what I want.
Second simple approach is to just call multiple times navigate() on navController. But as it seems simple it doesn't work
protected fun navigateTo(step: Int) {
val navController = findNavController(R.id.nav_host_fragment)
if(step >= 0 && step < stepFragments.count()) {
// go to step-th fragment
if(navController.currentDestination.id != stepFragments.first()) return
for(i in 0 until step) {
navController.navigate(stepFragments[i])
}
navController.navigate(stepFragments[step])
} else if(step == stepFragments.count()) {
// go to confirm fragment
navController.navigate(confirmFragment)
} else {
throw IndexOutOfBoundsException("Step index is out of wizard bounds!")
}
}
It navigates correctly but then the created back stack is odd, i.e. Back button seems to work, but onNavigatedListener listener from
navController.addOnNavigatedListener(this::onNavigatedListener)
is not called correctly. So I cannot listen for fragment changes in wizard. Moreover NavController seems to be break, as consecutive button listener's navController.navigate(actionId) throws error.
java.lang.IllegalArgumentException: navigation destination
is unknown to this NavController
UPDATE!
Intercepting onBackPressed() with debugger seems to show that Back Button Press doesn't call navController.popBackStack() and the navController.currentDestination property doesn't change. But inside NavHostFragment the fragment is changing.

It seems that this multi-step navController.navigate() works Ok.
And fixing of back-stack while pressing Back button to expected behaviour can be achieved by overriding default onBackPressed() behaviour to something like this:
//region BACK PRESSED - CUSTOM BACK STACK HANDLING
override fun onBackPressed() {
if(!findNavController(R.id.nav_host_fragment).popBackStack()) {
super.onBackPressed()
}
}
//endregion
And for the record
protected fun navigateTo(step: Int) {
val navController = findNavController(R.id.nav_host_fragment)
if(step >= 0 && step < stepFragments.count()) {
// go to step-th fragment
if(navController.currentDestination.id != stepFragments.first()) return
for(i in 0 until step) {
navController.navigate(stepFragments[i])
}
navController.navigate(stepFragments[step])
} else if(step == stepFragments.count()) {
// go to confirm fragment
navController.navigate(confirmFragment)
} else {
throw IndexOutOfBoundsException("Step index is out of wizard bounds!")
}
}

Related

View.findNavController() vs Fragment.findNavController()

Anywhere in a NavHostFragment I can do findNavController().navigateUp()
Or, if in my Fragment I have a button to be used for navigation, I could do either:
editButton.setOnClickListener { v ->
v.findNavController().navigateUp()
}
or
editButton.setOnClickListener {
findNavController().navigateUp()
}
Why would I use one extension function vs the other when setting up a button click listener in a Fragment?
They are almost the same, Fragment.findNavController() is just a handy shortcut, it actually calls Navigation.findNavController(view) at the end. Both of them are getting the NavController from the root view of the fragment.
// NavHostFragment.java
public static NavController findNavController(#NonNull Fragment fragment) {
....
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}
...
}
Thank you for the Java answer, I offer a ViewBinding solution in kotlin, doing the same
// NavHostFragment.kotlin
fun findMyNavController(#NonNull fragment: Fragment): NavController {
return Navigation.findNavController(binding.root)
}
Although they are not identical as suggested by the top voted answer, the performance difference should be negligible in general scenarios. However if you use the v.findNavController() on a View which is deeply nested down, then you should prefer Fragment extension findNavController() as it will perform better.
In general I don't see any reason to prefer the v.findNavController over the Fragment extension. following is the complete code of this method from NavHostFragment and it only calls Navigation.findNavController if it doesn't find the NavController using the fragment
public static NavController findNavController(#NonNull Fragment fragment) {
Fragment findFragment = fragment;
while (findFragment != null) {
if (findFragment instanceof NavHostFragment) {
return ((NavHostFragment) findFragment).getNavController();
}
Fragment primaryNavFragment = findFragment.requireFragmentManager()
.getPrimaryNavigationFragment();
if (primaryNavFragment instanceof NavHostFragment) {
return ((NavHostFragment) primaryNavFragment).getNavController();
}
findFragment = findFragment.getParentFragment();
}
// Try looking for one associated with the view instead, if applicable
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}
throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}

CharmListView Infinite Scroll

I need basically an event that triggers at each 200 records loaded, so more data can be loaded until the end of data.
I tried to extend CharmListCell and using the method updateItem like this:
#Override
public void updateItem(Model item, boolean empty) {
super.updateItem(item, empty);
currentItem = item;
if (!empty && item != null) {
update();
setGraphic(slidingTile);
} else {
setGraphic(null);
}
System.out.println(getIndex());
}
But the System.out.println(getIndex()); method returns -1;
I would like to call my backend method when the scroll down gets the end of last fetched block and so on, until get the end of data like the "infinite scroll" technique.
Thanks!
The CharmListCell doesn't expose the index of the underlying listView, but even if it did, that wouldn't be of much help to find out if you are scrolling over the end of the current list or not.
I'd suggest a different approach, which is also valid for a regular ListView, with the advantage of having the CharmListView features (mainly headers and the refresh indicator).
This short sample, created with a single view project using the Gluon IDE plugin and Charm 5.0.0, shows how to create a CharmListView control, and fill it with 30 items at a time. I haven't provided a factory cell, nor the headers, and for the sake of simplicity I'm just adding consecutive integers.
With a lookup, and after the view is shown (so the listView is added to the scene) we find the vertical ScrollBar of the listView, and then we add a listener to track its position. When it gets closer to 1, we simulate the load of another batch of items, with a pause transition that represents a heavy task.
Note the use of the refresh indicator. When new data is added, we scroll back to the first of the new items, so we can keep scrolling again.
public class BasicView extends View {
private final ObservableList<Integer> data;
private CharmListView<Integer, Integer> listView;
private final int batchSize = 30;
private PauseTransition pause;
public BasicView() {
data = FXCollections.observableArrayList();
listView = new CharmListView<>(data);
setOnShown(e -> {
ScrollBar scrollBar = null;
for (Node bar : listView.lookupAll(".scroll-bar")) {
if (bar instanceof ScrollBar && ((ScrollBar) bar).getOrientation().equals(Orientation.VERTICAL)) {
scrollBar = (ScrollBar) bar;
break;
}
}
if (scrollBar != null) {
scrollBar.valueProperty().addListener((obs, ov, nv) -> {
if (nv.doubleValue() > 0.95) {
addBatch();
}
});
addBatch();
}
});
setCenter(new VBox(listView));
}
private void addBatch() {
listView.setRefreshIndicatorVisible(true);
if (pause == null) {
pause = new PauseTransition(Duration.seconds(1));
pause.setOnFinished(f -> {
int size = data.size();
List<Integer> list = new ArrayList<>();
for (int i = size; i < size + batchSize; i++) {
list.add(i);
}
data.addAll(list);
listView.scrollTo(list.get(0));
listView.setRefreshIndicatorVisible(false);
});
} else {
pause.stop();
}
pause.playFromStart();
}
}
Note also that you could benefit from the setOnPullToRefresh() method, at any time. For instance, if you add this:
listView.setOnPullToRefresh(e -> addBatch());
whenever you go to the top of the list and drag it down (on a mobile device), it will make another call to load a new batch of items. Obviously, this is the opposite behavior as the "infinite scrolling", but it is possible as well with the CharmListView control.

How Clear Back Stack on Xamarin IOS?

When a user authenticates correctly, it will be directed to the HomeViewModel. I want to remove the possibility that it can return to the login screen so I have created a Custom Presenter to remove all the screens that are below the new screen.
The implementation is as follows:
public class CustomPresenter: MvxFormsIosPagePresenter
{
public CustomPresenter(UIWindow window, MvxFormsApplication mvxFormsApp)
: base(window, mvxFormsApp)
{
}
public override void Show(MvxViewModelRequest request)
{
if (request.PresentationValues?["NavigationCommand"] == "StackClear")
{
var navigation = FormsApplication.MainPage.Navigation;
Debug.WriteLine("Navigation Back Stack Count -> " + navigation.NavigationStack.Count());
navigation.PopToRootAsync();
Debug.WriteLine("Navigation Back Stack Count After PopToRootAsync -> " + navigation.NavigationStack.Count());
return;
}
base.Show(request);
}
}
When the authentication process finishes correctly, I navigate to the home screen by passing a bundle with this special command:
LoginWithFacebookCommand.Subscribe(token => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
var mvxBundle = new MvxBundle(new Dictionary<string, string> { { "NavigationCommand", "StackClear" } });
ShowViewModel<HomeViewModel>(presentationBundle: mvxBundle);
});
The problem is that it does not change the screen, it stays in the current one. What would be the way to do it correctly ?.
I am using MvvmCross 5.1.1 and MvvmCross.Forms 5.1.1
Thank you very much in advance.
As I understand it, PopToRootAsync() pops everything off the stack to the root. Which means you should then push your view that you wish to navigate to, onto your stack after that method is called i.e. use PushViewController(yourViewController) afterwards. Also, you should be using the new IMvxNavigationService by MvvmCross. You can give this a try:
var navigationService = Mvx.Resolve<IMvxNavigationService>();
LoginWithFacebookCommand.Subscribe(async (token) => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
await navigationService.Navigate<HomeViewModel>();
});
To clear the backstack you basically need to override the Show method in the presenter and check whether your viewmodel is being called. If it is then set a new array of viewControllers. (Credit to #pnavk!!)
public class CustomPresenter : MvxIosViewPresenter
{
public override void Show(IMvxIosView view, MvxViewModelRequest request)
{
if (MasterNavigationController != null && view.ViewModel.GetType() == typeof(HomeViewModel))
{
var viewController = view as UIViewController;
MasterNavigationController.SetViewControllers(new UIViewController[] { viewController }, true);
}
else
base.Show(view, request);
}
}
Try this:
navigation.SetViewControllers(new UIViewController[] { vc }, true);
vc is the ViewController you want to set as the root of the navigation stack. You will need to get a reference to it which you can using the ViewControllers property on the NavigationController.
true - means you want to animate.

Should I be thinking differently about cancelling the back button in Xamarin Forms

I have a Prism based Xamarin Forms app that contains an edit page that is wrapped in a Navigation page so there is a back button at top left on both Android and iOS. To avoid the user accidentally losing an edit in progress by accidentally clicking the back button (in particular on Android) we want to prompt them to confirm that they definitely want to cancel.
Thing is, this seems like something that is not baked in to Xamarin forms. You can override OnBackButtonPressed in a navigation page, but that only gets called for the hardware/software back button on Android. There are articles detailing techniques to intercept the actual arrow button at the top left on Android (involving overriding OnOptionsItemSelected in the Android MainActivity), but on iOS I'm not sure it is even possible.
So I can't help but wonder if I am going about this the wrong way? Should I not be intercepting the top left / hardware / software back button in this way? Seems like a pretty common thing to do (e.g. press back when editing a new contact in the android built in Contacts app and you get a prompt) but it really feels like I am fighting the system here somehow.
There are previous questions around this, most relevant appears to be How to intercept Navigation Bar Back Button Clicked in Xamarin Forms? - but I am looking for some broad brush suggestions for an approach here. My objective is to show the user a page with the <- arrow at top left for Android, "Cancel" for iOS but I would like to get some views about the best way to go about it that does not involve me fighting against prism / navigation pages / xamarin forms and (where possible) not breaking the various "best practices" on Android and iOS.
After going down the same path as you and being told not to prevent users from going back, I decided on showing an alert after they tap the back button (within ContentPage.OnDisappearing()) that says something like Would you like to save your work?.
If you go with this approach, be sure to use Application.MainPage.DisplayAlert() instead of just this.DisplayAlert() since your ContentPage might not be visible at that point.
Here is how I currently handle saving work when they click the back button (I consolidated a good bit of code and changed some things):
protected override async void OnDisappearing() {
base.OnDisappearing();
// At this point the page is gone or is disappearing, but all properties are still available
#region Auto-save Check and Execution
/*
* Checks to see if any edits have been made and if a save is not in progress, if both are true, it asks if they want to save, if yes, it checks for validation errors.
* If it finds them, it marks it as such in the model before saving the model to the DB and showing an alert stating what was done
*/
if(!_viewModel.WorkIsEdited || _viewModel.SaveInProgress) { //WorkIsEdited changes if they enter/change data or focus on certain elements such as a Picker
return;
}
if(!await Application.Current.MainPage.DisplayAlert("ALERT", "You have unsaved work! Would you like to save now?", "Yes", "No")) {
return;
}
if(await _viewModel.SaveClaimErrorsOrNotAsync()) { //The return value is whether validation succeeds or not, but it gets saved either way
App.SuccessToastConfig.Message = "Work saved successfully. Try saving it yourself next time!";
UserDialogs.Instance.Toast(App.SuccessToastConfig);
} else if(await Application.Current.MainPage.DisplayAlert("ERROR", "Work saved successfully but errors were detected. Tap the button to go back to your work.", "To Work Entry", "OK")) {
await Task.Delay(200); //BUG: On Android, the alert above could still be displayed when the page below is pushed, which prevents the page from displaying //BUG: On iOS 10+ currently the alerts are not fully removed from the view hierarchy when execution returns (a fix is in the works)
await Application.Current.MainPage.Navigation.PushAsync(new WorkPage(_viewModel.SavedWork));
}
#endregion
}
What you ask for is not possible. The back button tap cannot be canceled on iOS even in native apps. You can do some other tricks like having a custom 'back' button, but in general you shouldn't do that - you should instead have a modal dialog with the Done and Cancel buttons (or something similar).
If you use xamarin forms that code it is work.
CrossPlatform source
public class CoolContentPage : ContentPage
{
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(nameof(EnableBackButtonOverride), typeof(bool), typeof(CoolContentPage), false);
public bool EnableBackButtonOverride{
get { return (bool)GetValue(EnableBackButtonOverrideProperty); }
set { SetValue(EnableBackButtonOverrideProperty, value); }
}
}
}
Android source
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == 16908332)
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
return false;
}
return base.OnOptionsItemSelected(item);
}
else
{
return base.OnOptionsItemSelected(item);
}
}
public override void OnBackPressed()
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
}
else
{
base.OnBackPressed();
}
}
iOS source
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((CoolContentPage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
var backBtnImage = UIImage.FromBundle("iosbackarrow.png");
backBtnImage = backBtnImage.ImageWithRenderingMode
(UIImageRenderingMode.AlwaysTemplate);
var backBtn = new UIButton(UIButtonType.Custom)
{
HorizontalAlignment =
UIControlContentHorizontalAlignment.Left,
TitleEdgeInsets =
new UIEdgeInsets(11.5f, 15f, 10f, 0f),
ImageEdgeInsets =
new UIEdgeInsets(1f, 8f, 0f, 0f)
};
backBtn.SetTitle("Back", UIControlState.Normal);
backBtn.SetTitleColor(UIColor.White, UIControlState.Normal);
backBtn.SetTitleColor(UIColor.LightGray, UIControlState.Highlighted);
backBtn.Font = UIFont.FromName("HelveticaNeue", (nfloat)17);
backBtn.SetImage(backBtnImage, UIControlState.Normal);
backBtn.SizeToFit();
backBtn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if(((CoolContentPage)Element)?.
CustomBackButtonAction != null)
{
((CoolContentPage)Element)?.
CustomBackButtonAction.Invoke();
}
};
backBtn.Frame = new CGRect(
0,
0,
UIScreen.MainScreen.Bounds.Width / 4,
NavigationController.NavigationBar.Frame.Height);
var btnContainer = new UIView(
new CGRect(0, 0,
backBtn.Frame.Width, backBtn.Frame.Height));
btnContainer.AddSubview(backBtn);
var fixedSpace =
new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace)
{
Width = -16f
};
var backButtonItem = new UIBarButtonItem("",
UIBarButtonItemStyle.Plain, null)
{
CustomView = backBtn
};
NavigationController.TopViewController.NavigationItem.LeftBarButtonItems = new[] { fixedSpace, backButtonItem };
}
using in xamarin forms
public Page2()
{
InitializeComponent();
if (EnableBackButtonOverride)
{
this.CustomBackButtonAction = async () =>
{
var result = await this.DisplayAlert(null, "Go back?" Yes go back", "Nope");
if (result)
{
await Navigation.PopAsync(true);
}
};
}
}

If Rectangle doesn't Contains Mouse Position

I have a Rectangle which I can touch with this command below.
if ((mouse.LeftButton == ButtonState.Pressed )&&
TextureRectangle.Contains((int)MousePos.X,(int)MousePos.Y))
{
// Action;
}
But is there a Command like "Not Contains", so I wanna do something else if the user touch out of the "TextureRectangle" area?
When I click to the Rectangle that both actions starts. I really dont know where the problem is.
if (mouse.LeftButton == ButtonState.Pressed){
if(TextureRectangle.Contains((int)MousePos.X, (int)MousePos.Y)) {
music1.Play();
}
else{
music2.Play();
}
}
my problem is that music1 and music2 plays at same time if i click on the Rectangle, i want that when i click on the Rectangle that music1 plays only (here is the problem , both starts to play)and when i click out of the Rectangle should start only music2 to play ( this case is ok)
I would strongly recommend you to get a programming book / ebook and start reading it. This is basic computer logic stuff.
if (mouse.LeftButton == ButtonState.Pressed)
{
if (TextureRectangle.Contains((int)MousePos.X, (int)MousePos.Y))
{
// inside
}
else
{
// outside
}
}
OR
if (mouse.LeftButton == ButtonState.Pressed)
{
if (!TextureRectangle.Contains((int)MousePos.X, (int)MousePos.Y))
{
// outside
}
else
{
// inside
}
}

Resources