Flutter ListView.builder Tutorial: 5 Easy Examples

Flutter ListView.builder
Flutter ListView.builder

When building Flutter applications, displaying a list of items is one of the most common requirements. You might need to show products, user profiles, messages, notifications, or data fetched from an API. Flutter provides several widgets for this purpose, and ListView.builder is one of the most useful options.

In this Flutter ListView.builder tutorial, you will learn how to create dynamic lists, display data from a list, customize list items, fetch API data, and improve performance when working with large datasets.

Whether you are a beginner or an experienced Flutter developer, these examples will help you understand how to use ListView.builder in real-world applications.

What Is ListView.builder in Flutter?

ListView.builder is a Flutter constructor that creates list items on demand as they are needed for display. Instead of building every item at once, it builds visible items and additional items as the user scrolls.

This approach is useful when your application displays a large number of items or data received from an API.

For example, you can use ListView.builder to display:

  • Product lists in shopping applications
  • Contacts and user profiles
  • Chat messages
  • News articles and blog posts
  • API response data
  • Transaction and order histories

Basic Syntax

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

Here, itemCount defines the number of items, while itemBuilder returns the widget to display at each index.

ListView.builder vs ListView in Flutter

Flutter’s ListView widget supports multiple constructors, including ListView, ListView.builder, and ListView.separated.

FeatureListViewListView.builder
Best forSmall, predefined listsDynamic or large lists
Item creationChildren are supplied up frontItems are built on demand
PerformanceSuitable for a small number of itemsUsually more efficient for long lists
Data sourceUsually a predefined widget listLists, API data, or other indexed data
CustomizationSupports custom child widgetsSupports custom child widgets

Use the regular ListView constructor when you have a small, fixed number of widgets. Choose ListView.builder when your list is dynamic or contains many items.

Example 1: Simple Flutter ListView.builder

Let’s start with a basic example that displays a list of programming languages.

Complete Code

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  final List<String> languages = const [
    'Dart',
    'Flutter',
    'Java',
    'Kotlin',
    'Python',
    'JavaScript',
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ListView.builder Example'),
        ),
        body: ListView.builder(
          itemCount: languages.length,
          itemBuilder: (context, index) {
            return ListTile(
              leading: CircleAvatar(
                child: Text('${index + 1}'),
              ),
              title: Text(languages[index]),
            );
          },
        ),
      ),
    );
  }
}

How It Works

  1. languages contains the items to display.
  2. itemCount tells Flutter how many items are available.
  3. itemBuilder receives the current index.
  4. ListTile displays each language with a numbered avatar.

Important: Always provide itemCount when the total number of items is known. This helps Flutter determine the list’s extent and avoids requesting indexes outside the available data.

Example 2: Custom List Items with ListView.builder

In real applications, you often need more than a simple text row. You might want to display an image, title, subtitle, and action button together.

The following example creates a simple list of team members.

import 'package:flutter/material.dart';

class TeamMember {
  final String name;
  final String role;
  final IconData icon;

  const TeamMember({
    required this.name,
    required this.role,
    required this.icon,
  });
}

class TeamListPage extends StatelessWidget {
  TeamListPage({super.key});

