how to perform a single execution of a future? - flutter

What I am trying to do is that by starting the app, make a request to a server and all information is saved in the database, for that I use a FutureBuilder that does the whole process, once finished it starts the application as normal.
The problem is that the application executes my future synchronization more than twice, causing errors with the insert to database.
the following code is a basic example of what i am trying to do and the result i am getting.
main.dart
void main() => runApp(MyMaterialApp());
class MyMaterialApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Running MyMaterialApp');
return MaterialApp(
home: SplashLoad(),
);
}
}
class SplashLoad extends StatefulWidget {
#override
_SplashLoadState createState() => _SplashLoadState();
}
class _SplashLoadState extends State<SplashLoad> {
final apiSimulation = new ApiSimulation();
#override
Widget build(BuildContext context) {
print('SplashScreen');
return Scaffold(
body: Container(
child: Center(
child: FutureBuilder(
future: apiSimulation.sincronizacion(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
return Text('load finished data:${snapshot.data}');
}
return CircularProgressIndicator();
},
),
),
),
);
}
}
ApiSimulation
class ApiSimulation {
int number = 0;
Future<int> synchronization()async{
print('INIT SYNCHRONIZATION');
await Future.delayed(Duration(seconds: 2));
final data = await _getData();
return data;
}
Future<int> _getData() async{
number++;
print('running getData$number');
await Future.delayed(Duration(seconds: 2));
return number;
}
}
console result
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): running getData1
I/flutter (32404): running getData2
Sometimes reaching 4 in the value of the number

This is what the document says
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateWidget, 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.
class _SplashLoadState extends State<SplashLoad> {
late final Future simFuture;
#override
void initState() {
super.initState();
simFuture= ApiSimulation().sincronizacion(); //initiate your future here
}
Within your future builder use
FutureBuilder(
future: simFuture,
builder: (ctx,snap){..},
)

Related

Flutter - FutureBuilder fires twice on hot reload

In my flutter project when I start the project in the simulator everything works fine and the future builder only fires once, but when I do hot reload the FutureBuilder fires twice which causes an error any idea how to fix this?
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
I solved the issue. I put the Future function in the initState and then used the variable in the FutureBuilder. I'm not sure why it works this way, but here's the code:
var futures;
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
futures = frameFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futures,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
The solution as you already figured out is to move the future loading process to the initState of a StatefulWidget, but I'll explain the why it happens:
You were calling your future inside your build method like this:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
The issue is that Flutter calls the build method each time it renders the Widget, whenever a dependency changes(InheritedWidget, setState) or Flutter decides to rebuild it. So each time you redraw your UI frameFuture() gets called, this makes your build method to have side effects (this async call) which it should not, and is encouraged for widgets not to have side effects.
By moving the async computation to the initState you're only calling it once and then accessing the cached variable futures from your state.
As a plus here is an excerpt of the docs of the FutureBuilder class
"The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, 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."
Hope this makes clear the Why of the solution.
This can happen even when the Future is called from initState. The prior solution I was using felt ugly.
The cleanest solution is to use AsyncMemoizer which effectively just checks if a function is run before
import 'package:async/async.dart';
class SampleWid extends StatefulWidget {
const SampleWid({Key? key}) : super(key: key);
final AsyncMemoizer asyncResults = AsyncMemoizer();
#override
_SampleWidState createState() => _SampleWidState();
}
class _SampleWidState extends State<SampleWid> {
#override
void initState() {
super.initState();
_getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.asyncResults.future,
builder: (context, snapshot) {
if (!snapshot.hasData) return yourLoadingAnimation();
// ... Do things with the data!
});
}
// The async and await here aren't necessary.
_getData() async () {
await widget.asyncResults.runOnce(() => yourApiCall());
}
}
Surprisingly, there's no .reset() method. It seems like the best way to forcibly rerun it is to override it with a new AsyncMemoizer(). You could do that easily like this
_getData() async ({bool reload = false}) {
if (reload) widget.asyncResults = AsyncMemoizer();
await widget.asyncResults.runOnce(() => yourApiCall());
}

Flutter: Stateful Widget does not update

Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});

Flutter Bloc is not rebuilding state

