Navigating from a plugin callback - flutter

The problem is when I want to navigate from a callback - which is invoked by plugin - new page is pushed in as a widget inside my page.
This is the code:
import 'dart:async';
import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
) );
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
String barcode = "";
#override
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Barcode Scanner Example'),
),
body: new Center(
child: new Column(
children: <Widget>[
new Container(
child: new MaterialButton(
onPressed: scan, child: new Text("Scan")),
padding: const EdgeInsets.all(8.0),
),
],
),
));
}
Future scan() async {
try {
String barcode = await BarcodeScanner.scan();
print("${context}");
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return new BarcodePage(barcode);}
));
setState(() => this.barcode = barcode);
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.CameraAccessDenied) {
setState(() {
this.barcode = 'The user did not grant the camera permission!';
});
} else {
setState(() => this.barcode = 'Unknown error: $e');
}
} on FormatException{
setState(() => this.barcode = 'null (User returned using the "back"-button before scanning anything. Result)');
} catch (e) {
setState(() => this.barcode = 'Unknown error: $e');
}
}
}
class BarcodePage extends StatefulWidget {
BarcodePage(String s) {
str = s;
}
String str;
#override
State<StatefulWidget> createState() {
return _BarcodePageState(str);
}
}
class _BarcodePageState extends State<BarcodePage> {
String str;
_BarcodePageState(String s ){
str = s;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Bar code"),),
body: new Text(str),
);
}
}
You can find the application in https://github.com/arashbi/flutter_barcode_reader example folder
This is related to my question before, but simpler setup.

AFAI understand the problem is that callback is happening in the middle of render pipeline, and it causes the wrong behaviour. The solution is to use either Future.delayed or SchedulerBinding.instance.addPostFrameCallback
These methods causes the navigation to happen after the render pipeline, and the navigator can do its job properly

Related

Flutter: Error says - Could not find a generator for route RouteSettings while trying to navigate to another screen

