FutureBuilder doesn't wait until future completes - flutter

In my app I want to initialize something before my widgets will be created. I need to do it exactly in App class and trying to use FutureBuilder for this purpose. But _AppBlocProvider's build method is called before initInjectionContainer(), for example. My repository is not initialised yet in injectionContainer, but Blocs in provider are trying to access it's instance. What's wrong with this code?
I've also tried this:
void main() {
runApp(App());
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
Future<bool>? _myFuture;
Future<bool> _init() async {
...
await initInjectionContainer();
await sl<AudioManager>().preloadFiles();
return false;
}
...
#override
void initState() {
super.initState();
_myFuture = _init();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _myFuture,
builder: (context, _) {
return _BlocProvider(
child: Builder(
builder: (context) => MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MainMenu(),
),
),
);
},
);
}
}
doesn't work.

FutureBuilder doesn't just automatically block or show a loading screen or whatever. It builds once on initialization, and then again once the future completes. That second parameter in the builder that you anonymized is crucial to properly handling the state of the future and building accordingly.
FutureBuilder(
future: _someFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// Future not done, return a temporary loading widget
return CircularProgressIndicator();
}
// Future is done, handle it properly
return ...
},
),
That being said, if there is stuff that your entire app needs that you need to initialize, you can call it from main before you call runApp so that they become a part of the runtime loading process rather than forcing a widget to deal with it:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initInjectionContainer();
await sl<AudioManager>().preloadFiles();
runApp(App());
}
Now with that being said, if these processes can take a while, it's better to handle them with a widget so that you can display a loading state to the user so they know the app didn't just freeze on start-up.

Related

StreamProvider: Error: Could not find the correct Provider<User> above this App Widget

I'm using StreamProvider from the provider package for auth functionality in my flutter-firebase app, just like it is explained in this tutorial https://www.youtube.com/watch?v=j_SJ7XmT2MM&list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC&index=9.
When trying to run my app, I get an error message, with a suggestion how to do it correctly, but my code IS written in the way that is suggested.
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App());
}
class FirebaseWrapper extends StatelessWidget {
// Create the initialization Future outside of build():
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
// final Future<void> _initSharedPrefs = SharedPrefsHelper().initSharedPrefsInstance();
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return FutureBuilder(
// from: https://firebase.flutter.dev/docs/overview/#initializing-flutterfire
future: _initialization,
// future: Future.wait([_initialization, _initSharedPrefs]),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorPage(); //TODO better error pages
if (snapshot.connectionState == ConnectionState.done) return FirebaseAuthWrapper();
return Loading(); //waiting
},
);
}
}
class FirebaseAuthWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: Auth().userStream,
initialData: null,
child: App(),
);
}
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print('yeet');
return MaterialApp(
key: UniqueKey(),
title: 'Wanderapp',
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: (user == null) ? '/signIn' : '/',
routes: (user == null)
? {
'/signIn': (context) => SignIn(),
'/register': (context) => Register(),
// '/forgotPassword': (context) => ForgotPassword(),
}
: {
'/': (context) => Home(),
//...
},
);
}
}
the error message:
Error: Could not find the correct Provider<User> above this App Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that App is under your MultiProvider/Provider<User>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
```
consider using `builder` like so:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
```
I'm user the same "User" class from Firebase for StreamProvider and Provider.of, the hierarchy/scope also seems to be correct in my code, but it doesn't work.
Does anyone know what my mistake is? Thank you very much.
In this link about runApp it says:
Calling runApp again will detach the previous root widget from the
screen and attach the given widget in its place.
So, you just need to remove the second runApp, as App is being called anyway from the StreamProvider: child: App(),.
Solution:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App()); //*** Remove this line ***///
}

get a response from another API while FutureBuilder works