I have implement a this simple bloc with the flutter_bloc package:
class MainBloc extends Bloc<MainEvent, MainState> {
#override
MainState get initialState => Init();
#override
Stream<MainState> mapEventToState(MainEvent event) async* {
if (event is Event) {
yield* _mapEventToState();
}
}
Stream<MainState> _mapEventToState() async* {
final loadState = Load();
print("Yield state: $loadState");
yield loadState;
await sleep(Duration(seconds: 1));
final initState = Init();
print("Yield state: $initState");
yield initState;
}
}
with this event:
abstract class MainEvent {}
class Event extends MainEvent {}
and this state:
abstract class MainState {}
class Load extends MainState {}
class Init extends MainState {}
My UI looks like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: BlocProvider(
create: (context) => MainBloc(),
child: Scaffold(
appBar: AppBar(),
body: BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
print("Build state: $state");
if (state is Init) {
return MaterialButton(
onPressed: () => BlocProvider.of<MainBloc>(context).add(Event()),
child: Text("Press"),
);
} else {
return Text("Loading");
}
},
),
),
),
);
}
}
Unfortunately if I add my Event with the MaterialButton, the Load() state gets ignored. The UI doesn't rebuild the Load state. The output is the following:
I/flutter ( 1955): Yield state: Instance of 'Load'
I/flutter ( 1955): Yield state: Instance of 'Init'
I/flutter ( 1955): Build state: Instance of 'Init'
It's because you're using await sleep(Duration(seconds: 1)); (the await statement is of no use here, it is a synchronous function).
The sleep function pauses the execution of the main thread, meaning it will also block the rebuilding of the Main page of your app, and so it doesn't get the chance to rebuild because of the new Load state. Once freezing the app is over, it immediately gets a new state, so that's why you will never see the loading page.
Use await Future.delayed(Duration(seconds: 1)); instead, which pauses the execution of the rest of the _mapEventToState function but doesn't block the main thread and so your MainPage gets a rebuild.

flutter async call doesn't run asynchronously in initState()

I'm calling async function in initState(),
but the system actually waits the result of async function.
Could anyone tell me why?
Here's my code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<int> _f;
#override
void initState() {
super.initState();
Stopwatch stopwatch = new Stopwatch()..start();
print('executed in ${stopwatch.elapsed}');
_f = getFuture();
print('executed in ${stopwatch.elapsed}');
}
Future<int> getFuture() async {
int i = 0;
while(i < 1000000000) {
i++;
}
return i;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("test")),
body: FutureBuilder<int>(
future: _f,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Center(child: Text("snapshot: ${snapshot.data}"));
break;
default:
return Center(child: CircularProgressIndicator());
}
}
),
);
}
}
And here's the output:
I/flutter (22058): executed in 0:00:00.000384
I/flutter (22058): executed in 0:00:04.536278
A Future function is made to return a Future object asynchronously. Which means executing a statement of code once that will take a much longer time, for example getting json object from an api, than the usual time required for a normal statement, for example a declaration of a variable.
However, you are executing a normal statement, that requires milliseconds to run, a 1000000000 times. That will result in a longer execution time which was built up by the milliseconds of each execution.
Even though it is a Future function, it is not returning a Future object or variable, so it is not asynchronous.

FutureBuilder runs twice

I have problems with FutureBuilder starting twice.
First it fetch the data correctly, returning my StartScreen, then after few seconds, the StartScreen rebuilds and I noticed that the FutureBuilder fires again.
Here is my code and it's pretty simple, so I wonder what may the problem be?!?
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
FirebaseUser user;
#override
void initState() {
// TODO: implement initState
super.initState();
getNewestlocation();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'APP',
theme: buildTheme(),
home: FutureBuilder<FirebaseUser>(
future: Provider.of<AuthService>(context).getUser(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print('error');
return Text(snapshot.error.toString());
}
user = snapshot.data;
print('user here $user');
return snapshot.hasData ? StartScreen(user) : LoginScreen();
} else {
return LoadingCircle();
}
},
),
);
}
}
Can anyone help me with this, please?
The future is firing again because you're creating it in the build method at the same time as the FutureBuilder.
From the FutureBuilder docs:
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 to prevent it from firing you'd have to do something like this:
class _MyAppState extends State<MyApp> {
Future<String> _myString;
#override
void initState() {
super.initState();
_myString = _fetchString();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _myString,
builder: (context, snapshot) {
// build page stuff...
},
),
);
}
}
Future<String> _fetchString() async {
print('running future function');
await Future.delayed(Duration(seconds: 3));
return 'potatoes';
}
Note, to access a provider in initState() you have to set listen to false, as detailed in this answer.
I think you have some things bad in your code, maybe that's not the problem but is good to correct that:
first: It is not recommendable to do that job in your main file, you should have something like a Splash page to handle that.
second: You should use blocs and not write your logic code on the same place at the view(UI)
If you're using android studio, try if running from the terminal fix the issue. The run button attached the debug service, which then force the entire app to be rebuilt