Although questions with such error messages exist in this site, none solves my problem.
I have a button and on clicking the button, I just need to go to a different screen. But when ever I tap on the screen, the error shows up.
I first setup a route in MaterialApp and then tried to navigate to that route on tapping the button. The full code and the error message are given below:
Code:
import 'livesession1to1.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: CountDownTimer(),
navigatorKey: NavigationService.navigatorKey, // set property// Added by me later from prev project
// initialRoute: "/",
routes: <String, WidgetBuilder> {
'/liveSession1to1': (context) =>LiveSession1to1(),
},
)
);
}// end of main
class CountDownTimer extends StatefulWidget {
const CountDownTimer();
final String? title='';
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> {
#override
void initState() {
super.initState();
}// end of initstate
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Live Session'),
),
body: Text('Demo Text'),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_button(title: "Go", onPressed: () =>
Navigator.of(context ,rootNavigator: true).pushNamed('/liveSession1to1', arguments: {'room_found': 123 } )
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget _button({required String title, VoidCallback? onPressed}) {
return Expanded(
child: TextButton(
child: Text(
title,
style: const TextStyle(color: Colors.white),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: onPressed,
));
}
}
Error found:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/liveSession1to1", {room_found: 123}) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
For the "/" route, the "home" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
So how to solve the problem ?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
void main() {
locatorSetup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateRoute: generateRoute,
navigatorKey: _navService.navigatorKey,
// I don't know what your first screen is, so I'm assuming it's a Splash Screen
home: SplashScreen());
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.pushNamed(Routes.LiveSession1to1);
},
child: Text("Go to next page"),
),
));
}
}
class LiveSession1to1 extends StatefulWidget {
const LiveSession1to1({Key? key}) : super(key: key);
#override
State<LiveSession1to1> createState() => _LiveSession1to1State();
}
class _LiveSession1to1State extends State<LiveSession1to1> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.goBack();
},
child: Text("Go to previous page"),
),
));
}
}
GetIt locator = GetIt.instance;
void locatorSetup() {
locator
.registerLazySingleton<NavigationHandler>(() => NavigationHandlerImpl());
}
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.LiveSession1to1:
return _getPageRoute(view: LiveSession1to1(), routeName: settings.name);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
PageRoute _getPageRoute({String? routeName, Widget? view}) {
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => view!,
);
}
class Routes {
static const String LiveSession1to1 = "liveSession1to1";
}
abstract class NavigationHandler {
///Pushes `destinationRoute` route onto the stack
Future<dynamic>? pushNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` onto stack and removes stack items until
///`lastRoute` is hit
Future<dynamic>? pushNamedAndRemoveUntil(
String destinationRoute, String lastRoute,
{dynamic arg});
///Pushes `destinationRoute` onto stack with replacement
Future<dynamic>? pushReplacementNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` after popping current route off stack
Future<dynamic>? popAndPushNamed(String destinationRoute, {dynamic arg});
///Pops current route off stack
void goBack();
///Pops routes on stack until `destinationRoute` is hit
void popUntil(String destinationRoute);
///Exits app
void exitApp();
late GlobalKey<NavigatorState> navigatorKey;
}
/// Handles navigation
class NavigationHandlerImpl implements NavigationHandler {
#override
late GlobalKey<NavigatorState> navigatorKey;
/// Constructs a NavigationHandler instance
NavigationHandlerImpl({GlobalKey<NavigatorState>? navigatorKey}) {
this.navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
}
NavigatorState? get state => navigatorKey.currentState;
#override
void exitApp() {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
#override
void goBack() {
if (state != null) {
return state!.pop();
}
}
#override
Future? popAndPushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.popAndPushNamed(destinationRoute, arguments: arg);
}
}
#override
void popUntil(String destinationRoute) {
if (state != null) {
return state!.popUntil(ModalRoute.withName(destinationRoute));
}
}
#override
Future? pushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushNamed(destinationRoute, arguments: arg);
}
}
#override
Future? pushNamedAndRemoveUntil(String destinationRoute, String lastRoute,
{arg}) {
if (state != null) {
return state!.pushNamedAndRemoveUntil(
destinationRoute,
ModalRoute.withName(lastRoute),
arguments: arg,
);
}
}
#override
Future? pushReplacementNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushReplacementNamed(destinationRoute, arguments: arg);
}
}
}

How to remove package names from list that showing usage stats of all installed applications?

I simply made a flutter application that shows usage statistics of all installed application e.g if we spend two hours on WhatsApp my app show it,but the problem is : It also shows package names like
system ui, builder, launcher, com.package etc.
I am using this flutter package app_usage
Here is main.dart file
import 'package:flutter/material.dart';
import 'package:app_usage/app_usage.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<AppUsageInfo> _infos = [];
#override
void initState() {
super.initState();
}
void getUsageStats() async {
try {
DateTime endDate = new DateTime.now();
DateTime startDate = endDate.subtract(Duration(hours: 1));
List<AppUsageInfo> infoList =
await AppUsage.getAppUsage(startDate, endDate);
setState(() {
_infos = infoList;
});
for (var info in infoList) {
print(info.toString());
}
} on AppUsageException catch (exception) {
print(exception);
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('App Usage Example'),
backgroundColor: Colors.green,
),
body: ListView.builder(
itemCount: _infos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_infos[index].appName),
trailing: Text(_infos[index].usage.toString()));
}),
floatingActionButton: FloatingActionButton(
onPressed: getUsageStats, child: Icon(Icons.file_download)),
),
);
}
}
Here is my emulator output:

How to display an alert message if there is no data flutter