late Future<Kategori> _futureArticles;
late Future<Article> _futureSummary;
and the API's
#override
void initState() {
_futureArticles = _newsService.getArticlesByCategory(widget.id);
_futureSummary = _newsService.getArticleById(widget.id);
super.initState();
}
and FutureBuilder
child: FutureBuilder<Kategori>(
future: _futureArticles,
builder: (BuildContext context, AsyncSnapshot<Kategori> snapshot) {
if (snapshot.hasData) {
final articles = snapshot.data?.data;
now with FutureArticles and with this structure everything works but I need an another json value from _futureSummary. Both API's has got same ID values, so I can get the json.summary value from second API. But how? I tried to use future.wait but it did not work.
Meanwhile I am using second APi on different page to get all informations of a spesific news.
What is the correct approach?
Not sure what you are trying to achieve. Do you want your Future builder to rebuild only when both futures completed? If so - try to combine both futures. Future.wait will wait for all Future objects you pass to complete, and return List of results:
Let me update my answer with the working demo - you can test it in DartPad. Note that the first Future will complete after 1 second (and write the log in the console), but the FutureBuilder will wait until the second Future is completed, and only then show the values from both.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late Future<String> _futureArticles;
late Future<int> _futureSummary;
#override
void initState() {
_futureArticles = Future.delayed(const Duration(seconds:1), () {print("First is done"); return "First is done";});
_futureSummary = Future.delayed(const Duration(seconds:5), () => 10);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<dynamic>>(
future: Future.wait([_futureArticles, _futureSummary]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
final articles = snapshot.data![0] as String;
final summary= snapshot.data![1] as int;
return Column(children:[
Text(articles),
Text('$summary')
]);
} else {
return const CircularProgressIndicator();
}
});
}
}

flutter-web - Avoid initialRoute from initiating when the app launched with a different route via the browser's address bar?

New to Flutter.
I'm making an app that has a splash screen that initially shows up when the user opens the app. After 3 seconds, the app will show the login or the dashboard screen, depending on the authentication state.
Here's my code.
main.dart
void main() {
runApp(myApp);
}
MaterialApp myApp = MaterialApp(
initialRoute: "/",
routes: {
"/": (context) => SplashScreen(),
"/signin": (context) => SignInScreen(),
"/notes": (context) => NotesScreen(),
},
);
splash_screen.dart
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_goToNextScreen();
}
void _goToNextScreen() {
Future.delayed(
Duration(seconds:3),
() async {
AuthState authState = await Auth.getAuthState();
String route = authState == AuthState.SIGNED_IN ? "/notes" : "/signin";
Navigator.pushReplacementNamed(context, route);
}
);
}
// build() override goes here...
}
I've been debugging the app with a web-server. When the app launches with the url localhost:8000/, everything seems fine. However, if the app started with the url localhost:8000/notes, the splash screen, I think, still gets initiated. What happens is the app will show the notes screen, then after 3 seconds, the app will open another notes screen.
Any ideas?
Because first render always started at root '/', it's preferable to use your own path for splash screen, like
initialRoute: '/splash'.
To hide this path in the address bar, replace routes map with route generator:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (RouteSettings settings) {
// print current route for clarity.
print('>>> ${settings.name} <<<');
switch (settings.name) {
case '/splash':
return MaterialPageRoute(
builder: (context) => SplashScreen(),
// settings omitted to hide route name
);
case '/signin':
return MaterialPageRoute(
builder: (context) => SignInScreen(),
settings: settings,
);
case '/notes':
return MaterialPageRoute(
builder: (context) => NotesScreen(),
settings: settings,
);
case '/':
// don't generate route on start-up
return null;
default:
return MaterialPageRoute(
builder: (context) => FallbackScreen(),
);
}
},
initialRoute: '/splash',
);
}
}
See since the main logic is we cannot have await in the init state so the page will build irrespective of the any logic you provide. I have a solution to this, there may be some advance or other good solutions too, so this is what I would use.
I would use a concept of future builder. What it will do is wait for my server and then build the whole app.
So process is
In your main.dart
use
Future<void> main() async {
try {
WidgetsFlutterBinding.ensureInitialized();
//await for my server code and according to the variable I get I will take action
//I would have a global parameter lets say int InternetOff
await checkServer();
runApp(MyApp());
} catch (error) {
print(error);
print('Locator setup has failed');
//I can handle the error here
}
}
Now MyApp stateless Widget that will help us choose our path
class MyApp extends Stateless Widget{
Widget build(BuildContext context) {
//Using this FutureBuilder
return FutureBuilder<String>(
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// AsyncSnapshot<Your object type>
// Now if InternetOff is equal to one I would make it go to home
if(InternetOff==1) return MaterialApp(
theme: ThemeData.light(),
home: CheckInternet(),
debugShowCheckedModeBanner: false,
);
//else go to Home similarly with these if and else you can add more conditions
else {
return MaterialApp(
theme: ThemeData.dark(),
home: UserHome(),
debugShowCheckedModeBanner: false,
);
}
}
}
},
);
}
}
First of all, flutter-web like any other Single Page Application supports hash based routing. As a result if you want to access
localhost:8000/notes
you have to access it as
localhost:8000/#/notes
Cleaner way to handle auth state
Call getAuthState function before runApp() to make sure that the auth state is set before app is initialized. And pass authState to SplashScreen widget as parameter.
void main() {
WidgetsFlutterBinding.ensureInitialized();
AuthState authState = await Auth.getAuthState();
runApp(MaterialApp myApp = MaterialApp(
initialRoute: "/",
routes: {
"/": (context) => SplashScreen(authState: authState),
"/signin": (context) => SignInScreen(),
"/notes": (context) => NotesScreen(),
},
));
}
splash_screen.dart
class SplashScreen extends StatefulWidget {
final AuthState authState;
SplashScreen({Key key, this.authState}) : super(key: key);
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_goToNextScreen();
}
void _goToNextScreen() {
Future.delayed(
Duration(seconds:3),
() async {
String route = widget.authState == AuthState.SIGNED_IN ? "/notes" : "/signin";
Navigator.pushReplacementNamed(context, route);
}
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
And if you want even more cleaner way to handle auth state, you have to use state management solution like Provider.

Fetch state-object from database

I want to get my state from database upon startup. I use provider with ChangeNotivierProvider at my first widget. Code included for clarification.
This is my main method, it has the widget that provides the state i plan to use in the app:
void main() => runApp(
ChangeNotifierProvider(
child: MyApp(),
builder: (context) => StateDao.get(),
),
}
My DAO just returns the state from database (i use sembast, but could just as easily be sqflite or Firebase)
Future<State> get() async {
// Database logic
}
State is just an object extending ChangeNotifier
class State extends ChangeNotifier {
// Getters, setters, changeNotifiers etc.
}
This will not work as i cannot call async methods in the builder-method of ChangeNotifierProvider. How, when and where should this be initialized? As i understand it, async calls should not be done in any build methods. I tried overriding didChangeDependencies that provided context-access but i could not get past the async-call in builder method limitation.
You can initialize the value in initState. For example, you can store Future object and use FutureBuilder to build widget when a value is resolved:
Future<State> futureState;
#override
void initState() {
// Calling async method and storing Future object
futureState = StateDao.get();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<State>(
future: futureState,
builder: (context, snapshot) {
// No data yet, showing loader
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return ChangeNotifierProvider(
child: MyApp(),
builder: (context) => snapshot.data,
);
},
);
}
Another similar approach is to use StreamController and StreamBuilder:
final StreamController<State> stateStream = StreamController<State>();
#override
void initState() {
// Calling async method and setting callback to add data to the stream
StateDao.get().then((v) => stateStream.add(v));
super.initState();
}
#override
void dispose() {
// Closing the sink
stateStream.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<State>(
stream: stateStream.stream,
builder: (context, snapshot) {
// No data yet, showing loader
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return ChangeNotifierProvider(
child: MyApp(),
builder: (context) => snapshot.data,
);
},
);
}
If you need more complex architecture, you might want to look at BLoC, which is very popular architecture in Flutter community.

Use progress/activity indicator waiting for await to finish

I have an async function. I'm using it correctly. Here's what it looks like:
Future<void> getData() async
{
// get data from DB
}
It works. I call it from the
void initState ()
{
super.initState();
getData();
}
I would like to make sure that there is a progress indicator running until the function has completed whatever it was doing.
Can someone help me with that? I have no idea where to begin.
Parts of this function get completed at whatever time/speed it takes it each command to finish, but I need to wait for the entire getData() function to finish completely before my app starts. How can I do that?
While ejabu's solution works, it is far from perfect and calling asynchronous functions without await, especially in initState is a big nono.
Just like everything in Flutter, if you have a problem, there's a widget for that.
The ideal widget for rendering a page after a Future completed is FutureBuilder (docs).
FutureBuilder is a dead useful widget. You can use it for any widget that has to have a Future complete before it is built.
For example, if an app requires the user to sign in to Google, initialize a few APIs, then show the homepage, then you can use a FutureBuilder to show a progress indicator when the Future is running, an error page when an error occurs, or the homepage if all goes well and the Future completes without errors.
Here's a quick example from the build method of a widget:
Widget build() {
return FutureBuilder(
future: futureFunction(),
builder: (context, snapshot) {
// Future done with no errors
if (snapshot.connectionState == ConnectionState.done &&
!snapshot.hasError) {
return HomePage();
}
// Future with some errors
else if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasError) {
return Text("The error ${snapshot.error} occured");
// Future not done yet
else {
return Scaffold(
body: Center(
child: Container(
width: MediaQuery.of(context).size.width / 1.5,
height: MediaQuery.of(context).size.width / 1.5,
child: CircularProgressIndicator(strokeWidth: 10),
),
),
);
}
}
)
}
Can I quickly rant about how terrible the editor in Stack Overflow is. It caused me to accidently post my answer before I was done :(
Have a stateful widget before Home Screen
pushReplace Screen after fetch database completed
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: LoadingScreen(),
routes: {
'/homePage': (context){
return HomePage();
},
},
);
}
}
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
Future<void> getData() async {
// get data from DB
}
void navigationPage() async {
await getData();
Navigator.of(context).pushReplacementNamed('/homePage');
}
#override
void initState() {
super.initState();
navigationPage();
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
}