How to handle up button in toolbar - Kotlin - android-fragments

I have manually/programmatically set up an up button in my toolbar for a fragment page with the following code in onCreateOptionsMenu in the fragment:
(activity as AppCompatActivity).setSupportActionBar?.setDisplayHomeAsUpEnabled(true)
Tapping the system back button will take the user back to the previous fragment but without an up button (that works) I think some users may get lost.
I am having difficulty trying to work out how to catch and handle the up button to pop the fragment off the back stack.
This SO post shows how to catch the the up button click however it is in Java and doesn't go on to explain how you would navigate up.
I think it needs to look something like the code below but there are errors everywhere:
The case android.R.id.home is showing an 'Incompatible types:Int and MenuItem' error?
onBackPressed() is showing an 'Unresolved reference' error.
Bad code:
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item) {
android.R.id.home -> {
onBackPressed()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
UPDATE:
A comment to another SO post has a potential solution in Kotlin. It catches the click on the up button and goes back to the previous fragment page but then the up button doesn't go away. So the up button now persists even on the top level fragment destinations in my app (the pages corresponding to each tab in the BottomNavigationView).
I think this might have to do with the fact that there is only one activity in my app and the way that I have set up the up button in the fragment as mentioned above? If so, is there a workaround or other way to set up the up button by referencing the fragment instead of the whole activity?
If it helps, this is the code in the RecyclerView inner ViewHolder class in the adapter.kt file that navigates to the fragment page in question:
class AdapterListItemDetails(val items: List<ItemsList>) : RecyclerView.Adapter<AdapterListItemDeatils.ItemsViewHolder>() {
//overrides for OnCreateViewHolder, getItemCount, onBindViewHolder
inner class ItemsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var currentItem: ItemsList? = null
var currentPosition: Int = 0
init {
itemView.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.goto_details, null))
}
fun setData(itemsList: ItemsList, position: Int) {
itemView.tview_Keys.text = itemsList!!.nameText
this.currentItem = itemsList
this.currentPosition = position
}
}
}

You have to override onBackPressed() method in activity and handle the fragment transactions with your manual code. If you could share some snippet of activity and fragment transactions will help me to give some proper solution.

Hi this is what i usually do:
in an activity find the navController from your navHostFragment
val navController = this.findNavController(R.id.myNavHostFragment)
Make sure it's connected to the ActionBar
NavigationUI.setupActionBarWithNavController(this, navController)
Then simply override onSupportNavigateUp, find your navController then navigate up
override fun onSupportNavigateUp(): Boolean{
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}

Related

Xamarin Forms AppShell How return to previous page

