The app I am developing attempts to adopt, as much as possible, the most native experience for Android & iOS users.
To do so, the app runs a CupertinoApp and a MaterialApp.
On iOS, I use the CupertinoScaffold showing a CupertinoTabBar with BottomNavigationBarItems. It performs very well as expected:
pages are created only once, the first time I click on a tab;
pages are restored when returning to a previously displayed tab: in one of my tab, there is a list and its scroll position is always maintained, persisted.
on Android, the story is different: I am using the Drawer widget.
When I click on an entry, I call Navigator#push to show the associated page. However, this will keep adding new instances of each entry's page in the stack.
I don't seem to be able to return to an existing page. At least, not how I can do it with iOS.
Looking at the Navigator I see functions that seem to achieve what I am looking for:
popUntil => will show my existing screen, but at the expanses of all the other ones that will therefore make a sacrifice for the sake of just one page. So much ado for nothing...
pushReplacement => will show a new instance of the target page and destroy the current one. Not desirable.
What am I missing ? How can I achieve that the CupertinoTabBar seems to be able to do?
Well, I solved my issue by taking another direction.
Simply put I don't think there is a way, with the Navigator to achieve what I want.
The solution I find out was: Keep state of widgets with Bottom Navigation Bar in Flutter
This guide suggests to use a IndexedStack that displays only one child at the time while keeping its state.
So this is how I managed to make it work:
Create your MaterialApp
Set its home to be a custom stateful widget called MaterialHomePage
This MaterialHomePage builds a Scaffold:
with an AppBar and its title (the title of the active page);
with a drawer and its arguments (more on that later)
and the body is that so-called wonderful IndexedStack:
set its children to be the list of pages / widgets, the same pages available in the drawer
set index to be the property _selectedPageIndex of your MaterialHomePage
and with a function that will be triggered when the user chooses a page to navigate to, in which case the usual call to setState occurs inside of which you may update the _selectedPageIndex property as well as _selectedPageIndexproperty`.
The Drawer:
takes two parameters:
the current active page index (defaults to 0 at app launch)
a Function with the target page title & index
must list as many entries (usually ListTile) as pages you have declared in the IndexedStack displayed in MaterialHomePage.
upon clic on the entry, it calls the aforementioned function and passes the index of the clicked entry as well as the associated page title.
At the end, what happens:
The user opens the drawer.
The user chooses a new destination (let's say: entry #3).
MaterialHomePage gets notified (via callback) of the user choice which triggers a setStage that will both:
update the current page thanks to IndexedStack with index = 3
and update its AppBar's title
and voilĂ there you go: each page gets retained, no loss of state and a smooth navigation.
NB: if the user uses the Android's back button feature, at this point it will close the app since the Navigator stack is empty. Maybe there is a way to listen for such an event if you desire to default to the first entry / page (displayed at launch) if the user was in a another page.
NB2: I also wonder if there is a way to animate page transitions.
PS: Let me know if the answers is a fine solution in which case I'll validate it. Or if you find a proper way to achieve it.
Related
short version: navigating into a RecyclerView with TalkBack gestures forces you to swipe through every item before you can move on, is that normal? Is there a standard way to navigate out and onto the next element?
I'm trying to get accessibility features working smoothly in an app I'm working on, but I've run into a problem with part of the UI. I'm using RecyclerViews sized so you can only see one item at a time, snapping to the centre, so you can swipe to change the current value from a range. It's basically the NumberPicker approach but directly in the UI, instead of a separate dialog.
The issue is by doing swipe gestures (Default linear navigation) through the UI, as soon as it lands on the RecyclerView, it starts on the current item and then navigates through every single item in the list. It won't exit the RecyclerView until it's hit the last item and has nowhere else to go.
My question is, is this normal? I'm new to TalkBack and I can't find much info on what's expected in every situation. I know my situation is a little unusual (since navigating the RecyclerView implicitly means changing the current item) but just being trapped inside the list until you go through every item seems a really strange way to navigate in general.
I know there are solutions like adding navigation headings etc (which is how the Play Store seems to handle this with their "infinite" horizontally scrolling app selections) but I just wanted to check I wasn't missing something. TalkBack announces "in list" and "out of list" when you tap on/outside of the RecyclerView, so it seems like there should be a way to explicitly navigate in and out with a gesture too...? Would users be used to switching to Controls or Headings navigations to do this?
Thanks! I really want to make this work for everyone but it's hard to know if I've done something wrong, or if I'm just bumping into limitations with the standard components
I have built a Xamarin Android app that presents the user with a series of data entry forms, like a wizard. The wizard has a bottom navigation bar with previous and next buttons, and a menu button that when pressed displays a list of all forms in the wizard and allows the user to jump to any given form.
The desired functionality is to preserve the linear navigation, so that when the user jumps to the middle of the wizard, they can still use the previous and next buttons to page through the various forms in order. They should also be able to use the hardware back button to view the previous form in the wizard.
I suspect my implementation is not MvvmCross friendly because I'm seeing some bugs with it, specifically my viewmodels are not destroyed when I clear the fragment backstack (wizard hosted in an Activity, each form is a Fragment).
How should I implement this?
Have you tried using this overload of PopBackStackImmediate? This one will pop all of the fragments until the one you specify in the string (if inclusive flag is passed, then that fragment is also popped) so you spare iterating all over the fragment backstack.
Activity.SupportFragmentManager.PopBackStackImmediate("myViewTag", (int)PopBackStackFlags.Inclusive);
where "myViewTag" is the UniqueImmutableCacheTag of your View
There has to be something unconventional about my implementation, but with a deadline and little help from the community, what do you do to fix your hack, but to hack it more?
My solution was twofold:
1) instead of using FragmentManager.PopBackstackImmediate(), I implemented a while loop with the condition: activity.SupportFragmentManager.BackStackEntryCount > 0, calling Close(fragment.ViewModel) in the body. This should have fixed the bug, but it didn't.
2) The ViewModels I was requesting to Close were still not being disposed, so I had to resolve the current IMvxMultipleViewModelCache and call GetAndClear on it with the expected parameters. This forced my ViewModels to be disposed so they will be recreated on the next viewing of its Fragment.
This feels like a hack. Closing a ViewModel should dispose of it whether it's associated with a Fragment or Activity, but for some reason it wasn't. That's the key to this bug, but like I said, deadline, hack on hack.
Use case:
Enable the Google TalkBack accessibility service and navigate to an
app (e.g. Google Messenger)
In this app, assuming that you'll be presented with a list of items
to select, arbitrarily navigate to one of them using Talkback gestures (swipe right or just click once on one of them)
Rotate the screen
Expected behaviour:
The previously highlighted item should still be highlighted; the user
should be able to continue it's navigation
The Google Messenger app is a perfect example of this correct
behaviour
Messenger After rotation
My Sunshine app behaviour:
The previously highlighted item is not highlighted after Screen
Rotation
The user has to navigate again to the previously selected item
Depending on the screen, finding and having an item selected again can be a pain... not what we want to induce to our user
Sunshine After rotation
How should we implement this?
I'm thinking, as a solution, at Accessibility Events and intercepting them... but this doesn't seem to be right, doesn't seem to be "best practice" (e.g. creating a Custom View and implement the methods handling these Events)
! Note that the green highlighted list item doesn't seem to be focused (getCurrentFocused() returns null)
! Note that the list items become focused if we use D-Pad navigation, instead of TalkBack navigation (but this is another discussion...)
L.E:
I've spend a whole day on this, trying to "get the focus" of the
highlighted item, but the item is NOT focused. This is why I assume
that this feature must be tackled in some other way and I would like
to know your (!) experienced opinion before I spend another 2 days
re-creating all the used Android components (as I assume it could be done - this doesn't sound very "best practice", doesn't it ?)
This is NOT a homework, there's nothing wrong with my current code
(so, there's no code to post, unless one would like my whole project)
and, given that this is my first post, I could not attach more than 2
pictures (this is why the "before" screens are missing)
Just give me a good hint, based on experience, and I will implement it and post the finished, working code here.
It should work out of the box if you implement stable IDs in your RecyclerView.Adapter (and potentially disable the ItemAnimator on the RecyclerView (rv.setItemAnimator(null)) which you can do conditionally if TalkBack is enabled).
Here's a blog post I wrote about the item animator bug.
I ran into the same problem while working on the same project.
Hint: listView.setItemChecked(int position, boolean value);
Good luck :)
I'm trying to work out the best way to implement the 'ancestral' and 'temporal' navigation (utilising the up button and back buttons), for a music player I'm working on.
Currently, the user is presented with a viewpager, and can page between three main fragments (ArtistMain, AlbumMain and SongMain). Upon choosing an item inside that view, a fragment transaction occurs, and the viewpager goes out of view, replaced by a new fragment (AlbumSub, Songsub or player, depending on where the user came from). The user can then navigate deeper, until a song is chosen, and then they are taken to the 'player' screen.
I guess the question is: How do I implement all of this conditional navigation?
I'm fairly new to android and programming in general, and I just can't seem to come up with an efficient way to achieve this. At the moment, as each fragment is brought into view, the app is checking to see where the user just came from, and then determines where the user should be taken if back or home is called. This means I have a booleans like "fromArtistMain", "fromAlbumSub", and I'm checking for things like "fromSongSub && fromPlayer".. it's all turning into a bit of a mess.
I've drawn a diagram (in paint, sorry!!), to depict the navigation I'm trying to achieve. The yellow represents the 'up' button press, the red is the 'back' button press, and blue is just normal navigation. The green arrows are meant to represent the view paging:
Any advice is welcome. It might take something really simple that I've just overlooked.
Thanks for your time.
Edit:
I have been adding fragments to the backstack, and using popBackStack() calls, the problem is that popping the backstack is not necessarily the correct option in each case.
I've currently got a whole mess of code trying to determine whether a transaction should be added to the backstack in the first place.
Consider the following:
User chooses a song straight from 'SongMain', and is taken to 'Player'. Now using the home button should (in my mind), take the user to SongSub (the list of songs from the album that the chosen song belongs to). Now if the user navigates up again, they will be taken to 'AlbumSub', the list of Albums by that artist. This is a fragment transaction, but adding to the backstack would mean the user would be taken down a level on back press (which I think would be unexpected). So in this case I don't add that particular transaction to the backstack - seems fine, but there are quite a few different cases, and combined with a viewpager at the top which needs to come in and out of visibility, the code gets really messy.
All of this means a whole bunch of conditionals determining where the user came from and which path they took to get there..
Would it be wise to have a bunch of booleans in the host activity which get set depending on where the user has navigated, and then checking those booleans to see if a transaction should be added tot he backstack? This is kind of what I already have, but it seems really inefficient.
What you're looking into is called the Back stack. You can read more about it here at developer.android.com.
If you use a single Activity to host each of these Fragments then you can modify your code to explicity add your Fragments to the Fragment Back stack using code like so:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.container, fragment);
transaction.addToBackStack("NameOfFragment");
transaction.commit();
So, if you do the above code with Fragments like so:
Fragment1 -> Fragment2 -> Fragment3 -> Fragment4
Then from Fragment4 if you call this method:
getSupportFragmentManager().popBackStackImmediate();
Then Fragment4 will be finished and Fragment3 will restart. Simple. You can have this line called from a button click or you can override the behaviour of the back button to call it.
Please note in the examples I've used the function getSupportFragmentManager() which is a method name in the Compatibility Package. If you're not using the Compatibility Package then you can instead call getFragmentManager().
EDIT
The problem with the navigation you envisage is that breaking out of the backstack paradigm half way through means that your app will "Act Differently" than the rest of the OS. This is by and large discouraged by Google. But then again, saying that, I do exactly the same in my app for very similar reasons :).
When you navigate "up", along one of your yellow lines, you are following a discrete link (so, startActivity(new Intent(this, SongSub)); or whatever) and you want this to "break" the backstack.
It's at this stage you can make a decision about how you want to go forward:
You can start a Task (backstack) using SongSub as 0th item. This is from memory what the Google Music app does and you're right, it's annoying. When you press back it should technically exit the app. Yuk. IMO if you're in an obvious page hierarchy, back should always navigate down the hierarchy over exiting the app.
You can start a new Task using ArtistMain as the 0th element and layer fragments discretely ontop before commiting your transaction, in effect creating a new backstack each time you go "up" rather than "back" (your backstack would now be ArtistMain->ArtistSub->SongSub). This is what I think your trying to ask here. It's possible but it's messy.
You can create a more linear structure (probably the best idea if possible). Ignore the backstack paradigm, make "back" and "up" always go up a level no matter where you came from (Player always goes to SongSub, SongSub always goes to AlbumSub). This will give the user the least confusing and most transparent (as well as easiest to implement) experience - the user will learn quickly how to navigate (and how many "backs" to press) to get to where they want to be.
I'd like to have a button in the top left corner of one of my activities that holds the name of the activity under it on the stack. Is this possible? The button will essentially just finish the current activity, I just need to set the text to something like the label for the activity below it on the stack.
I've got a half-working solution where I just pass a string-extra to the activity wherever I start it from, which allows me to set the text to anything that makes sense. This does not work when I'm using notifications.
You could also use a custom Application object as a singleton to store the name of the last known activity (setting that in each activity's onCreate), but if you're launching multiple tasks or tasks with different affinities this won't really work properly.
But question your basic assumptions here: Is what your app is doing so unusual that you need to use up a chunk of screen real estate and introduce a navigational element totally unfamiliar to Android users who expect to just use the physical back button and to see the name of the current app/activity in the title bar (not the previous one)?