Flutter developers have rarely been able to carry out tasks in asynchronous programming without having to deal with futures and asynchronous functions. And FutureBuilder has become one of the most utilized widgets in this area. In today's blog post, we'll be tackling Flutter's FutureBuilder, a class built specifically to deal with future functions in Flutter.
The Flutter FutureBuilder class provides a straightforward way of dealing with asynchronous operations. With the FutureBuilder widget, we can define the behavior of our Flutter applications based on the outcome of a future. The FutureBuilder provides us with the builder function which is called when a future is completed, and through this, we can manipulate the UI based on the future's outcome – whether the future completed successfully, or resulted in an error, which makes FutureBuilder a staple tool in the Flutter pipeline.
The FutureBuilder is a widget that returns another widget based on the latest snapshot. Snapshot, which is an AsyncSnapshot, holds the most recent interaction with the future, including data, error, and the state of the connection. So, how does it work? It's pretty simple. The FutureBuilder takes a future and a builder function as parameters.
The future is your asynchronous computation. Whether it's fetching data from an API or waiting for user input, any asynchronous task you have can be handled here. The builder function, as the name suggests, is responsible for rendering the UI. This function takes in two parameters: the BuildContext context and an AsyncSnapshot snapshot.
Putting this together, your FutureBuilder Flutter structure would look something like this:
1FutureBuilder( 2 future: futureFunction(), 3 builder: (BuildContext context, AsyncSnapshot snapshot){ 4 /// Depending on the state of our future, 5 /// we return the appropriate ui elements 6 if (snapshot.connectionState == ConnectionState.waiting){ 7 return CircularProgressIndicator(); 8 }else if (snapshot.hasError){ 9 return Text("Error: ${snapshot.error}"); 10 }else { 11 return Text("${snapshot.data}"); 12 } 13 }, 14)
In the snippet above, we pass our asynchronous task, futureFunction(), to the future parameter. futureFunction() is merely a placeholder for any asynchronous function you might have in your Flutter code. The BuildContext context and the AsyncSnapshot snapshot are then used in the builder method to decide which UI element to render.
Let's take a closer look with a more precise example:
1FutureBuilder<String>( 2 future: fetchUserOrder(), 3 builder: (BuildContext context, AsyncSnapshot<String> snapshot) { 4 List<Widget> children; 5 if (snapshot.hasData) { 6 children = <Widget>[ 7 Icon( 8 Icons.check_circle_outline, 9 color: Colors.green, 10 size: 60, 11 ), 12 Padding( 13 padding: const EdgeInsets.only(top: 16), 14 child: Text('Your order is: ${snapshot.data}'), 15 ), 16 ]; 17 } else if (snapshot.hasError) { 18 children = <Widget>[ 19 Icon( 20 Icons.error_outline, 21 color: Colors.red, 22 size: 60, 23 ), 24 Padding( 25 padding: const EdgeInsets.only(top: 16), 26 child: Text('Error: ${snapshot.error}'), 27 ), 28 ]; 29 } else { 30 children = const <Widget>[ 31 SizedBox( 32 child: CircularProgressIndicator(), 33 width: 60, 34 height: 60, 35 ), 36 Padding( 37 padding: EdgeInsets.only(top: 16), 38 child: Text('Awaiting result...'), 39 ) 40 ]; 41 } 42 return Center( 43 child: Column( 44 mainAxisAlignment: MainAxisAlignment.center, 45 children: children, 46 ), 47 ); 48 }, 49)
This localized FutureBuilder example shows how you can build rich, responsive UIs with ease. fetchUserOrder is an asynchronous function that, in practice, might communicate with a server and wait for a response.
You may want to take note of the snapshot.hasData, snapshot.hasError, and snapshot.connectionState, which represent different states of the future and provide more granular control over the UI representation of your data.
In essence, FutureBuilder works by taking a Future and a builder function; then it waits for the Future to complete. Once Future completes or encounters an error message, FutureBuilder calls the builder's function. The builder function then builds the widget per the current state of the Future computation.
The FutureBuilder class requires two essential properties: future and builder.
The future is where we pass in the Future function that we desire to complete. Bear in mind that it's crucial to ensure that the FutureBuilder doesn't recreate the Future when the Widget build method is called multiple times.
To achieve this, you have to separate the Future function from the FutureBuilder widget to prevent constant fetching of data.
1Future<void> main() { 2 runApp(const MyApp()); 3} 4 5class MyApp extends StatelessWidget { 6 final Future<String> _calculation = Future<String>.delayed( 7 const Duration(seconds: 2), 8 () => 'Data Loaded', 9 ); 10 11 @override 12 Widget build(BuildContext context) { 13 return MaterialApp( 14 home: Scaffold( 15 body: FutureBuilder<String>( 16 future: _calculation, 17 builder: (BuildContext context, AsyncSnapshot<String> snapshot) { 18 return snapshot.connectionState == ConnectionState.waiting 19 ? CircularProgressIndicator() 20 : Text(snapshot.data ?? 'No Data'); 21 }, 22 ), 23 ), 24 ); 25 } 26}
In the example above, the _calculation function, which is the Future function, was separated from the widget build function. This way, the function's outcome - Data Loaded in this case - is not re-computed every time the widget tree rebuilds.
The builder is a function that builds and returns a Widget. The build strategy currently selected, given the current timing-dependent sub-sequence of connectionState, data, and error states from the most recent AsyncSnapshot representing future.
When using a FutureBuilder, it's common to fetch data from an external source like an API. This asynchronous task occurs in the future and, when completed successfully, the data can be accessed via the snapshot's data property.
Below is an example of how you might use the FutureBuilder to fetch data:
1FutureBuilder( 2 future: fetchData(), // Suppose this function fetches data from an API 3 builder: (BuildContext context, AsyncSnapshot snapshot) { 4 if (snapshot.connectionState == ConnectionState.waiting) { 5 return CircularProgressIndicator(); 6 } else if (snapshot.hasError) { 7 return Text('Error: ${snapshot.error}'); 8 } else { 9 return ListView.builder( // This ListView displays the fetched data 10 itemCount: snapshot.data.length, 11 itemBuilder: (context, index) { 12 return ListTile( 13 title: Text('${snapshot.data[index]}'), 14 ); 15 }, 16 ); 17 } 18 }, 19)
In the above code, fetchData() is an asynchronous function that returns a Future, representing the result of an HTTP request, for instance. The Builder then checks the future's current state and, depending on the state decides which widget to display.
Notice how the ListView.builder uses snapshot.data to create the list items when data has been fetched successfully. This works because the snapshot.data returns whatever data the Future returned when it is completed. In this case, a list of items.
A common mistake developers make when dealing with futures is not considering all possible states the Future could be in. When the FutureBuilder's future runs into any issue, an error is thrown. It’s important to handle errors properly to avoid unexpected app crashes.
In the Flutter FutureBuilder class, error handling revolves around the snapshot. When an error occurs during the computation of the future, the snapshot’s hasError property returns true, and snapshot.error contains the error object.
1FutureBuilder( 2 future: futureFunction(), 3 builder: (BuildContext context, AsyncSnapshot snapshot) { 4 if (snapshot.hasError) { 5 return Text('Error: ${snapshot.error}'); 6 } else if (snapshot.connectionState == ConnectionState.waiting) { 7 return CircularProgressIndicator(); 8 } else { 9 return Text('${snapshot.data}'); 10 } 11 }, 12)
In this example, if an error occurs during the computation of the futureFunction(), it's caught and displayed in the Text widget. The FutureBuilder, hence, equips you with the ability to handle errors gracefully and provide feedback to users.
1FutureBuilder( 2 future: fetchUserOrder(), 3 builder: (context, snapshot) { 4 if (snapshot.connectionState == ConnectionState.waiting) { 5 return CircularProgressIndicator(); 6 } else { 7 if (snapshot.error != null) { 8 // Error handling... 9 return Center( 10 child: Text('Something went wrong!'), 11 ); 12 } else { 13 return Text('Fetched Order: ${snapshot.data}'); 14 } 15 } 16 }, 17);
This set of recommendations will assist you in utilizing the FutureBuilder widget to its maximum potential. The FutureBuilder widget can handle different widget states based on the resolution of a Future, whether it’s still loading data, completed with data, or error.
FutureBuilder is an incredible tool for efficiently managing asynchronous operations in your Flutter applications, providing a seamless user experience.
In today's blog post, we've taken a deep dive into Flutter's FutureBuilder class. We've dissected its structure and how it works, explained through an example, and even touched upon error handling and best practices when using this essential Flutter class.
The FutureBuilder widget is a priceless tool in asynchronous programming as it enables us to streamline our code and enhance our application's responsiveness, creating an improved user experience.
Incorporating FutureBuilder into your development toolkit will significantly optimize your handling of asynchronous tasks, making your Flutter applications more robust and reliable.
We hope this guide serves as a helpful resource in your Flutter development journey. As always, stay tuned for more posts on creating extraordinary Flutter applications.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.