ListView with JSON / API Data

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 http package.
  • 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.get makes 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&lt;List&lt;dynamic&gt;&gt;(
    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&#91;index];
            return ListTile(
              title: Text(post&#91;"title"]),
              subtitle: Text(post&#91;"body"]),
            );
          },
        );
      }
    },
  ),
);
} }

Explanation

  1. FutureBuilder
    • Handles the asynchronous API call.
    • Displays loading spinner while waiting.
    • Shows error if request fails.
    • Renders ListView once data arrives.
  2. ListView.builder
    • Efficiently renders only visible items.
    • Each ListTile displays a post’s title and body.

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&#91;"id"],
  title: json&#91;"title"],
  body: json&#91;"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) =&gt; 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&#91;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:

  1. Loading → Show a spinner (CircularProgressIndicator).
  2. Error → Show an error message.
  3. 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&#91;"id"],
  name: json&#91;"name"],
  email: json&#91;"email"],
);
} }

UI

ListView.builder(
  itemCount: users.length,
  itemBuilder: (context, index) {
final user = users&#91;index];
return ListTile(
  leading: CircleAvatar(child: Text(user.name&#91;0])),
  title: Text(user.name),
  subtitle: Text(user.email),
);
}, )

Best Practices for ListView with API Data

  1. Always use models instead of raw maps
    • Improves readability and reduces bugs.
  2. Use FutureBuilder or StreamBuilder
    • Handles asynchronous API calls gracefully.
  3. Handle loading and error states
    • Never leave the user with a blank screen.
  4. Optimize large lists with ListView.builder
    • Avoid rendering all items at once.
  5. Paginate if necessary
    • For APIs with thousands of items, load in chunks.
  6. Test with real-world APIs
    • Start with free APIs like JSONPlaceholder or ReqRes.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *