Flutter navigation best practice - flutter

I have a question for flutter developers that they have experience with angular or android sdk.
in android when you want to navigate between different screens without decreasing the performance we use fragments and we change them using frame layout
same thing for angular, we use router outlet and we navigate between components using routes
I want to implement the same thing with flutter (ps: I sow some articles like this one where the author navigate between his pages by updating the state and showing the right page conditionally witch I don't think that is a good practice)
So my question is there any way to use navigation in flutter like we do in angular or android ?
Thank you

Actually, there is no such thing as you said "Navigate just the specific area" in Flutter. Usually, I develop android apps, and I didn't see that in android either. Yes, you can change the fragment but this action is not navigation. It is kind of replace. If you try to replace the widget (or fragment) in flutter yes you can but, it is not navigation. Navigation is something else. Navigation is replacing the whole page.
Flutter focuses on widget's specific states. You can change every widget's state at any time. So, maybe you should change your perspective.

I have an app with multiple pages, I have used RouteGenerator to manges these routes, I also come from Angular background and found this to be the best approach as all my routes are located in one file.
https://flutter.dev/docs/cookbook/navigation/named-routes

You could use PageView.
PageController pageController = PageController(initialPage: 0);
int _selectedIndex = 0;
void _selectCurrentTab(int index) {
setState(() {
_selectedIndex = index;
});
pageController.animateToPage(_selectedIndex,
duration: Duration(milliseconds: 250), curve: Curves.linear);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: ...,
body: PageView(
pageSnapping: true,
controller: pageController,
children: [HomePage(), AboutPage()],
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(...),
BottomNavigationBarItem(...)
],
type: BottomNavigationBarType.fixed,
showUnselectedLabels: true,
currentIndex: _selectedIndex,
onTap: _selectCurrentTab,
)));
}

What you need is Nested Routing. Check this out Link on how to implement Nested Routing using Navigator 2.0.

Related

Flutter: push a new page and scroll down to certain widget

