Calling method in main/App class in Flutter? - 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();
}

Related

Flutter - How to safely change theme after awaiting theme data from an API in a Widget?

Using a normal setup for handling Theme with a ChangeNotifier that notifies the whole app / everything below it in the three - that something should be redrawn.
This approach seems general and there's multiple "guides" doing it this way. And this works works well when clicking a Button to change it. However, if the data for a Theme is coming from an API - where can we safely update the same value before rendering a Widget?
This is an example code where the ThemeData is somehow "downloaded" and supposed to be updated before rendering the view once the StreamBuilder is done. This, of course, causes the same Widget that's downloading something being redrawn while building so I'm getting a warning for that.
How can this be solved? The Theme can just be a single color that is downloaded and changed dynamically. And so far I haven't seen themes being changed inside one single widget while the "main one" is unchanged. Not sure what's the best approach to this (or similiar) issue - since it can't be uncommon in an mostly online based world.
Edit #1: Just to clarify - the Theme might change depending on the Widget / Page / Screen being loaded and it's not a "one time thing" where you initialize it at the beginning but with each screen being loaded - to customize that particular page based on online API data.
Example code:
void main() {
runApp(ChangeNotifierProvider(
create: (context) => ThemeConfig(),
child: MyApp()
));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child)
{
return MaterialApp(
theme: state.getTheme()
)
});
}
}
class _MyScreen extends State<MyScreen>
{
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Api.downloadTheme(),
builder: (context, snapshot)
{
// If OK render screen - But where to safely set the "Theme" from API?
return MyWidget(context.data)
});
)
}
}
class _MyWidget extends State<MyWidget>
{
#override
void initState() {
super.initState();
// This will cause the Widget tree to be redrawn while it's drawing and not work at all
// So when I've downloaded the data - where can this safely be changed?
Provider.of<ThemeConfig>(context).setTheme(widget.data.theme);
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I'm not sure if I understood you correctly, but if you wander how to update the theme by fetching if from an api, here is the example of simulating an api call which updates the theme:
void main() {
runApp(
ChangeNotifierProvider(
lazy: false, // triggering ThemeConfig constructor
create: (context) => ThemeConfig(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child) {
return MaterialApp(
theme: state.theme,
home: MyScreen(),
);
});
}
}
class ThemeConfig with ChangeNotifier {
ThemeConfig() {
// trigger theme fetch
getTheme();
}
ThemeData theme = ThemeData(primarySwatch: Colors.blue); // initial theme
Future<void> getTheme() async {
// TODO: fetch your theme data here and then update it like below
await Future.delayed(Duration(seconds: 3)); // simulating waiting for response
theme = ThemeData(primarySwatch: Colors.red);
notifyListeners();
}
}
class MyScreen extends StatelessWidget {
const MyScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(), // notice how the colors change
body: Center(
child: Container(
height: 200,
width: 200,
color: Theme.of(context).primaryColor,
),
),
);
}
}

Flutter navigation push, while keeping the same Appbar

I'm currently building a Flutter app where I'm struggling to figure out the best way to implement navigation.
I have 2 pages which are:
HomePage: from there I want to use an IndexedStack to manage the feed.
ProfilePage: the profile page, which (graphically) shares the same AppBar and the same Drawer as the home page.
In my App the user reaches the HomePage immediately after logging in. There is no navigation involved.
From there, I now have a TextButton, which calls Navigator.of(context).pushNamed(AppRoutes.profile).
As I said, both pages share the same Appbar and Drawer, so I created a custom myScaffold.
Both pages use this scaffold.
So the behavior is correct, since after clicking the button, the ProfilePage is moved over the HomePage.
My problem is that graphically the appbar should remain the same, but when the profile page is pushed, the animation makes it clear that it is not the same app bar.
Is it possible to animate the entry of the profile page, without
animating the rebuilding of the appbar?
Or is it possible to push a route directly into the scaffold content?
As an alternative I was just thinking of writing a function which
returns the page widget to be displayed within the scaffold content.
But this kind of approach doesn't seem right to me, since there are
routes.
From the official documentation you can see from the Interactive example what I mean:
Docs
When the second route is built over the first one, a new Appbar is built over the previous one.
But what if I need the appbar to stay the same?
You can create a sub-navigator using Navigator class.
I created a routes library (routes.dart) in my current project for navigating to other screens while bottomNavigationBar is still displayed. Using the same idea, you can perform navigations while using the same AppBar.
Here's the sample codes for your scenario.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorKey: Routes.rootNavigatorKey,
initialRoute: Routes.PAGE_INITIAL,
onGenerateRoute: Routes.onGenerateRoute,
);
}
}
routes.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter2sample/pages/home_page.dart';
import 'package:flutter2sample/pages/initial_page.dart';
import 'package:flutter2sample/pages/main_page.dart';
import 'package:flutter2sample/pages/profile_page.dart';
class Routes {
Routes._();
static const String PAGE_INITIAL = '/';
static const String PAGE_MAIN = '/main';
static const String PAGE_HOME = '/home';
static const String PAGE_PROFILE = '/profile';
static final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
static final GlobalKey<NavigatorState> mainNavigatorKey =
GlobalKey<NavigatorState>();
static String currentSubNavigatorInitialRoute;
static CupertinoPageRoute<Widget> onGenerateRoute(RouteSettings settings) {
Widget page;
switch (settings.name) {
case PAGE_INITIAL:
page = InitialPage();
break;
case PAGE_MAIN:
page = MainPage();
break;
case PAGE_HOME:
page = HomePage();
break;
case PAGE_PROFILE:
page = ProfilePage();
break;
}
if (settings.name == PAGE_INITIAL &&
currentSubNavigatorInitialRoute != null) {
// When current sub-navigator initial route is set,
// do not display initial route because it is already displayed.
return null;
}
return CupertinoPageRoute<Widget>(
builder: (_) {
if (currentSubNavigatorInitialRoute == settings.name) {
return WillPopScope(
onWillPop: () async => false,
child: page,
);
}
return page;
},
settings: settings,
);
}
/// [MaterialApp] navigator key.
///
///
static NavigatorState get rootNavigator => rootNavigatorKey.currentState;
/// [PAGE_MAIN] navigator key.
///
///
static NavigatorState get mainNavigator => mainNavigatorKey.currentState;
/// Navigate to screen via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void push(Widget screen, {NavigatorState navigator}) {
final CupertinoPageRoute<Widget> route = CupertinoPageRoute<Widget>(
builder: (_) => screen,
);
if (navigator != null) {
navigator.push(route);
return;
}
rootNavigator.push(route);
}
/// Navigate to route name via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pushNamed(
String routeName, {
NavigatorState navigator,
Object arguments,
}) {
if (navigator != null) {
navigator.pushNamed(routeName, arguments: arguments);
return;
}
rootNavigator.pushNamed(routeName, arguments: arguments);
}
/// Pop current route of [navigator].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pop<T extends Object>({
NavigatorState navigator,
T result,
}) {
if (navigator != null) {
navigator.pop(result);
return;
}
rootNavigator.pop(result);
}
}
//--------------------------------------------------------------------------------
/// A navigator widget who is a child of [MaterialApp] navigator.
///
///
class SubNavigator extends StatelessWidget {
const SubNavigator({
#required this.navigatorKey,
#required this.initialRoute,
Key key,
}) : super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final String initialRoute;
#override
Widget build(BuildContext context) {
final _SubNavigatorObserver _navigatorObserver = _SubNavigatorObserver(
initialRoute,
navigatorKey,
);
Routes.currentSubNavigatorInitialRoute = initialRoute;
return WillPopScope(
onWillPop: () async {
if (_navigatorObserver.isInitialPage) {
Routes.currentSubNavigatorInitialRoute = null;
await SystemNavigator.pop();
return true;
}
final bool canPop = navigatorKey.currentState.canPop();
if (canPop) {
navigatorKey.currentState.pop();
}
return !canPop;
},
child: Navigator(
key: navigatorKey,
observers: <NavigatorObserver>[_navigatorObserver],
initialRoute: initialRoute,
onGenerateRoute: Routes.onGenerateRoute,
),
);
}
}
//--------------------------------------------------------------------------------
/// [NavigatorObserver] of [SubNavigator] widget.
///
///
class _SubNavigatorObserver extends NavigatorObserver {
_SubNavigatorObserver(this._initialRoute, this._navigatorKey);
final String _initialRoute;
final GlobalKey<NavigatorState> _navigatorKey;
final List<String> _routeNameStack = <String>[];
bool _isInitialPage = false;
/// Flag if current route is the initial page.
///
///
bool get isInitialPage => _isInitialPage;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.add(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_routeNameStack.remove(oldRoute.settings.name);
_routeNameStack.add(newRoute.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
}
initial_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class InitialPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Initial Page'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is INITIAL page'),
TextButton(
onPressed: () => Routes.pushNamed(Routes.PAGE_MAIN),
child: const Text('To Main page'),
),
],
),
),
);
}
}
main_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main Page'),
),
body: SubNavigator(
navigatorKey: Routes.mainNavigatorKey,
initialRoute: Routes.PAGE_HOME,
),
);
}
}
home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.yellow,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is HOME page'),
TextButton(
onPressed: () => Routes.pushNamed(
Routes.PAGE_PROFILE,
navigator: Routes.mainNavigator,
),
child: const Text('To Profile page'),
),
],
),
),
),
);
}
}
profile_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class ProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is PROFILE page'),
TextButton(
onPressed: () => Routes.pop(navigator: Routes.mainNavigator),
child: const Text('Back to Home page'),
),
],
),
),
),
);
}
}

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});