I see a lot of threads on this sort of subject but none seem to apply to my question. I don't have a navigation page, I have a hamburger menu -- so Push/PopAsync() would not appear to be the answer. And I don't want to go to a specific page, I want to go back to the previous page (whatever it was) so GoToAsync() would not appear to be the answer.
Xamarin app for Android and UWP with iOS somewhere in the future. The description of the problem below is specific to Android; it works a little differently on UWP.
I want to put a Help entry in the hamburger menu that will take the user to a help page in the default browser. Hamburger menu seems to only go to an app page, so I defined a "dummy" View page that displays "Loading ..." and issues Browser.OpenAsync() in its OnAppearing() method, and that pretty much works. The problem is that the user would expect that the Back button would take him or her to the page they were on before clicking Help. I tried a couple of things. I have gotten close with the following but it does not quite work correctly:
In each of my other Views' OnAppearing() I call a method that saves the value of Current.MainPage.CurrentItem in a static. Then in the Help page after the OpenAsync() I set Current.MainPage.CurrentItem to its setting from the last page before the Help page.
Console.WriteLine("#### HelpPage loading Web Help");
_ = State.DisplayHelpPage(this, "MainHelp"); // _ = await Browser.OpenAsync(uri, blo);
Console.WriteLine("#### HelpPage returning to previous page");
State.ReloadPreviousPage(); // Current.MainPage).CurrentItem = lastFlyoutItem;
It almost works. The first time I click Help in the hamburger menu I get
#### HelpPage loading Web Help
#### HelpPage returning to previous page
#### HelpPage loading Web Help
#### HelpPage returning to previous page
The Web page loads perfectly. But when I click the Back button it displays again. Obviously my OnAppearing() method has been driven twice, which I do not understand.
If I click the Back button again I come back to the previous page in the application just as I wanted. The next time I click Help in the Hamburger menu it takes me to my dummy View page with no Web page. Obviously, my OnAppearing() is not being driven at all. But after that it works perfectly. I can go to any app page, and click Help in the menu. I get the Web page, and the Back button takes me back to the app and the page. In UWP of course the browser does not load on top of the app Views, and I seem to see it being loaded twice every time.
So ... what should I be doing differently? And why is my OnAppearing() being driven twice and then not at all ... and thereafter as I would expect?
There are several parts to this answer:
Get the previous page on to the Navigation stack. This is done by intercepting the Route "//HelpPage", and replacing it with a route that ISN'T a child of Shell.
Remember "FakePageVisible", so we know to do "PopAsync" in OnResume, when app returns from browser.
(Optional) "Entering" flag prevents going to browser twice.
App.xaml.cs:
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
protected override void OnResume()
{
if (HelpPage.FakePageVisible) {
HelpPage.FakePageVisible = false;
var shell = MainPage as AppShell;
if (shell != null) {
shell.Navigation.PopAsync();
}
}
}
}
AppShell.xaml.cs:
public partial class AppShell : Xamarin.Forms.Shell
{
public AppShell()
{
InitializeComponent();
// Define a route that isn't a child of Shell.
Routing.RegisterRoute("Help2", typeof(HelpPage));
}
protected override void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
if (args.Current != null) {
if (args.Source == ShellNavigationSource.ShellItemChanged) {
if (args.Target.Location.OriginalString == "//HelpPage") {
// Cancel the original route.
args.Cancel();
Device.BeginInvokeOnMainThread(() => {
// Used by the next OnAppearing.
HelpPage.Entering = true;
// Go there by a route that isn't a child of Shell.
// Doing so, pushes our previous location on to Navigation stack.
Shell.Current.GoToAsync("Help2");
});
}
}
}
}
}
HelpPage.xaml.cs:
public partial class HelpPage : ContentPage
{
public static bool Entering;
public static bool FakePageVisible;
protected override void OnAppearing
{
// Make sure this only happens once (just in case).
if (Entering) {
Entering = false;
FakePageVisible = true;
Xamarin.Essentials.Browser.OpenAsync("https://aka.ms/xamarin-quickstart");
}
}
}
For a simple demo, this code communicates via static variables in HelpPage. Re-factor as appropriate for your situation.

Prism navigation usage from Xamarin Forms App.OnAppLinkRequestReceived

I am building an app that makes use of some App links. In below example, I want to open a ResetPasswordPage when the user activates a link from an e-mail.
protected override async void OnInitialized()
{
InitializeComponent();
await NavigationService.NavigateAsync("/NavigationPage/LoginPage");
}
protected override void OnAppLinkRequestReceived(Uri uri)
{
if (uri.Host.EndsWith("site.nl", StringComparison.OrdinalIgnoreCase))
{
if (uri.AbsolutePath.StartsWith(#"/appname/resetpassword/"))
{
if (uri.Segments.Length == 4)
{
string resetCode = uri.Segments[3];
NavigationParameters param = new();
param.Add(NavConst.PasswordResetCode, resetCode);
await NavigationService.NavigateAsync("ResetPasswordPage", param);
}
}
}
}
This code works, but not what I would expect (for both iOS and Android). I would expect that ResetPasswordPage would be added to the navigation stack that was set in OnInitialized, so that it would contain a back button in the navigation bar, bringing the user back to the login page in this case.
But instead, it looks like the navigation stack gets replaced. When ResetPasswordPage is shown, and the user clicks the hardware back button, the app is closed. However, i want it to go back to where the user was before.
Am I not understanding some navigation concepts well or might this be a bug?
The NavigationService is a rather unique service within Prism for Xamarin.Forms. It is constructed new for each page as navigation and the navigation stack is dependent on where you are navigating from.
For instance when you Navigate from the context of a MasterDetailPage / FlyoutPage, it understands that you aren't trying to push a modal on top of that page but instead you are starting from the context of Detail/Flyout. If the NavigationService continually updated the Page context then the navigation service injected into that MasterDetailPage/FlyoutPage's ViewModel would have no idea that it needed that page as the context to navigate from.
Your issue here ultimately is that you are Navigating from the context of the Application which has a NavigationService with no Page set. As a result even a relative Navigation will still have the effect of an absolute Navigation thus resetting the Application.MainPage since that NavigationService does not have the contextual understanding of another page.
Depending on your business requirements you have a few different options. One is to do an absolute Navigation that gives you navigation stack that you're looking for.
Optionally you may want to get the currently displayed page and update the NavigationService to understand it. In the App class you might add something like:
protected override void OnAppLinkRequestReceived(Uri uri)
{
if (NavigationService is IPageAware pa)
{
page.Page = PageUtilities.GetCurrentPage(MainPage);
}
if (someCondition)
{
// This will now navigate from relatively from the page
// returned by GetCurrentPage.
NavigationService.NavigateAsync("SomeRelativeUrl")
.OnNavigationError(HandleNavigationError);
}
}

fix java.lang.NullPointerException in the code

I am getting NullPointerException in the code for contextmenu.
here is the onCreateContextmenu Method
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
menu.add(0, MENU_EDIT, 0, "Edit");
menu.add(0, MENU_DELETE, 0, "Delete");
}
I am getting the error in the line long buttonId = info.id; in the code below
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
long buttonId = info.id;
switch (item.getItemId()) {
case MENU_EDIT:
function1(buttonId);
break;
case MENU_DELETE:
function2(buttonId);
break;
}
return true;
}
Can some one help me fix this
view isn't passed to onContextItemSelected and
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
long buttonId = info.id;
This code doesn't help because menuInfo is null when view is a button. The Android doc says "menuInfo Extra information about the item for which the context menu should be shown. This information will vary depending on the class ofv". When v is a ListView menuInfoapproach is fine. When it is a Button, it doesn't work.
In onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo), the View v is the button that produced the context menu. Copyv to a global View varaiable and use that where you need to have the view of the button that produced the context menu.
How to get the Button view which triggered a Context Menu?