I have a web app in Flutter. My app bar has 5 buttons. Four of them scroll within the first page. The 5th one pushes a new page.
There are three different widgets on stage:
page A (HomeView)
page B (ContactView)
the AppBar
When the user is on page A, the buttons of page A should scroll down or up, and the button of page B should push the new view. When the user is on page B (contact) and presses any of the buttons of page A, it should push the home page and scroll to the required position.
I tried to do this with ScrollController.animateTo and Riverpod for state management, but I couldn't do it. The page scrolled down but, for some reason, it scrolled up again and didn't maintain position. I couldn't find any related post on the Internet and keepScrollOffset was not the answer. I also didn't know how to scroll to the desired position when the user is not on 0 (it scrolled down from current position).
After that, I tried to do it with GlobalKeys. It worked, up to a certain point. On page A, everything worked fine. However, from page B to page A, it said the keys are duplicated. I used pop instead of pushNamed as suggested in one of the answers of this post (the only answer that worked for me). The behavior was finally as expected, but I can't use pop since I have more pages on the website, so I can't ensure that the user is coming from page A. I tried with pushReplacement as the same answer gives that for an alternative option. It doesn't work now. When trying to go from page B (contact) to page A, it throws error Unexpected null value.. Furthermore, I need to keep the back button functionality since it is a website. I would prefer, then, to not pop the current page from the stack.
The page A (HomeView) is built like this inside of a StatefulWidget:
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
HomeHeader(key: HomeView.homeKey),
WhatWeOffer(key: HomeView.servicesKey),
WhatWeDo(key: HomeView.referencesKey),
WhoWeAre(key: HomeView.aboutKey),
const LetsGetInTouch(),
],
),
);
}
The GlobalKeys are defined like this inside of the Page A (HomeView) widget:
static GlobalKey homeKey = GlobalKey(debugLabel: 'homeHeaderKey');
static GlobalKey servicesKey = GlobalKey(debugLabel: 'whatWeOfferKey');
static GlobalKey referencesKey = GlobalKey(debugLabel: 'whatWeDoKey');
static GlobalKey aboutKey = GlobalKey(debugLabel: 'whoWeAreKey');
This is how I implement the navigation to page A:
if (ModalRoute.of(context)?.settings.name != '/') {
Navigator.pushReplacementNamed(
context,
Navigation(context).routes["/"]!,
);
}
Scrollable.ensureVisible(
HomeView.homeKey.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
And this is how I implement the navigation to page B:
Navigator.pushReplacementNamed(
context,
Navigation(context).routes["/contact"]!,
),
Maybe GlobalKey is not the right approach to my problem? Is there any easier way to push a new page and scroll down to a widget?
EDIT:
I think the Unexpected null value problem was caused because the view was not built yet when I was trying to call it. I solved it by making the functions async and waiting before using the keys:
TextButton(
child: Text('home', style: style),
onPressed: () async {
if (ModalRoute.of(context)?.settings.name != '/') {
Navigator.pushReplacementNamed(
context,
Navigation(context).routes['/']!,
);
await Future.delayed(const Duration(milliseconds: 500));
}
Scrollable.ensureVisible(
HomeView.homeKey.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
),
However, this does not solve my problem of the back button. As I use pushReplacementNamed instead of pushNamed, I can't go back. Is there a more optimal way to do this, where I can keep the back button functionality?

Flutter: Laggy performance with PageView when updating the currentIndex in a BottomNavigationBar (but no lag if I don't update the currentIndex)

I'm creating an app with a Scaffold that contains:
A FutureBuilder in the body that creates a PageView as its child when data is loaded.
A BottomNavigationBar that syncs with the PageView for a more intuitive navigation.
Functionality-wise, everything works fine. I can swipe left and right between pages and the currentIndex gets updated correctly in the BottomNavigationBar, and if I tap on the BottomNavigationBar elements the PageView will animate to the correct page as expected.
However... performance is really bad when switching between pages, even in Profile mode.
After a lot of investigation, I've confirmed that lag is only present if I update the currentIndex of the BottomNavigationBar.
If I don't update the BottomNavigationBar, animations remain very smooth when switching between pages, both when swiping on the PageView and when tapping on the BottomNavigationBar elements themselves.
I can also confirm that this happens exactly the same when using setState and when using Provider. I was really hoping that it was just the setState method being inefficient... but no luck :(
For the setState impementation, this is what I'm doing:
On the PageView:
onPageChanged: (page) {
setState(() {
_selectedIndex = page;
});
}
On the BottomBarNavigation:
onTap: _onTappedBar,
currentIndex: _selectedIndex
and below:
void _onTappedBar(int value) {
_pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
setState(() {
_selectedIndex = value;
});
}
If I comment out both setState methods, the app becomes buttery smooth again and I can use the BottomNavigationBar correctly as well - it just doesn't update the selected item.
Interestingly enough, if I ONLY comment out the line inside both setState methods (_selectedIndex = page; and _selectedIndex = value;) but leave the methods there, the app still lags all the same even though the setState methods are completely empty and aren't updating anything...??
And this is the Provider version:
On the PageView:
onPageChanged: (page) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = page;
}
On the BottomBarNavigation:
onTap: _onTappedBar,
currentIndex: Provider.of<BottomNavigationBarProvider>(context).currentIndex,
and below:
void _onTappedBar(int value) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = value;
pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
}
As said, just as laggy as the setState version :(
Any idea of what's causing this lag and how to fix this? I really don't know what else to try.
Ok, so I think I managed to solve it while also learning a valuable lesson about Flutter!
I was on the right track with the setState / Provider dilemma - you do need to use Provider (or another state management solution) if you want to avoid rebuilding the whole page.
However, that's not enough.
In order to leverage the modularity of that implementation, you ALSO need to extract the relevant widget (in this case, the whole BottomNavigationBar) outside the main widget. If you don't, it seems everything on the main page will still get rebuilt, even if only a small widget is listening for Provider notifications.
So this is the structure of my root_screen's build method now (simplified body contents for readaibility):
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: <Widget>[
HomeScreen(),
PerformanceScreen(),
SettingsScreen(),
],
onPageChanged: (page) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = page;
},
);
bottomNavigationBar: MyBottomNavigationBar(onTapped: _onTappedBar),
);
}
Notice how the bottomNavigationBar: parameter is no longer defined in this root_screen. Instead, I've created a new class (a StatelessWidget) in a separate Dart file that takes in an onTapped function as a parameter, and I'm instantiating it from here.
Said _onTappedBar function is defined right here on the root_screen, just below the build method:
void _onTappedBar(int value) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = value;
_pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
}
And this is the separate Dart file containing the new MyBottomNavigationBar class:
class MyBottomNavigationBar extends StatelessWidget {
#override
const MyBottomNavigationBar({
Key key,
#required this.onTapped,
}) : super(key: key);
final Function onTapped;
Widget build(BuildContext context) {
return BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up), title: Text('Performance')),
BottomNavigationBarItem(
icon: Icon(Icons.settings), title: Text('Settings')),
],
onTap: onTapped,
currentIndex:
Provider.of<BottomNavigationBarProvider>(context).currentIndex,
);
}
}
Also for completeness (and because I absolutely needed to know), I tried using the setState approach again while keeping the BottomNavigationBar in its new separate file. I wanted to understand if simply extracting the widgets was enough to do the trick, or if you still need to use a state management solution no matter what.
It turns out... it wasn't enough! Performance using setState was horrible again, even though the BottomNavigationBar widget was extracted in its own class file.
So bottom line, in order to keep your app efficient and animations smooth, remember to extract widgets and modularise your Flutter code as much as possible, as well as using a state management solution instead of setState. That seems to be the only way to avoid unnecessary redraws (and your code will obviously be much cleaner and easier to debug).

Flutter - Keep page static throughout lifecycle of app?

I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)

Flutter TextEditingController does not scroll above keyboard

