Flutter Push vs. State-related views - flutter

There are two ways to change what a user sees on display: I can push to another page or I can change the state of my stateful widget and rebuild it. Can you tell me, which way is best practice? (And if it depends - what I guess - on what?)
Pushing:
class Pushing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () => Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage())),)
),
);
}
}
Using States
class UsingStates extends StatefulWidget {
#override
State createState() => new _UsingStatesState();
}
class _UsingStatesState extends State<UsingStates> {
bool isPageTwo;
#override
void initState() {
isPageTwo = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isPageTwo ? Center(child: Text('Page two')) : Center(child: RaisedButton(onPressed: () {
setState(() {
isPageTwo = true;
});
})),
);
}
}

Of course the answer is: It depends.
When to use Navigator:
Routes pushed with the navigator are in a way equivalent to...
Activity and Fragment in Android
A route in Angular or React
A html file of a classic web page
You would use the Navigator to switch between logically separate parts of your app. Think of a StackOverflow app that comes with different pages, e.g. "Question List", "Question Detail", "Ask Question" form and "User Profile".
Navigator takes care of back navigation (hardware back button on android phones + back arrow in AppBar)
Note that a route does not have to overlay the full screen. showDialog also uses Navigator.push() internally (that's why you use Navigator.pop() to dismiss a dialog.
Similar to startActivityForResult on Android, a route can also return a result when pop is called. Think of a dialog that lets you pick a date.
When to use State:
Use State when the screens are a logical unit, e.g.:
When you load a list of items from a server, you would have 4 different states:
Loading
"An error occured..."
Placeholder displayed when the list is empty
The ListView
A form with multiple steps
A screen with multiple tabs (in this case the navigations is handled by the tab bar)
A "Please wait" overlay that blocks the screen while a POST request is sent to the server
After all Navigator is also a StatefulWidget that keeps track of the route history. Stateful widgets are a fundamental building block of Flutter. When your app is really complex and Navigator does not fit your needs, you can always create your own StatefulWidget for full control.
It always helps to look at Flutter's source code (CTRL + B in Android Studio).

import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Page1(title: 'Flutter Demo Home Page'),
);
}
}
class Page1 extends StatefulWidget {
Page1({Key key, this.title}) : super(key: key);
final String title;
#override
Page1State createState() => new Page1State();
}
class Page1State extends State<Page1> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 1, Press here to go to page 2"),onPressed:()=>streamController.add(2) ,),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 1 stream : "+intValue.toString());
if(intValue==2){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page2(title: "Page 2",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
class Page2 extends StatefulWidget {
Page2({Key key, this.title}) : super(key: key);
final String title;
#override
Page2State createState() => new Page2State();
}
class Page2State extends State<Page2> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 2, Press here to go to page 2"),onPressed:()=> streamController.add(1),),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 2 stream : "+intValue.toString());
if(intValue==1){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page1(title: "Page 1",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
Sorry for bad formatting. You can run this code without any dependencies. Hope it helped

Related

Flutter pushing two page to navigator sequentially

I want to push two pages to the flutter navigator one after another, so that going back from 2nd page redirects me to the first page. The code for this action will look somewhat like below -
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
The above code works fine. But my confusion is, will it work always as the pushNamed function is asynchronous as it returns a future value. So it could happen that the second page got pushed to navigator before the first page.
The ideal implementation seems to me to wait for the first call to pushNamed return its value and then call the second one. But strangely the following two solutions don't work. The first page did get pushed but it doesn't push the second page.
Solution 1(Not working):
Navigator.of(context).pushNamed(
FirstPage.PATH.then((_) =>
Navigator.of(context).pushNamed(SecondPage.PATH));
Solution 2(Not working):
await Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
Can anyone please clarify what I'm thinking wrong? Any help will be much appreciated. Thanks in Advance!
As an option you can pass a callback to pageA, add animation listener and call this callback when animation is finished.
this is full example:
import 'package:flutter/material.dart';
main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
routes: {
'pageA': (context) => PageA(),
'pageB': (context) => PageB(),
},
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _onPressed,
child: Text('press me'),
),
),
);
}
void _onPressed() {
Navigator.of(context).pushNamed(PageA.routeName, arguments: _pushNextPage);
}
void _pushNextPage() {
Navigator.of(context).pushNamed(PageB.routeName);
}
}
class PageA extends StatefulWidget {
static const routeName = 'pageA';
#override
_PageAState createState() => _PageAState();
}
class _PageAState extends State<PageA> {
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
ModalRoute.of(context)?.animation?.addStatusListener(_statusListener);
});
}
void _statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
final route = ModalRoute.of(context);
route?.animation?.removeStatusListener(_statusListener);
final callback = route?.settings.arguments as VoidCallback;
callback.call();
}
}
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE A')));
}
class PageB extends StatelessWidget {
static const routeName = 'pageB';
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE B')));
}
Your solutions do not work, because the Future returned by pushNamed is only completed when the page is removed from the navigation stack again.
So in your examples, the second page is pushed, once the first page has been closed.
I don't think it can happen, that the second page will be pushed before the first page in this example:
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
This should be a valid solution for what you want to achieve.