UINavigationController and UINavigationBarDelegate.ShouldPopItem() with MonoTouch

How do I pop up an UIAlertView when the back button of a UINavigationBar (controlled by a UINavigationController) was tapped? Under certain conditions, I want to ask the user an "Are you sure?" type of question so he could either abort the action and stay on the current view or pop the navigation stack and go to the parent view.
The most appealing approach I found was to override ShouldPopItem() on UINavigationBar's Delegate.
Now, there is a quite similar question here: iphone navigationController : wait for uialertview response before to quit the current view
There are also a few other questions of similar nature, for example here:
Checking if a UIViewController is about to get Popped from a navigation stack?
and How to tell when back button is pressed in a UINavigationControllerStack
All of these state "subclass UINavigationController" as possible answers.
Then there is this one that reads like subclassing UINavigationController is generally not a good idea:
Monotouch: UINavigationController, override initWithRootViewController
The apple docs also say that UINavigationController is not intended to be subclassed.
A few others state that overriding ShouldPopItem() is not even possible when using a UINavigationController as that does not allow to assign a custom/subclassed UINavigationBarDelegate to the UINavigationBar.
None of my attempts of subclassing worked, my custom Delegate was not accepted.
I also read somewhere that it might be possible to implement ShouldPopItem() within my custom UINavigationController since it assigns itself as Delegate of its UINavigationBar.
Not much of a surprise, this didn't work. How would a subclass of UINavigationController know of the Methods belonging to UINavigationBarDelegate. It was rejected: "no suitable method found to override". Removing the "override" keyword compiled, but the method is ignored completely (as expected). I think, with Obj-C one could implement several Protocols (similar to Interfaces in C# AFAIK) to achieve that. Unfortunately, UINavigationBarDelegate is not an Interface but a Class in MonoTouch, so that seems impossible.
I'm pretty much lost here. How to override ShouldPopItem() on UINavigationBar's Delegate when it is controlled by a UINavigationController? Or is there any other way to pop up an UIAlertView and wait for it's result before possibly popping the navigation stack?
This post is a bit old, but in case you're still interested in a solution (still involves subclassing though):
This implements a "Are you sure you want to Quit?" alert when the back button is pressed, modified from the code here: http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller/
Turns out if you implement the UINavigationBarDelegate in the CustomNavigationController, you can make use of the shouldPopItem method:
CustomNavigationController.h :
#import <Foundation/Foundation.h>
#interface CustomNavigationController : UINavigationController <UIAlertViewDelegate, UINavigationBarDelegate> {
BOOL alertViewClicked;
BOOL regularPop;
}
#end
CustomNavigationController.m :
#import "CustomNavigationController.h"
#import "SettingsTableController.h"
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if (regularPop) {
regularPop = FALSE;
return YES;
}
if (alertViewClicked) {
alertViewClicked = FALSE;
return YES;
}
if ([self.topViewController isMemberOfClass:[SettingsTableViewController class]]) {
UIAlertView * exitAlert = [[[UIAlertView alloc] initWithTitle:#"Are you sure you want to quit?" message:nil delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Yes", nil] autorelease];
[exitAlert show];
return NO;
}
else {
regularPop = TRUE;
[self popViewControllerAnimated:YES];
return NO;
}
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
//Cancel button
}
else if (buttonIndex == 1) {
//Yes button
alertViewClicked = TRUE;
[self popViewControllerAnimated:YES];
}
}
#end
The weird logic with the "regularPop" bool is because for some reason just returning "YES" on shouldPopItem only pops the navbar, not the view associated with the navBar - for that to happen you have to directly call popViewControllerAnimated (which then calls shouldPopItem as part of its logic.)
For reference, the route I took after giving up on ShouldPopItem() is to replace the back button with a UIBarButtonItem that has a custom UIButton assigned as it's CustomView. The UIButton is crafted to look like the original back button using two images for the normal and the pressed state. Finally, hiding the original back button is required.
Way too much code for what it's supposed to do. So yeah, thanks Apple.
BTW: Another possibility is creating a UIButton with the secret UIButtonType 101 (which is actually the back button) but I avoided this as it may break at any later iOS version.
Override only UINavigationBarDelegate methods in a UINavigationController subclass and it should simply work. Be cautious that the protocol methods are also called when you push or pop a view controller from inside your code and not only when the back button is pressed. This is because them are push/pop notifications not button pressed actions.
Xamarin does provide the IUINavigationBarDelegate interface to allow you to implement the UINavigationBarDelegate as part of your custom UINavigationController class.
The interface however does not require that the ShouldPopItem method be implemented. All the interface does is add the appropriate Protocol attribute to the class so it can be used as a UINavigationBarDelegate.
So in addition you need to add the ShouldPopItem declaration to the class as follows:
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
I've merged this solution with a native Obj-C solution. This is the way I'm currently handling the cancellation of the BACK button in iOS
It seems that it is possible to handle the shouldPopItem method of the NavigationBar in this way:
Subclass a UINavigationController
Mark your custom UINavigationController with the IUINavigationBarDelegate
Add this method with the Export attribute
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
Now you can handle popping in the ShoulPopItem method. An example to this is to create an interface like this
public interface INavigationBackButton
{
// This method should return TRUE to cancel the "back operation" or "FALSE" to allow normal back
bool BackButtonPressed();
}
Then mark your UIViewController which needs to handle the back button with this interface. Implement something like this
public bool BackButtonPressed()
{
bool needToCancel = // Put your logic here. Remember to return true to CANCEL the back operation (like in Android)
return needToCancel;
}
Then in your ShouldPopItem Implementation have something like this
tanks to: https://github.com/onegray/UIViewController-BackButtonHandler/blob/master/UIViewController%2BBackButtonHandler.m
[Export("navigationBar:shouldPopItem:")]
public bool ShouldPopItem(UINavigationBar navigationBar, UINavigationItem item)
{
if (this.ViewControllers.Length < this.NavigationBar.Items.Length)
return true;
bool shouldPop = true;
UIViewController controller = this.TopViewController;
if (controller is INavigationBackButton)
shouldPop = !((INavigationBackButton)controller).BackButtonPressed();
if (shouldPop)
{
//MonoTouch.CoreFoundation.DispatchQueue.DispatchAsync
CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(
() =>
{
PopViewController(true);
});
}
else
{
// Workaround for iOS7.1. Thanks to #boliva - http://stackoverflow.com/posts/comments/34452906
foreach (UIView subview in this.NavigationBar.Subviews)
{
if(subview.Alpha < 1f)
UIView.Animate(.25f, () => subview.Alpha = 1);
}
}
return false;
}

FLEX: Programmatically remove Alert?

I need to programmatically remove an alert.
This is why:
My application uses BrowserManager to enable deep linking based off of the content in the #hash part of the url. If an alert is currently up, and the user hits the back button, the application will revert back to its previous state. But the Alert will still be up, and in many cases irrelevant at that point.
So is there a way to programmatically remove the Alert? so when the hash fragment changes I can remove it.
Thanks!
It turns out the Alert.show function returns an Alert reference and then just uses PopUpManager to add it to the display list. so if you capture the return reference when you call Alert.show you can tell PopUpManager to remove it. :)
You can do this by keeping the Alert object as member data, and then setting its visible property to false when you're done with it. Next time you need to show an Alert, don't create a new one - grab the one you've already created and set its properties, then set visible to true again.
private var myAlert : Alert;
public void showAlert( message: String, title : String ) : void
{
hideAlert();
myAlert = Alert.show( message, title, Alert.OK | Alert.NONMODAL );
}
public void hideAlert() : void
{
if( myAlert != null && myAlert.visible ) {
myAlert.visible = false;
}
}
I don't think that is possible.
You can create your own alert component subclassing TitleWindow and then use PopupManager to show/hide them.

Resources