In android version, Flutter TextEditingController does not scroll above keyboard like default text fields do when you start typing in field. I tried to look in sample apps provided in flutter example directory, but even there are no example of TextEditController with such behaviour.
Is there any way to implement this.
Thanks in advance.
so simple
if your textfields is between 5-10 fields
SingleChildScrollView(
reverse: true, // add this line in scroll view
child: ...
)
(August 20, 2021 Flutter 2.2.3)
I think my answer might be the cleanest solution for this problem:
#override
Widget build(BuildContext context) {
/// Get the [BuildContext] of the currently-focused
/// input field anywhere in the entire widget tree.
final focusedCtx = FocusManager.instance.primaryFocus!.context;
/// If u call [ensureVisible] while the keyboard is moving up
/// (the keyboard's display animation does not yet finish), this
/// will not work. U have to wait for the keyboard to be fully visible
Future.delayed(const Duration(milliseconds: 400))
.then((_) => Scrollable.ensureVisible(
focusedCtx!,
duration: const Duration(milliseconds: 200),
curve: Curves.easeIn,
));
/// [return] a [Column] of [TextField]s here...
}
Every time the keyboard kicks in or disappears, the Flutter framework will automatically call the build() method for u. U can try to place a breakpoint in the IDE to figure out this behavior yourself.
Future.delayed() will first immediately return a pending Future that will complete successfully after 400 milliseconds. Whenever the Dart runtime see a Future, it will enter a non-blocking I/O time (= inactive time = async time = pending time, meaning that the CPU is idle waiting for something to complete). While Dart runtime is waiting for this Future to complete, it will proceed to the lines of code below to build a column of text fields. And when the Future is complete, it will immediately jump back to the line of code of this Future and execute .then() method.
More about asynchronous programming from this simple visualization about non-blocking I/O and from the Flutter team.
Flutter does not have such thing by default.
Add your TextField in a ListView.
create ScrollController and assign it to the ListView's controller.
When you select the TextField, scroll the ListView using:
controller.jumpTo(value);
or if you wish to to have scrolling animation:
controller.animateTo(offset, duration: null, curve: null);
EDIT: Of course the duration and curve won't be null. I just copied and pasted it here.
Thank you all for the helpful answers #user2785693 pointed in the right direction.
I found complete working solution here:
here
Issue with just using scroll or focusNode.listner is, it was working only if I focus on textbox for the first time, but if I minimize the keyboard and again click on same text box which already had focus, the listner callback was not firing, so the auto scroll code was not running. Solution was to add "WidgetsBindingObserver" to state class and override "didChangeMetrics" function.
Hope this helps others to make Flutter forms more user friendly.
This is an attempt to provide a complete answer which combines information on how to detect the focus from this StackOverflow post with information on how to scroll from Arnold Parge.
I have only been using Flutter for a couple days so this might not be the best example of how to create a page or the input widget.
The link to the gist provided in the other post also looks like a more robust solution but I haven't tried it yet. The code below definitely works in my small test project.
import 'package:flutter/material.dart';
class MyPage extends StatefulWidget {
#override createState() => new MyPageState();
}
class MyPageState extends State<MyPage> {
ScrollController _scroll;
FocusNode _focus = new FocusNode();
#override void initState() {
super.initState();
_scroll = new ScrollController();
_focus.addListener(() {
_scroll.jumpTo(-1.0);
});
}
#override Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Some Page Title'),
),
body: new DropdownButtonHideUnderline(
child: new SafeArea(
top: false,
bottom: false,
child: new ListView(
controller: _scroll,
padding: const EdgeInsets.all(16.0),
children: <Widget>[
// ... several other input widgets which force the TextField lower down the page ...
new TextField(
decoration: const InputDecoration(labelText: 'The label'),
focusNode: _focus,
),
],
),
),
),
);
}
}

Flutter: Use Navigator with TabBar and TabBarView

I'm trying to understand the class Navigator.
A Navigator Route seem to always return a new widget but what if I want to manage the TabBar and TabBarView so that each Tab when tapped or swipe on, will be pushed to the Navigator stack, I don't find a what to do that.
On a more general case, can I react to a route change without creating a new widget but instead taking another action like scrolling to a specific item in a listView?
I've tried recreating the entire app structure every time but doing this way I don't have the nice default animation and, also, doesn't seem a good approach to me.
You can use WillPopScope widget and its onWillPop to catch the back button pressure and handle it yourself. Find more info here https://docs.flutter.io/flutter/widgets/WillPopScope-class.html
On a more general case, can I react to a route change without creating
a new widget but instead taking another action like scrolling to a
specific item in a listView?
This looks more specific rather than general. However, you do need to set a ScrollController in your ListView and let it scroll the list for you to the desired point. A simple example function returning to top:
class MyFancyClass extends StatelessWidget{
...
ScrollController _scrollController;
...
#override
Widget build(BuildContext Context){
return ....
new ListView(
...
controller: _scrollController,
...
}
void _toTop() {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.ease,
);
}
Check https://docs.flutter.io/flutter/widgets/ScrollController-class.html for more details and behaviors. In case you want the back button to bring you to the top:
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onTop,
child:
...
),
);
}
Concerning the tab behavior I suggest you to read https://docs.flutter.io/flutter/material/TabController-class.html to better understand how to implement what you have in your mind.