The callback which is called everytime Navigator.push to next page

I have two Pages moving each others like this.
MyHomePage -> ResPage -> MyHomePage -> ResPage
I want to exec the function when every time the ResPage appears.
code is this
Navigator.push(context,MaterialPageRoute(
builder: (context) => ResPage()));
Navigator.push(context,MaterialPageRoute(
builder: (context) => MyHomePage())
resPage is Stateful Widget.
class ResPage extends StatefulWidget {
ResPage({Key key}) : super(key: key);
#override
_ResPageState createState() => _ResPageState();
}
class _ResPageState extends State<ResPage> {
void initState(){ // it called just once.
}
#override
Widget build(BuildContext context) {
return Stack( // it called many times.
children: <Widget>[
background,
Scaffold(
backgroundColor: Colors.transparent,
body: resStack
),
],
);
}
initState() is called one time, and build is called many times.
Is there any call back when the page appears??
Yes it is!
You can add a post frame callback(a call after the widget was built)
like this:
Widget build(BuildContext context) {
WidgetsBinding.addPostFrameCallback(functionToCall);
}

Calling method in main/App class in Flutter?

I need to call from another widget the App class. How can I get a reference to the app class? I tried something like this:
static App myApp = this;
but a "this" is not defined, nor a "self".
Is there a way to make a "App" variable or put the app object into some kind of global variable?
EDIT:
To be more clear: I use a tabbed navigation style app and want to display a fullscreen spinning indicator (ModalProgressHud) that something is loading from the backend.
Now when I add the spinner code to some of the screens, the tabs will still be visible and clickable when the spinner is shown. Hence the idea to move the spinner code to the main app file, surrounding the tabbar creation.
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'My cool app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: new App(),
);
}
}
Now in the App class, I initiate the tabs like this and wrap them in the build function inside the spinning indicators call ("ModalProgressHud"):
body: ModalProgressHUD(child: buildTabs(context), inAsyncCall: _saving, color: Colors.grey, opacity: 0.5),
import 'package:modal_progress_hud/modal_progress_hud.dart';
class App extends StatefulWidget {
#override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
bool _saving = false;
TabItem currentTab = TabItem.Dashboard;
Map<TabItem, GlobalKey<NavigatorState>> navigatorKeys = {
TabItem.Dashboard: GlobalKey<NavigatorState>(),
TabItem.Family: GlobalKey<NavigatorState>(),
TabItem.Groups: GlobalKey<NavigatorState>(),
TabItem.ShoppingList: GlobalKey<NavigatorState>(),
TabItem.Me: GlobalKey<NavigatorState>(),
};
AppState() {
_initManagers();
}
void _initManagers() {
new BackendManager();
new ShoppingListManager();
new UserManager();
}
void _selectTab(TabItem tabItem) {
UserManager user = new UserManager();
if (user.userIsLoggedIn()) {
// only if user is logged-in we allow to switch bottom navi tabs
setState(() {
currentTab = tabItem;
});
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async =>
!await navigatorKeys[currentTab].currentState.maybePop(),
child: Scaffold(
resizeToAvoidBottomPadding: false,
resizeToAvoidBottomInset: false,
// HERE THE WRAP OF THE MAIN TABS IN THE HUD WIDGET
body: ModalProgressHUD(child: buildTabs(context), inAsyncCall: _saving, color: Colors.grey, opacity: 0.5),
bottomNavigationBar: BottomNavigation(
currentTab: currentTab,
onSelectTab: _selectTab,
),
),
);
}
Widget buildTabs(BuildContext context) {
return Stack(children: <Widget>[
_buildOffstageNavigator(TabItem.Dashboard),
_buildOffstageNavigator(TabItem.Search),
_buildOffstageNavigator(TabItem.Shop),
_buildOffstageNavigator(TabItem.ShoppingList),
_buildOffstageNavigator(TabItem.Me),
]);
}
Widget _buildOffstageNavigator(TabItem tabItem) {
return Offstage(
offstage: currentTab != tabItem,
child: TabNavigator(
navigatorKey: navigatorKeys[tabItem],
tabItem: tabItem,
),
);
}
void _submit() {
setState(() {
_saving = true;
});
//Simulate a service call
print('>>>>>> submitting to backend...');
new Future.delayed(new Duration(seconds: 4), () {
setState(() {
_saving = false;
});
});
}
}
The ModalProgressHud is now in the app class. My problem is now, I want to set / call the ModalProgressHud from any other widget to show the fullscreen overlay spinning indicator.
Hence I was thinking if a global static variable works (and how do I set this?) or if there is any other way to call the submit() function inside the App class.
First, I wonder why you need to pass the reference of the app class?
If you want to pass the app instance or reference it to the child widget, you can create a widget class with a constructor accepting the App object.
NOTE: If you need this because you have to pass on some data, you might consider using provider or inheritedWidget + BloC.
This is just a rough example, if you can provide more details that might actually help, please do.
AnotherWidget.dart
class AnotherWidget extends StatelessWidget {
final MyApp myApp;
const AnotherWidget({Key key, this.myApp}) : super(key: key);
// Do whatever you want with myApp instance
}
MyApp.dart
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AnotherWidgetDemo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AnotherWidget(myApp: this),
);
}
}
Hope this helps.
Try something like they do in Flutter for Web, import the file and use as, then you have the reference for calling a method
import 'package:hello_web/main.dart' as app;
main() async {
app.main();
}

