The pattern of retrieving some data asynchronously and updating the user interface based on that data is quite common. So common in fact that in Flutter, there is a widget that helps you remove some of the boilerplate code you need to build the UI based on Futures: it’s the FutureBuilder
widget.
You can use a FutureBuilder
to integrate Futures within a widget tree that automatically updates its content when the Future updates. As a FutureBuilder
builds itself based on the status of a Future, you can skip the setState instruction, and Flutter will only rebuild the part of the user interface that needs updating.
FutureBuilder
implements reactive programming, as it will take care of updating the user interface as soon as data is retrieved, and this is probably the main reason why you should use it in your code: it’s an easy way for the UI to react to data in a Future.
FutureBuilder
requires a future property, containing the Future
object whose content you want to show, and a builder. In the builder, you actually build the user interface, but you can also check the status of the data: in particular, you can leverage the connectionState
of your data so you know exactly when the Future
has returned its data.
Step-by-Step Guide to Help You Use FutureBuilder
Effectively
Step 1: Create a Flutter Project
First, create a new Flutter project if you don’t have one, Open the project in your preferred IDE (like Visual Studio Code or Android Studio)
Step 2: Add a Future Method
In your Dart file, add a method that returns a Future. This could be a network request, a database query, or any asynchronous operation. For example, let’s simulate a network request that fetches some data:
Future<String> fetchData() async {
// Simulate a network delay
await Future.delayed(const Duration(seconds: 2));
return 'Hello, Flutter!';
}
Step 4: Create FutureBuilder
Use the FutureBuilder widget to build UI based on the state of the Future
class MyFutureBuilder extends StatelessWidget {
final Future<String> _futureData = fetchData();
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _futureData, // Provide the future you want to resolve
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// Check the state of the Future
if (snapshot.connectionState == ConnectionState.waiting) {
// While the future is being resolved, show a loading spinner
return CircularProgressIndicator();
} else if (snapshot.hasError) {
// If the future completes with an error, display the error
return Text('Error: ${snapshot.error}');
} else {
// If the future completes successfully, display the result
return Text('Result: ${snapshot.data}');
}
},
);
}
}
FutureBuilder<String>
is used because our Future returns a String.- The future parameter is set to
_futureData
, which holds the Future returned byfetchData
. - The builder function receives the current
BuildContext
and anAsyncSnapshot
. TheAsyncSnapshot
contains the state of the Future.
Example
In this example, we will build the same UI that we built in the previous article: how to use Futures with StatefulWidgets. We will find the user location coordinates and show them on the screen, leveraging the Geolocator library
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
class LocationScreen extends StatefulWidget {
const LocationScreen({super.key});
@override
State<LocationScreen> createState() => _LocationScreenState();
}
class _LocationScreenState extends State<LocationScreen> {
Future<Position>? position;
@override
void initState() {
super.initState();
position = getPosition();
}
Future<Position> getPosition() async {
await Geolocator.requestPermission();
await Geolocator.isLocationServiceEnabled();
await Future.delayed(const Duration(seconds: 3));
Position position = await Geolocator.getCurrentPosition();
return position;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('FutureBuilder')),
body: Center(
child: FutureBuilder(
future: position,
builder: (BuildContext context, AsyncSnapshot<Position> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('something terrible happened!');
}
return Text(snapshot.data.toString());
} else {
return const Text('');
}
}),
),
);
}
}
Tips
- Initial Data: You can provide initial data using the initialData parameter of FutureBuilder.
- Error Handling: Always handle errors gracefully to improve user experience.
- Optimization: Ensure that the Future is not recreated unnecessarily, which can cause multiple builds. Store the Future in a variable and reuse it as shown in the example with _futureData.
Leave a Reply