Flutter Push vs. State-related views

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

Flutter One time Intro Screen?

I have an intro screen for my app, but it shows every time I open the app,
I need to show that for the 1st time only.
How to do that?
//THIS IS THE SCREEN COMES 1ST WHEN OPENING THE APP (SPLASHSCREEN)
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
//After 2seconds of time the Introscreen will e opened by bellow code
Timer(Duration(seconds: 2), () => MyNavigator.goToIntroscreen(context));
}
//The below code has the text to show for the spalshing screen
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Center(
child: Text('SPLASH SCREEN'),
),
);
}
}
Every time this screen opens the intro screen with 2 seconds delay.
but I want for the first time only How to do that with sharedpreference??
Please add the required code.
If you wish to show the intro screen only for the first time, you will need to save locally that this user has already seen intro.
For such thing you may use Shared Preference. There is a flutter package for Shared Preference which you can use
EDITED:
Please refer to the below complete tested code to understand how to use it:
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
color: Colors.blue,
home: new Splash(),
);
}
}
class Splash extends StatefulWidget {
#override
SplashState createState() => new SplashState();
}
class SplashState extends State<Splash> with AfterLayoutMixin<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new Home()));
} else {
await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (context) => new IntroScreen()));
}
}
#override
void afterFirstLayout(BuildContext context) => checkFirstSeen();
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text('Loading...'),
),
);
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
Thanks to Ben B for noticing the incorrect use of delay in initState. I had used a delay because sometimes the context is not ready immediately inside initState.
So now I have replaced that with afterFirstLayout which is ready with the context. You will need to install the package after_layout.
I was able to do without using after_layout package and Mixins and instead I have used FutureBuilder.
class SplashState extends State<Splash> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
return HomeScreen.id;
} else {
// Set the flag to true at the end of onboarding screen if everything is successfull and so I am commenting it out
// await prefs.setBool('seen', true);
return IntroScreen.id;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: checkFirstSeen(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return MaterialApp(
initialRoute: snapshot.data,
routes: {
IntroScreen.id: (context) => IntroScreen(),
HomeScreen.id: (context) => HomeScreen(),
},
);
}
});
}
}
class HomeScreen extends StatelessWidget {
static String id = 'HomeScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hello'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
class IntroScreen extends StatelessWidget {
static String id = 'IntroScreen';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('IntroScreen'),
),
body: new Center(
child: new Text('This is the IntroScreen'),
),
);
}
}
I always try to use minimum count of packages, because in future it can conflict with ios or android. So my simple solution without any package:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final splashDelay = 2;
#override
void initState() {
super.initState();
_loadWidget();
}
_loadWidget() async {
var _duration = Duration(seconds: splashDelay);
return Timer(_duration, checkFirstSeen);
}
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _introSeen = (prefs.getBool('intro_seen') ?? false);
Navigator.pop(context);
if (_introSeen) {
Navigator.pushNamed(context, Routing.HomeViewRoute);
} else {
await prefs.setBool('intro_seen', true);
Navigator.pushNamed(context, Routing.IntroViewRoute);
}
}
#override
Widget build(BuildContext context) {
//your splash screen code
}
}
Use shared_preferences:
Full code:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
var boolKey = 'isFirstTime';
var isFirstTime = prefs.getBool(boolKey) ?? true;
runApp(MaterialApp(home: isFirstTime ? IntroScreen(prefs, boolKey) : RegularScreen()));
}
class IntroScreen extends StatelessWidget {
final SharedPreferences prefs;
final String boolKey;
IntroScreen(this.prefs, this.boolKey);
Widget build(BuildContext context) {
prefs.setBool(boolKey, false); // You might want to save this on a callback.
return Scaffold();
}
}
class RegularScreen extends StatelessWidget {
Widget build(BuildContext context) => Scaffold();
}
I just had to do exactly the same thing, here's how I did it:
First, in my main method, I open the normal main page and the tutorial:
MaterialApp(
title: 'myApp',
onGenerateInitialRoutes: (_) => [MaterialPageRoute(builder: mainPageRoute), MaterialPageRoute(builder: tutorialSliderRoute)],
)
...and then I use a FutureBuilder to build the tutorial only if necessary:
var tutorialSliderRoute = (context) => FutureBuilder(
future: Provider.of<UserConfiguration>(context, listen: false).loadShowTutorial() // does a lookup using Shared Preferences
.timeout(Duration(seconds: 3), onTimeout: () => false),
initialData: null,
builder: (context, snapshot){
if (snapshot.data == null){
return CircularProgressIndicator(); // This is displayed for up to 3 seconds, in case data loading doesn't return for some reason...
} else if (snapshot.data == true){
return TutorialSlider(); // The Tutorial, implemented using IntroSlider()
} else {
// In case the tutorial shouldn't be shown, just return an empty Container and immediately pop it again so that the app's main page becomes visible.
SchedulerBinding.instance.addPostFrameCallback((_){Navigator.of(context).pop();});
return Container(width: 0, height: 0);
}
},
);
Also, I think the tutorial should be shown again in case the user does not finish it, so I set only set the variable showTutorial to false once the user has completed (or skipped) the tutorial:
class TutorialSlider extends StatefulWidget {
#override
State<StatefulWidget> createState() => TutorialSliderState();
}
class TutorialSliderState extends State<TutorialSlider> {
...
#override
Widget build(BuildContext context) => IntroSlider(
...
onDonePress: (){
Provider.of<UserConfiguration>(context, listen: false).setShowTutorial(false);
Navigator.of(context).pop();
}
);
}
I took a different approach. I agree with the other answers that you should save your isFirstRun status via SharedPreferences. The tricky part then is how to show the correct widget in such a way that when you hit back you close out of the app correctly, etc. I first tried doing this by launching a my SplashWidget while building my HomePageWidget, but this turned out to lead to some weird Navigator errors.
Instead, I wound up calling runApp() multiple times with my different widget as appropriate. When I need to close the SplashWidget, rather than pop it, I just call runApp() again, this time with my HomePageWidget as the child property. It is safe to call runApp() multiple times according to this issue, indeed even for splash screens.
So it looks something like this (simplified obviously):
Future<void> main() async {
bool needsFirstRun = await retrieveNeedsFirstRunFromPrefs();
if (needsFirstRun) {
// This is will probably be an async method but no need to
// delay the first widget.
saveFirstRunSeen();
runApp(child: SplashScreenWidget(isFirstRun: true));
} else {
runApp(child: HomePageWidget());
}
}
I have an isFirstRun property on SplashScreenWidget because I can launch it in two ways--once as a true splash screen, and once from settings so that users can see it again if they want. I then inspect that in SplashScreenWidget to determine how I should return to the app.
class SplashScreenWidget extends StatefulWidget {
final bool isFirstRun;
// <snip> the constructor and getState()
}
class _SplashScreenWidgetState extends State<SplashScreenWidget> {
// This is invoked either by a 'skip' button or by completing the
// splash screen experience. If they just hit back, they'll be
// kicked out of the app (which seems like the correct behavior
// to me), but if you wanted to prevent that you could build a
// WillPopScope widget that instead launches the home screen if
// you want to make sure they always see it.
void dismissSplashScreen(BuildContext ctx) {
if (widget.isFirstRun) {
// Then we can't just Navigator.pop, because that will leave
// the user with nothing to go back to. Instead, we will
// call runApp() again, setting the base app widget to be
// our home screen.
runApp(child: HomePageWidget());
} else {
// It was launched via a MaterialRoute elsewhere in the
// app. We want the dismissal to just return them to where
// they were before.
Navigator.of(ctx).pop();
}
}
}