Accessing/Passing values between Widgets

I was wondering what would be the best way to access/pass variables between widgets. For example,
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
int number = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Jungle Book',
home: new Scaffold(
appBar: new AppBar(title: new Text('Jungle Book')),
drawer: new AppMenu(),
body: new MainPageManager(),
floatingActionButton: new FloatingActionButton(onPressed: null,child: new Icon(Icons.shuffle),),
),
);
}
}
class MainPageManager extends StatefulWidget {
#override
_MainPageManagerState createState() => _MainPageManagerState();
}
class _MainPageManagerState extends State<MainPageManager> {
List<String> keys = ["flamingo"]; // more to add later
final _random = new Random();
#override
Widget build(BuildContext context) {
return Center(
child: new Image.asset('images/'+keys[_random.nextInt(keys.length)]+'.jpg',
fit: BoxFit.fill,)
);
}
}
If I need to pass a value during the floatingActionButton.OnPressed to the MainPageManager, what would be best way to do it.
What am attempting to do is, when the FloatingActionButton is pressed, i need to update the MainPageManager with a new image.
I read somewhere that using Global keys is not a good idea. What could be alternative ?
PS: am more of a beginner with flutter.
Flutter is not opinionated about how you approach state management.
Since MainPageManager is a direct child of MyApp you could turn MyApp into a stateful widget and MainPageManager into a stateless widget instead, as you are not using the setState method at all in your MainPageManager. Then, in your onPressed callback you'd use the setState method to set the state of your MyApp component, which will cause flutter to rebuild MyApp and all child widgets (so MainPageManager will be rebuild as well).
The last thing to do would be to add a property to your now stateless MainPageManager, that you'd pass down to that component.
Example:
class MainPageManager extends StatelessWidget {
final String value;
MainPageManager({this.value});
#override
Widget build(BuildContext context) {
return Container(
child: Text("$value"),
);
}
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _valueBinding;
#override
void initState() {
_valueBinding = "Test";
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
MainPageManager(
value: _valueBinding,
),
MaterialButton(onPressed: () {
setState(() {
_valueBinding = "This will be shown in MainPageManager";
});
}, child: Text("Update Value"),)
],
);
}
}
For more complex use-cases:
Flutter comes with a state-management solution called InheritedWidget. However, the API is not that convenient and the flutter team actually promotes using a package called Provider instead, which builds on InheritedWidget.
If your app grows really big I actually recommend using the BloC pattern instead.

