FutureBuilder

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.

Properties

builder

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.

future

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.

Example

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
    }
  }
),

snpashot

The argument snapshot has four properties:

Example

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();
            },
          ),
        ),
      ),
    );
  }
}

ConnectionState

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");
    }
  }
);

connectionSate vs hasData

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.