Learn how to effectively display fetched data in Flutter using FutureBuilder, handle asynchronous states, and optimize your app's UI responsiveness.
In the world of mobile app development, displaying data fetched from a network or database is a common requirement. Flutter provides a powerful widget called FutureBuilder
that allows developers to build widgets based on the state of a Future
. This section will guide you through the process of displaying fetched data in the UI using FutureBuilder
, understanding the snapshot properties, handling different states, and best practices for maintaining a responsive UI.
Before we dive into using FutureBuilder
, it’s essential to understand how to create asynchronous functions in Flutter. Asynchronous functions are necessary for performing network calls or any operation that might take some time to complete. In Flutter, these functions return Future
objects.
Here’s a simple example of an asynchronous function that fetches user data:
Future<User> fetchUser() async {
// Simulate a network call
await Future.delayed(Duration(seconds: 2));
return User(name: 'John Doe', age: 30);
}
In this example, fetchUser
is an asynchronous function that simulates a network call by delaying for 2 seconds before returning a User
object.
FutureBuilder
FutureBuilder
is a widget that builds itself based on the latest snapshot of interaction with a Future
. It is a powerful tool for handling asynchronous data in Flutter.
Let’s see how to use FutureBuilder
to display user data fetched by the fetchUser
function:
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
User user = snapshot.data!;
return Text('Hello, ${user.name}');
} else {
return Text('No data available.');
}
},
);
}
In this example, FutureBuilder
takes a Future<User>
and a builder function. The builder function is called whenever the Future
’s state changes, and it receives a snapshot
containing the current state of the Future
.
snapshot
The snapshot
object in the FutureBuilder
builder function provides several properties that help you understand the current state of the Future
.
connectionState
: Indicates the state of the asynchronous computation. It can be one of the following:
none
: No connection is initiated.waiting
: The Future
is waiting for data.active
: The Future
is active (rarely used).done
: The Future
has completed.data
: Contains the data returned by the Future
if it has completed successfully.
error
: Contains any error thrown during the Future
’s execution.
Understanding the connection states is crucial for providing appropriate UI feedback to the user.
Loading State: When connectionState
is waiting
, it’s a good practice to show a loading indicator, such as CircularProgressIndicator
, to inform the user that data is being fetched.
Error State: If snapshot.hasError
is true, display an error message. Ensure that the error message is user-friendly and provides enough information for the user to understand what went wrong.
Data Available: When snapshot.hasData
is true, it means the Future
has completed successfully, and you can display the fetched data.
Let’s explore how to handle different states in FutureBuilder
with a more detailed example:
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Press button to start.');
case ConnectionState.waiting:
return CircularProgressIndicator();
case ConnectionState.active:
return Text('Fetching data...');
case ConnectionState.done:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
User user = snapshot.data!;
return Text('Hello, ${user.name}');
} else {
return Text('No data available.');
}
}
},
);
}
In this example, we use a switch
statement to handle different connection states. This approach makes the code more readable and easier to maintain.
ListView.builder
with Fetched DataWhen fetching a list of items, you can use ListView.builder
inside FutureBuilder
to display the data efficiently. Here’s how you can do it:
Future<List<Item>> fetchItems() async {
// Simulate a network call
await Future.delayed(Duration(seconds: 2));
return List.generate(10, (index) => Item(title: 'Item $index'));
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Item>>(
future: fetchItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
Item item = snapshot.data![index];
return ListTile(
title: Text(item.title),
);
},
);
} else {
return Text('No data available.');
}
},
);
}
In this example, fetchItems
is an asynchronous function that returns a list of Item
objects. FutureBuilder
is used to build a ListView
once the data is available.
To ensure a smooth user experience and maintainable code, consider the following best practices:
Keep the UI Responsive: Always show loading indicators when data is being fetched. This provides feedback to the user and prevents the UI from appearing frozen.
User-Friendly Error Messages: Display clear and concise error messages. Avoid technical jargon that might confuse the user.
Separation of Concerns: Separate UI code from data fetching logic. This can be achieved by using services or repositories to handle data fetching, making your code more modular and easier to test.
Error Handling: Implement robust error handling to manage network failures or unexpected errors gracefully.
Optimize Network Calls: Cache data when possible to reduce network calls and improve performance.
To reinforce your understanding of displaying fetched data in the UI, try the following exercises:
Build a Simple News App: Create a news app that fetches articles from an API and displays them in a list. Use FutureBuilder
to handle the asynchronous data fetching.
Implement Pull-to-Refresh: Enhance the news app by adding pull-to-refresh functionality, allowing users to refresh the list of articles manually.
Error Handling Exercise: Modify the news app to handle network errors gracefully. Display a retry button when an error occurs.
Data Caching: Implement data caching in the news app to improve performance and reduce network usage.
UI Enhancements: Experiment with different loading indicators and error messages to improve the user experience.
Displaying fetched data in the UI is a fundamental aspect of mobile app development. By using FutureBuilder
, you can efficiently manage asynchronous data fetching and provide a responsive user interface. Remember to handle different states appropriately and follow best practices to ensure a smooth user experience.