Flutter using inherited widgets with Navigator returning NoSuchMethodError

I'm making a flutter app. I have a homepage widget which shows two things
- device code;
temperature
At first, these are set to some default values, but the user then goes from the home page to a new route, call this widget the sensor widget. In this new page, it basically connects to a sensor. This sensor sends out the device code and temperature, and I am able to show it inthe sensor widget.
The problem comes when i want to show this new information onthe homepage. I made a button where the user can go back to the homepage, but I want a way to update the homepage with the values I have in my sensor widget.
Im making use of the InheritedWidget class to make this happen, but I keep getting a null error when I try to access the variables in the homepage.
Below is the inherited widget class for this.
class TemperatureContext extends InheritedWidget {
final int _deviceCode;
final double _temperature;
int get deviceCode => _deviceCode;
double get temperature => _temperature;
set deviceCode(int d) {_deviceCode = d;}
set temperature(double t) {_temperature = t}
TemperatureContext(this.deviceCode, this.temperature, {Key key, Widget child})
.super(key: key, child:child)
#override
bool updateShouldNotify(Widget oldWidget) {
return (temperature != oldWidget.temperature && deviceCode != oldWidget.deviceCode) }
static TemperatureContext of(BuildContext context) {
return context.inheritFromWidgetOfExactType(TemperatureContext) }
}
I then have the homepage, new_widget is a function that builds a widget based on the
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static int deviceCode = 0;
static double deviceCode = get_temp_globally();
#override
Widget build(BuildContext context) {
final tempContext = TemperatureContext.of(context);
Widget output = new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('Homepage'),
),
body: new Column(
children: <Widget>[
new_widget(tempContext.deviceCode, tempContext.temperature),
new FlatButton(
child: new Text('Set new sensor'),
onPressed: () {
Navigator.of(context).pushNamed('/ChangePage');
})
],
)));
return output;
}
Next is the change page widget where the user is taken to when they press the button in the home page
class SensorWidget extends StatefulWidget {
SensorWidget({Key key, this.title}) : super(key: key);
final String title;
#override
_SensorWidgetState createState() => new _SensorWidgetState();
}
class _SensorWidgetState extends State<SensorWidget> {
static int deviceCode = 0;
static double temperature = get_temp_globally();
/* Some code that gets the deviceCode,
temperature and sets them to the above
variables */
#override
Widget build(BuildContext context) {
output = TemperatureContext(
deviceCode,
temperature,
child: MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Sensor widget'),
actions: _buildActionButtons(),
),
floatingActionButton: _buildScanningButton(),
body: new Container(
child: new FlatButton(
child: new Text("Go back"),
onPressed: () {
Navigator.of(context).pop(true);
}
),
),
),
),
);
return output;
}
}
And this is my main.dart file
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Temperature detector',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new HomePage(),
routes: <String, WidgetBuilder> {
'/HomePage' : (BuildContext context) => new HomePage(),
'/SensorWidget': (BuildContext context) => new SensorWidget(),
},
);
}
}
Basically when I put the new_widget function in my HomePage class (which I didnt put here, but basically builds a widget based on the two arguements provided), I get a "NoSuchMethodError": the getter deviceCode was called on null.
I dont get why this is null since I already initialized it. Any help? Thanks
See https://flutter.io/flutter-for-android/#what-is-the-equivalent-of-startactivityforresult :
The Navigator class handles routing in Flutter and is used to get a result back from a route that you have pushed on the stack. This is done by awaiting on the Future returned by push().
For example, to start a location route that lets the user select their location, you could do the following:
Map coordinates = await Navigator.of(context).pushNamed('/location');
And then, inside your location route, once the user has selected their location you can pop the stack with the result:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});