Displaying dynamic data is one of the most important tasks in modern mobile applications. Whether it’s a list of products, news articles, or user profiles, apps often rely on external APIs that return data in JSON format.
Flutter provides powerful tools to fetch, parse, and render API data seamlessly inside a ListView. In this guide, we will explore:
- Fetching data from an API using the
httppackage. - Parsing JSON data into Dart objects.
- Rendering the parsed data in a scrollable
ListView. - Handling errors and showing loading states.
- Real-world examples and best practices.
By the end, you’ll be able to create production-ready apps that consume APIs and display lists dynamically.
Introduction to JSON and API Data
What is JSON?
JSON (JavaScript Object Notation) is a lightweight data format widely used for exchanging information between clients and servers. Example:
[
{
"id": 1,
"title": "First Post",
"body": "This is the first post body."
},
{
"id": 2,
"title": "Second Post",
"body": "This is the second post body."
}
]
This JSON array represents a list of posts with id, title, and body.
Why Use ListView with API Data?
- To display dynamic, real-world content instead of static lists.
- To update UI automatically as data changes on the server.
- To build apps like news feeds, e-commerce catalogs, and social networks.
Fetching Data from an API
In Flutter, the most common way to fetch data is by using the http package.
Step 1: Add the http Package
In pubspec.yaml:
dependencies:
http: ^1.2.0
flutter:
sdk: flutter
Run:
flutter pub get
Step 2: Import the Package
import 'package:http/http.dart' as http;
import 'dart:convert';
Step 3: Make an API Request
For example, fetching posts from JSONPlaceholder (a free fake API):
Future<List<dynamic>> fetchPosts() async {
final response =
await http.get(Uri.parse("https://jsonplaceholder.typicode.com/posts"));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception("Failed to load posts");
}
}
http.getmakes a GET request.- If successful, we decode the JSON string into a Dart object using
json.decode. - Otherwise, throw an error.
Displaying Parsed JSON in a ListView
Now that we can fetch the data, let’s display it inside a ListView.
Step 1: Create a StatefulWidget
class PostListPage extends StatefulWidget {
@override
_PostListPageState createState() => _PostListPageState();
}
class _PostListPageState extends State<PostListPage> {
late Future<List<dynamic>> futurePosts;
@override
void initState() {
super.initState();
futurePosts = fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Posts")),
body: FutureBuilder<List<dynamic>>(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text("Error: ${snapshot.error}"));
} else {
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post["title"]),
subtitle: Text(post["body"]),
);
},
);
}
},
),
);
}
}
Explanation
- FutureBuilder
- Handles the asynchronous API call.
- Displays loading spinner while waiting.
- Shows error if request fails.
- Renders
ListViewonce data arrives.
- ListView.builder
- Efficiently renders only visible items.
- Each
ListTiledisplays a post’stitleandbody.
Parsing JSON into Dart Models
While directly using Map<String, dynamic> works, it’s better to create Dart models for type safety and cleaner code.
Step 1: Create a Post Model
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json["id"],
title: json["title"],
body: json["body"],
);
}
}
Step 2: Modify Fetch Function
Future<List<Post>> fetchPosts() async {
final response =
await http.get(Uri.parse("https://jsonplaceholder.typicode.com/posts"));
if (response.statusCode == 200) {
List jsonData = json.decode(response.body);
return jsonData.map((post) => Post.fromJson(post)).toList();
} else {
throw Exception("Failed to load posts");
}
}
Step 3: Update UI Code
FutureBuilder<List<Post>>(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text("Error: ${snapshot.error}"));
} else {
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
leading: CircleAvatar(child: Text(post.id.toString())),
title: Text(post.title),
subtitle: Text(post.body),
);
},
);
}
},
)
Now each item in the list is strongly typed as a Post.
Adding Images and Rich Layouts
Many APIs return images (like profile pictures or product images). You can extend the ListTile to include images.
Example with Images
ListTile(
leading: Image.network("https://via.placeholder.com/150"),
title: Text(post.title),
subtitle: Text(post.body),
)
This displays an image from a URL in each list item.
Handling Loading and Error States
It’s important to handle three states:
- Loading → Show a spinner (
CircularProgressIndicator). - Error → Show an error message.
- Success → Show the list of items.
FutureBuilder makes this easy, as shown in earlier examples.
Real-World Example: Users API
Suppose you fetch users from an API:
[
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]"
}
]
Dart Model
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json["id"],
name: json["name"],
email: json["email"],
);
}
}
UI
ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
subtitle: Text(user.email),
);
},
)
Best Practices for ListView with API Data
- Always use models instead of raw maps
- Improves readability and reduces bugs.
- Use FutureBuilder or StreamBuilder
- Handles asynchronous API calls gracefully.
- Handle loading and error states
- Never leave the user with a blank screen.
- Optimize large lists with ListView.builder
- Avoid rendering all items at once.
- Paginate if necessary
- For APIs with thousands of items, load in chunks.
- Test with real-world APIs
- Start with free APIs like JSONPlaceholder or ReqRes.
Leave a Reply