I am doing a query about the data from a mysql database and they are displayed in a list.
My problem is if there is no data in the database. I would like to display an alert message stating that or display a text to warn that there is no available data.
This is the code page that I use to display the data:
void main() {
runApp(Your()
);
}
class Your extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<Your> {
MyPreferences _myPreferences = MyPreferences();
String apiURL;
var id;
List<Flowerdata> _flowersDataForTheListView = List<Flowerdata>();
Future<List<Flowerdata>> fetchNotes() async {
id=_myPreferences.id;
final String url = 'https://=============/All.php?id=' + id.toString();
var response = await http.get(url);
var flowers = List<Flowerdata>();
if (response.statusCode == 200) {
var flowersJsonData = json.decode(response.body);
for (var flower in flowersJsonData) {
flowers.add(Flowerdata.fromJson(flower));
}
}
else{
}
return flowers;
}
#override
void initState() {
fetchNotes();
super.initState();
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return _listItem(index);
},
);
}
_listItem(index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text( _flowersDataForTheListView[index].Name,style: TextStyle(fontSize: 17)),
SizedBox(height: 5.0,),
],
),
),
],
),
],
);
}
}
PHP contact page:
<?php
include 'connt.php';
$id=$_GET['id'];
$sql = "SELECT * FROM user WHERE id=? ORDER BY id DESC";
$stmt = $con->prepare($sql);
$stmt->bind_param("s",$id);
$stmt->execute();
$result = $stmt->get_result();
//$result = $con->query($sql);
if ($result->num_rows >0) {
while($row[] = $result->fetch_assoc()) {
$item = $row;
$json = json_encode($item, JSON_NUMERIC_CHECK);
}
} else {
echo "No";
}
echo $json;
$con->close();
?>
How can I do this if someone knows please help me.
You don't seem to be using MaterialApp in your flutter code, but if you use it, Scaffold widget has a method called showSnackbar where you can display a message that pops up at the bottom of the page.
Here is a sample code for showSnackbar:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: MyList(),
);
}
}
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
#override
Widget build(BuildContext context) {
return Container();
}
#override
void initState() {
fetchNotes();
super.initState();
}
Future<void> fetchNotes() async {
// Here, you actually fetch the data from API
await Future.delayed(Duration(seconds: 3));
final flowers = [];
if (flowers.isEmpty) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Data is empty'),
),
);
}
}
}

Implementing Google Vision API with Flutter

I'm unsure of how to incorporate this into an existing flutter project and I haven't been able to find any useful guides or tips online. Im looking to implement a 2D only barcode scanner, and none of the existing flutter barcode scanners have that option.
I know ZXing also has the 2d only functionality in it so I could be swayed to use that if anyone can point me in the direction of how to implement them in flutter
Please check this url
https://pub.dartlang.org/packages/qrcode_reader
Here is implementation code
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qrcode_reader/QRCodeReader.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'QRCode Reader Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
final Map<String, dynamic> pluginParameters = {
};
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<String> _barcodeString;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('QRCode Reader Example'),
),
body: new Center(
child: new FutureBuilder<String>(
future: _barcodeString,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return new Text(snapshot.data != null ? snapshot.data : '');
})),
floatingActionButton: new FloatingActionButton(
onPressed: () {
setState(() {
_barcodeString = new QRCodeReader()
.setAutoFocusIntervalInMs(200)
.setForceAutoFocus(true)
.setTorchEnabled(true)
.setHandlePermissions(true)
.setExecuteAfterPermissionGranted(true)
.scan();
});
},
tooltip: 'Reader the QRCode',
child: new Icon(Icons.add_a_photo),
),
);
}
}
This can be done by using flutter barcode_scan dependency.
Future _openQRScanner() async {
try {
// Below code will open camera preview and return result after qr scan
String _content = await BarcodeScanner.scan();
setState(() => this._content = _content);
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.CameraAccessDenied) {
showSnackBar('Please grant camera permission!');
setState(() {
this._content = null;
});
} else {
showSnackBar('Error: $e');
setState(() {
this._content = null;
});
}
} on FormatException {
showSnackBar('User pressed "back" button before scanning');
setState(() {
this._content = null;
});
} catch (e) {
showSnackBar('Error: $e');
setState(() {
this._content = null;
});
}
}
Please find the repo.
If you want to take a look at Flutter you can find some good examples at our companie’s Github page. Also, you can check our company's page FlutterDevs.

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