  final List<TeamMember> members = const [
    TeamMember(
      name: 'Aarav Sharma',
      role: 'Flutter Developer',
      icon: Icons.code,
    ),
    TeamMember(
      name: 'Priya Verma',
      role: 'UI/UX Designer',
      icon: Icons.design_services,
    ),
    TeamMember(
      name: 'Rahul Mehta',
      role: 'Backend Developer',
      icon: Icons.storage,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Our Team')),
      body: ListView.builder(
        itemCount: members.length,
        padding: const EdgeInsets.all(12),
        itemBuilder: (context, index) {
          final member = members[index];

          return Card(
            child: ListTile(
              leading: CircleAvatar(
                child: Icon(member.icon),
              ),
              title: Text(member.name),
              subtitle: Text(member.role),
              trailing: const Icon(Icons.arrow_forward_ios),
              onTap: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('Selected ${member.name}'),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

In this example, each item is represented by a TeamMember object. The builder reads the object at the current index and creates a customized card.

You can adapt this structure to display products, employees, categories, or other application data.

Example 3: Flutter ListView.builder with API Data

One of the most common use cases for ListView.builder is displaying data fetched from a REST API.

For this example, we will use the JSONPlaceholder API, which provides sample post data. We will use the Dio package to make the HTTP request.

Step 1: Install Dio

Run this command in your Flutter project:

flutter pub add dio

If you want to understand Dio in more detail, read our Flutter Dio API Call tutorial.

Step 2: Create an API Service

Create a file named api_service.dart.

import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio = Dio();

  Future<List<dynamic>> fetchPosts() async {
    final response = await _dio.get(
      'https://jsonplaceholder.typicode.com/posts',
    );

    final data = response.data;

    if (data is! List) {
      throw const FormatException(
        'Unexpected API response format',
      );
    }

    return data;
  }
}

The service sends a GET request and returns the decoded list. Dio throws an exception when the request fails, which we will handle in the UI.

Step 3: Display API Data Using ListView.builder

Create a file named posts_page.dart.

import 'package:flutter/material.dart';
import 'api_service.dart';

class PostsPage extends StatefulWidget {
  const PostsPage({super.key});

  @override
  State<PostsPage> createState() => _PostsPageState();
}

class _PostsPageState extends State<PostsPage> {
  final ApiService _apiService = ApiService();

  late Future<List<dynamic>> _postsFuture;

  @override
  void initState() {
    super.initState();
    _postsFuture = _apiService.fetchPosts();
  }

  void _retry() {
    setState(() {
      _postsFuture = _apiService.fetchPosts();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('API Posts'),
        actions: [
          IconButton(
            onPressed: _retry,
            icon: const Icon(Icons.refresh),
            tooltip: 'Refresh posts',
          ),
        ],
      ),
      body: FutureBuilder<List<dynamic>>(
        future: _postsFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('Unable to load posts.'),
                  const SizedBox(height: 8),
                  ElevatedButton(
                    onPressed: _retry,
                    child: const Text('Try Again'),
                  ),
                ],
              ),
            );
          }

          final posts = snapshot.data ?? [];

          if (posts.isEmpty) {
            return const Center(
              child: Text('No posts available.'),
            );
          }

          return ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];

              return ListTile(
                leading: CircleAvatar(
                  child: Text('${post['id']}'),
                ),
                title: Text(
                  post['title']?.toString() ?? 'Untitled',
                ),
                subtitle: Text(
                  post['body']?.toString() ?? '',
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Set PostsPage as the home screen in your MaterialApp to run the example.

How This API Example Works

  • initState() starts the initial API request.
  • FutureBuilder manages the loading, error, and success states.
  • ListView.builder renders the returned posts.
  • _retry() creates a new future when the user refreshes or retries.

The key point is that the future is stored in state instead of creating a new request every time build() runs.

For more details about asynchronous UI, see our Flutter FutureBuilder tutorial.

Example 4: Flutter ListView.separated

Sometimes you need a divider or spacing between list items. Flutter provides ListView.separated for this purpose.

ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
  separatorBuilder: (context, index) {
    return const Divider(height: 1);
  },
)

The itemBuilder creates each item, while separatorBuilder creates the widget between adjacent items.

Use this constructor for contact lists, settings screens, menus, and other layouts where separators improve readability.

Example 5: Improve ListView.builder Performance

ListView.builder already builds children on demand, but a few additional choices can make your lists easier to maintain and more efficient.

1. Set itemCount

When the list length is known, use:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Text(items[index]);
  },
)

Providing a count makes the list’s extent easier to determine and prevents unnecessary out-of-range access.

2. Use itemExtent for Uniform Rows

If every item has the same height, you can provide itemExtent.

ListView.builder(
  itemCount: items.length,
  itemExtent: 60,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

This tells Flutter that every row is 60 logical pixels high. Only use it when the row layout genuinely fits that fixed extent.

3. Avoid Expensive Work Inside itemBuilder

Do not repeatedly make API calls, parse large files, or perform expensive calculations inside itemBuilder. Prepare your data before building the list and keep each item’s build logic lightweight.

4. Use ListView.separated When Needed

If your list needs dividers, use ListView.separated instead of manually adding divider widgets to the item list.

5. Use Pagination for Very Large Datasets

For thousands of records from an API, consider loading data in pages rather than downloading everything at once. A scroll listener or ScrollController can trigger the next request as the user approaches the end.

Pagination should include safeguards against duplicate requests and should handle loading and error states for subsequent pages.

Common Mistakes When Using ListView.builder

Calling the API Inside itemBuilder

Avoid this pattern:

itemBuilder: (context, index) {
  // Avoid starting an API request here.
  return const SizedBox();
}

The builder may run multiple times as Flutter lays out or rebuilds list items. Fetch data in a service or state-management layer instead.

Using an Invalid Index

If you access items[index], make sure itemCount matches the available data. Otherwise, an invalid index can cause a range error.

Nesting ListView Inside Another Scrollable

Putting a ListView inside another vertically scrolling widget can create layout or scrolling issues. For a small list inside a Column, you can use Expanded to give the list bounded space.

If you need a single scrolling page with multiple sections, consider CustomScrollView with slivers, or configure nested scrolling deliberately.

Recreating Futures on Every Build

When using FutureBuilder, avoid calling the API directly in the future argument if that call creates a new future during every build. Store the future in state and replace it when a refresh is needed.

Frequently Asked Questions

1. What is Flutter ListView.builder used for?

ListView.builder is used to display scrollable lists where items are created on demand. It is especially useful for dynamic data and long lists.

2. What is the difference between ListView and ListView.builder?

The regular ListView constructor accepts a list of child widgets that you provide up front. ListView.builder creates child widgets as they are needed, making it a good choice for large or dynamic lists.

3. Can ListView.builder display API data?

Yes. Fetch data from an API using a service or repository, manage loading and error states, and use ListView.builder to display the resulting collection.

4. What does itemCount do in ListView.builder?

itemCount specifies the number of items available in the list. When the count is known, providing it helps Flutter determine the list’s extent and avoids requesting unavailable indexes.

5. How can I add dividers between items?

Use ListView.separated with a separatorBuilder that returns a Divider or another separator widget.

6. Is ListView.builder better for large lists?

It is generally preferable for long or dynamic lists because it builds children on demand. However, overall performance also depends on widget complexity, data handling, image loading, and how the list is structured.

Conclusion

ListView.builder is an essential Flutter widget for creating dynamic and scrollable lists. You can use it for simple text lists, custom cards, API responses, and large datasets.

Start with the basic example, then try displaying API data with FutureBuilder and Dio. As your app grows, consider fixed item extents, pagination, and suitable state management.

For more Flutter development guides, explore our tutorials on Dio API calls, FutureBuilder, and responsive UI in Flutter.

Additional Resources

Leave a Comment