I am trying to set the home page of the Flutter app asynchronously, but that is not working because the build method cannot have async properties.
class _MyAppState extends State<MyApp> {
// Widget homeWidget;
// #override
// void initState() async {
// super.initState();
// homeWidget = (await AuthUser.getCurrentUser() != null)
// ? NavBarPage()
// : OnBoardingWidget();
// }
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WizTkt',
theme: Theme.of(context).copyWith(
appBarTheme: Theme.of(context)
.appBarTheme
.copyWith(brightness: Brightness.dark),
primaryColor: Colors.blue),
home: (await AuthUser.getCurrentUser() != null)
? NavBarPage()
: OnBoardingWidget(),
);
}
}
As you can see in the code, I also tried to use initState to set the homepage widget but I cannot make initState an asynchronous function. I feel like there is a better way to choose your homepage in Flutter. What am I missing?
Do note that AuthUser.getCurrentUser() has to be an async function because I use the SharedPreferences library to obtain the login token stored in memory.
You can use FutureBuilder which allows you to build an Widget in a future time.
Here an example:
class OnBoardingWidget extends StatefulWidget {
const OnBoardingWidget({Key key}) : super(key: key);
#override
State<OnBoardingWidget> createState() => _OnBoardingWidgetState();
}
class _OnBoardingWidgetState extends State<OnBoardingWidget> {
final Future<String> _waiter = Future<String>.delayed(
const Duration(seconds: 2), () => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder<String>(
future: _waiter,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget wdgt;
if (snapshot.hasData) {
wdgt = Text('Result: ${snapshot.data}');
} else if (snapshot.hasError) {
wdgt = Text('Ops ops ops');
} else {
wdgt = Text('Not ready yet');
}
return Center(child: wdgt);
},
),
);
}
}
Related
My problem is that, before showing the screen. It should load the necessary data while displaying a splashscreen.
It works fine, until it goes to the create provider, the data which has been loaded into the list is getting cleared due to the list getting recreated. I wonder how can i tackle this? How should i load the data (json) file into the list instead.
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<void> loadJson;
#override
void initState() {
loadJson = QuestionProvider().loadJsonFiles();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: loadJson,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const MaterialApp(home: Splash());
} else {
return MultiProvider(
providers: [
ChangeNotifierProvider<QuestionProvider>(create: (_) => QuestionProvider()),
],
child: MaterialApp(
title: "MyApp",
theme: ThemeData(
primarySwatch: Colors.amber,
),
home: const Home(),
)
);
}
},
);
}
}
class QuestionProvider with ChangeNotifier {
final List<QuestionModel> questionList = <QuestionModel>[];
Future<void> loadJsonFiles() async {
final String response = await rootBundle.loadString("assets/questions.json");
final Map<String, dynamic> data = await jsonDecode(response);
for (int i = 0; i < data.length; i++) {
questionList.add(QuestionModel.fromJson(data[i]));
}
}
}
Why not invert the future builder and the providers?
Widget build(BuildContext context) {
return MultiProvider(
[...],
child: Builder(
builder: (context) =>
FutureBuilder(
future: Provider.of<QuestionProvider>().loadJsonFiles,
child: [...]
),
),
);
}
There may or may not be some disadvantages to this method, specifically, the value of the future is no longer cached, if this worries you, I recommend you cache the value within the QuestionProvider class itself.
I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}
I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.
Below code always show OnboardingScreen a little time (maybe miliseconds), after that display MyHomePage. I am sure that you all understand what i try to do. I am using FutureBuilder to check getString method has data. Whats my fault ? Or any other best way for this ?
saveString() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('firstOpen', '1');
}
getString() method always return string.
getString() async {
final prefs = await SharedPreferences.getInstance();
String txt = prefs.getString('firstOpen');
return txt;
}
main.dart
home: new FutureBuilder(
future: getString(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
})
Usually I'm using another route, rather than FutureBuilder. Because futurebuilder every hot reload will reset the futureBuilder.
There always will be some delay before the data loads, so you need to show something before the data will load.
Snapshot.hasData is showing only the return data of the resolved future.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
const isOnboardingFinished = 'isOnboardingFinished';
class _SplashScreenState extends State<SplashScreen> {
Timer timer;
bool isLoading = true;
#override
void initState() {
_checkIfFirstOpen();
super.initState();
}
Future<void> _checkIfFirstOpen() async {
final prefs = await SharedPreferences.getInstance();
var hasOpened = prefs.getBool(isOnboardingFinished) ?? false;
if (hasOpened) {
_changePage();
} else {
setState(() {
isLoading = false;
});
}
}
_changePage() {
Navigator.of(context).pushReplacement(
// this is route builder without any animation
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading ? Container() : OnBoarding();
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(child: Text('homePage'));
}
}
class OnBoarding extends StatelessWidget {
Future<void> handleClose(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(isOnboardingFinished, true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
onPressed: () => handleClose(context),
child: Text('finish on bording and never show again'),
),
),
);
}
}
From the FutureBuilder class documentation:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
So you need to create a new Stateful widget to store this Future's as a State. With this state you can check which page to show. As suggested, you can start the future in the initState method:
class FirstPage extends StatefulWidget {
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final Future<String> storedFuture;
#override
void initState() {
super.initState();
storedFuture = getString();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: storedFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
});
}
}
So in your home property you can call it FirstPage:
home: FirstPage(),
Your mistake was calling getString() from within the build method, which would restart the async call everytime the screen gets rebuilt.
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();
}
}
}