Widget that builds itself based on the latest snapshot of interaction with a Future
.
When you should use it? Lets say you want to fetch data from backend on launch of page and show loader till data comes.
In FutureBuilder waits for the result, and as soon as the future produces the result it calls the builder function where we build the widget.
The build strategy currently used by this builder. The builder method receives two argument. The first on is context
, and the second one is snapshot
. You can use snapshot
argument to check if the future is yet to produce the result and based on this you can return different widget.
An Future object that will produce some result. The widget will use this object and will wait for result to produce. As soon as it produce the result, the builder
method will be called to draw the UI.
Future<http.Response> fetchAlbum() { return http.get('https://jsonplaceholder.typicode.com/albums/1'); } Future<http.Response> futureObject = fetchAlbum(); FutureBuilder( future: futureObject, builder(context, snapshot){ if(snapshot.hasData){ // Return a Widget }else if(snapshot.hasError){ // Return an widget that shows error }else{ // Return a loading indicator widget } } ),
Note that snapshot.hasData
only returns true
when the snapshot contains a non-null data value. This is why the fetchAlbum function should throw an exception even in the case of a “404 Not Found” server response. If fetchAlbum returns null
then the spinner displays indefinitely. So this is important that the Future should never resolves to null
value.
Here is an updated version of the above example:
Future<Map<String, dynamic>> fetchAlbum() async { final response = await http.get('https://jsonplaceholder.typicode.com/albums/1'); if (response.statusCode == 200) { // If the server did return a 200 OK response, // then parse the JSON. return json.decode(response.body); } else { // If the server did not return a 200 OK response, // then throw an exception. throw Exception('Failed to load album'); } } Future<Map<String, dynamic>> futureObject = fetchAlbum(); FutureBuilder( future: futureObject, builder(context, snapshot){ if(snapshot.hasData){ // Return a Widget // Use snapshot.data to get the data }else if(snapshot.hasError){ // Return an widget that shows error }else{ // Return a loading indicator widget } } ),
The argument snapshot
has four properties:
hasData
: Becomes true
when the Future resolves to the value.hasError
: Becomes true
when the Future encounters an error. Like the future contains an exception.data
: You can access the future data through this property. So your widget may use snapshot.data
: User to obtain the data from the snapshot.connectionState
: It contains ConnectionState
enum value. It changes during the lifecycle of the provided future.Here is an complete modified version of the above example.
import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; Future<Album> fetchAlbum() async { final response = await http.get('https://jsonplaceholder.typicode.com/albums/1'); if (response.statusCode == 200) { // If the server did return a 200 OK response, // then parse the JSON. return Album.fromJson(json.decode(response.body)); } else { // If the server did not return a 200 OK response, // then throw an exception. throw Exception('Failed to load album'); } } class Album { final int userId; final int id; final String title; Album({this.userId, this.id, this.title}); factory Album.fromJson(Map<String, dynamic> json) { return Album( userId: json['userId'], id: json['id'], title: json['title'], ); } } void main() => runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key key}) : super(key: key); @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { Future<Album> futureAlbum; @override void initState() { super.initState(); futureAlbum = fetchAlbum(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Fetch Data Example', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Fetch Data Example'), ), body: Center( child: FutureBuilder<Album>( future: futureAlbum, builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data.title); } else if (snapshot.hasError) { return Text("${snapshot.error}"); } // By default, show a loading spinner. return CircularProgressIndicator(); }, ), ), ), ); } }
The snapshot
argument of the builder
callback contains a ConnectionState
enum value which changes during the life cycle of the provided future object. Here is an example:
FutureBuilder( future: future, builder: (context, snapshot){ switch (snapshot.connectionState) { case ConnectionState.none: print("Connection is still yet to make"); case ConnectionState.waiting: print("Connection is waiting for Future to be completed"); print("Show Loading Indicator"); case ConnectionState.active: print("Connection is Active"); case ConnectionState.done: print("Future has completed"); } } );
Within the builder
callback you can use both either use hasData
or connectionState
to determine what to show to the user. But there is a difference.
Consider this example where you have initialized the FutureBuilder
widget with a future object. In your code you show a spinner until the content loads and when the content loads you show the content. It works fine. Now you dynamically change the future object. What would you expect that the builder function should receive a fresh snapshot so that you will show the spinner again until the new future object completes and then show the content. But no, when you dynamically change the future object the snapshot will still contain the old value, the value of the previous future object which is completed and contains data. When the new future is completed, then you will receive new snapshot based on the new provided future and it will contain the value of new future. So the problem is from the point you change the future object dynamically with a new future object and to the point the new future completes, your snapshot is still the old snapshot that was received when the previous future completed. So you won't be able to show a spinner after you change the future dynamically if you use snapshot.hasData
to determine if the snapshot has data or not.
The solution is to use connectionState
instead of hasData
. Even though after changing the future object the snapshot will contain previous data, the connectionState
will be new.
That's why it is recommended to use connectionState
whenever possible.