How to use Future Builders Effectively


What are Futures and FutureBuilders

Simply a Future is a data that is yet to come and a FutureBuilder is a Widget that can be used to show that data after they received. If you need a detailed description on how to use FutureBuilders you can refer to the following article.

How to get data from async function to show on a widget

This is a detailed article about different ways of using FutureBuilders in your app

www.fcodelabs.com

App Structure

We will use Firebase for this case. This view will get a DocumentReference as a parameter. Then it will fetch data from the Firestore and show it in a Scaffold. For that reason, this view must be a StatefulWidget. Here is the code for that Widget. We will call it the SecondPage.

1
2
3
4
5
6
7
8
9
10
11
class SecondPage extends StatefulWidget {
final DocumentReference ref;

const SecondPage({
Key key,
@required this.ref,
}) : super(key: key);

@override
_SecondPageState createState() => _SecondPageState();
}

App State

I think that the above part was straight forward. The state of that Widget will be trickier if you haven’t worked with states that will depend on the values of its Widget.

1
2
3
4
5
6
7
8
9
10
11
class _SecondPageState extends State<SecondPage> {

// This variable will be used to store
// Future data for this State
Future<Map<String, dynamic>> data;

@override
Widget build(BuildContext context) {
...
}
}

Now we need to figure out a way to get DocumentReference in the Widget and map it into this data variable. You can achieve that using the following method.

1
2
3
4
5
6
7
8
9
@override
void initState() {
super.initState();
data = mapData();
}

Future<Map<String, dynamic>> mapData() async {
return (await widget.ref.get()).data;
}

What if the page rebuilds

This will get the data from DocumentReference and store it in the data variable as a Future. That code is written inside initState block, therefore it will run once when this SecondPage widget builds for the first time in the Widget tree.

But what if this SecondPage widget was called a second time with a different DocumentReference. Then the initState method will never run and the data variable will never be updated. Then although you called the widget with the correct value, it will never be shown in the UI. Trust me, it will be very confusing if you didn’t know what was happening. To overcome this problem, we can use the following method.

1
2
3
4
5
6
7
@override
void didUpdateWidget(SecondPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.ref.path != widget.ref.path) {
data = mapData();
}
}

This method is called every time the Widget associated with the State (In our case SecondPage widget) rebuilds. Then it will check whether the DocumentReference passed as an argument has changed and update the data variable accordingly. After setting the value to the data variable, there is no need to call setState because rebuilding is guaranteed after this function call. After all, as I said earlier, the app will go through this routine when the Widget associated with this State is rebuilding. So, calling the build method is guaranteed.

Showing the data

Then what you need is to use a FutureBuilder to present the data when they arrived.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<Map<String, dynamic>>(
future: data,
builder: (context, snapshot) {
if (snapshot.hasData) {
final name = snapshot.data['name'];
final email = snapshot.data['email'];
return Column(
children: <Widget>[
Text("Name: $name"),
Text("Email: $email"),
],
);
}
return CircularProgressIndicator();
}
),
);
}

This will show a CircularProgressIndicator until the data comes. After they were received, it will be shown in the Scaffold.

What is the need of this data variable

You will ask, why use this data variable? You can just remove it and make this a StatelessWidget and in the FutureBuilder you can call mapData() method. Then the FutureBuilder will fetch data from that method, and the app will work as expected. Right?

Yes, it will fetch and show the data. However, there is a problem. mapData() method will be called every time the widget rebuilds. Then in every call, it will fetch data from Firestore and will create a Future. That means, every time this widget rebuilds, it will show a CircularProgressIndicator first and then will show the data from the database. You don’t need to fetch the data unless the DocumentReference has changed. Therefore, it is not a good way to use a Future inside a build method.

As a summary, I will put the whole code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class SecondPage extends StatefulWidget {
final DocumentReference ref;

const SecondPage({
Key key,
@required this.ref,
}) : super(key: key);

@override
_SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
Future<Map<String, dynamic>> data;

@override
void initState() {
super.initState();
data = mapData();
}

@override
void didUpdateWidget(SecondPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.ref.path != widget.ref.path) {
data = mapData();
}
}

Future<Map<String, dynamic>> mapData() async {
return (await widget.ref.get()).data;
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<Map<String, dynamic>>(
future: data,
builder: (context, snapshot) {
if (snapshot.hasData) {
final name = snapshot.data['name'];
final email = snapshot.data['email'];
return Column(
children: <Widget>[
Text("Name: $name"),
Text("Email: $email"),
],
);
}
return CircularProgressIndicator();
}
),
);
}
}