How to Implement a ListView.builder with Different Item Types in Flutter

You render heterogeneous items in a single scrollable list by returning different widgets from the itemBuilder callback based on the current index or underlying data model type.

The ListView.builder constructor in the flutter/flutter repository provides the standard mechanism for creating memory-efficient lists with mixed content layouts. By leveraging the IndexedWidgetBuilder function defined in packages/flutter/lib/src/widgets/scroll_view.dart, you can conditionally return headers, dividers, cards, or standard rows within the same listview builder flutter different item types implementation without loading all widgets into memory simultaneously.

Architecture of Lazy List Building

ListView.builder forwards your itemBuilder and itemCount parameters to a SliverChildBuilderDelegate located in packages/flutter/lib/src/widgets/sliver.dart. This delegate invokes your callback only for indices currently visible on screen (plus a small buffer), automatically handling keep-alive, repaint boundaries, and semantic indexing regardless of which widget type you return.

The itemBuilder signature (BuildContext, int) → Widget? receives the build context and current index. Returning null signals the end of the list when itemCount is omitted, though supplying an explicit itemCount allows the framework to compute the exact scroll extent.

Technique 1: Index-Based Conditional Rendering

For lists with a fixed, predictable structure, branch directly on the index inside itemBuilder. This approach keeps logic lightweight and avoids data model overhead.

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

  @override
  Widget build(BuildContext context) {
    const int itemCount = 7;

    return Scaffold(
      body: ListView.builder(
        itemCount: itemCount,
        itemBuilder: (BuildContext context, int index) {
          switch (index) {
            case 0: // Header
              return const ListTile(
                title: Text('🗂  Header'),
                tileColor: Colors.blueAccent,
              );
            case 1:
            case 2:
            case 3: // Regular items
              return ListTile(
                leading: const Icon(Icons.person),
                title: Text('Item #$index'),
              );
            case 4: // Divider
              return const Divider(thickness: 2);
            case 5: // Card with image
              return Card(
                margin: const EdgeInsets.all(8),
                child: Column(
                  children: [
                    Image.network(
                      'https://flutter.dev/assets/homepage/carousel/slide_1-bg.png',
                      height: 150,
                    ),
                    const ListTile(title: Text('Featured Card')),
                  ],
                ),
              );
            case 6: // Footer
              return const ListTile(
                title: Center(child: Text('🏁 End of List')),
              );
            default:
              return const SizedBox.shrink();
          }
        },
      ),
    );
  }
}

Technique 2: Model-Driven Type Dispatch

For dynamic data—such as API responses with heterogeneous schemas—define an enum or sealed class to represent row types, then switch on the model property inside itemBuilder.

enum RowType { header, text, image, divider, footer }

class RowItem {
  RowItem(this.type, {this.title, this.subtitle, this.imageUrl});
  final RowType type;
  final String? title;
  final String? subtitle;
  final String? imageUrl;
}

final List<RowItem> items = [
  RowItem(RowType.header, title: '🗂  Header'),
  RowItem(RowType.text, title: 'First item', subtitle: 'Standard row'),
  RowItem(RowType.text, title: 'Second item', subtitle: 'Standard row'),
  RowItem(RowType.divider),
  RowItem(RowType.image, title: 'Featured', imageUrl: 'https://flutter.dev/assets/homepage/carousel/slide_1-bg.png'),
  RowItem(RowType.footer, title: '🏁 End of List'),
];

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

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        final item = items[index];
        switch (item.type) {
          case RowType.header:
            return ListTile(
              title: Text(item.title ?? ''),
              tileColor: Colors.indigo,
            );
          case RowType.text:
            return ListTile(
              leading: const Icon(Icons.label),
              title: Text(item.title ?? ''),
              subtitle: Text(item.subtitle ?? ''),
            );
          case RowType.image:
            return Card(
              margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              child: Column(
                children: [
                  Image.network(item.imageUrl ?? ''),
                  ListTile(title: Text(item.title ?? '')),
                ],
              ),
            );
          case RowType.divider:
            return const Divider(thickness: 2);
          case RowType.footer:
            return ListTile(
              title: Center(child: Text(item.title ?? '')),
            );
        }
      },
    );
  }
}

Technique 3: Runtime Type Checking

When your data layer uses distinct classes rather than enums, employ Dart’s is operator to differentiate types. This pattern appears in the official Flutter cookbook and works well with sealed classes or inheritance hierarchies.

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    final item = items[index];
    if (item is HeaderItem) {
      return HeaderWidget(item);
    } else if (item is MessageItem) {
      return MessageWidget(item);
    } else {
      return const SizedBox.shrink();
    }
  },
);

Performance Considerations

Because itemBuilder runs on the UI thread, keep conditional logic lightweight—avoid heavy computation, synchronous file I/O, or unbounded loops. If an item requires async data, trigger the load inside the returned widget’s initState or wrap it in a FutureBuilder. The SliverChildBuilderDelegate referenced in packages/flutter/lib/src/widgets/sliver.dart automatically manages widget lifecycles, but returning complex nested trees for every index can still impact rasterization time.

For stateful interactions with mixed items, reference examples/api/lib/widgets/scroll_view/list_view.0.dart in the Flutter repository, which demonstrates selection handling and state management within lazy-built lists.

Summary

  • ListView.builder in packages/flutter/lib/src/widgets/scroll_view.dart uses SliverChildBuilderDelegate to lazily instantiate children only when they approach the viewport.
  • Index-based branching works best for static layouts with predefined positions (e.g., header at index 0, footer at last index).
  • Model-driven dispatch scales better for dynamic API data, using an enum or type field to select the appropriate widget.
  • Runtime type checks (is/as) provide compile-time safety when using sealed classes or distinct data models.
  • Performance remains efficient because the framework recycles widgets and handles repaint boundaries automatically, provided you keep itemBuilder logic synchronous and lightweight.

Frequently Asked Questions

Can ListView.builder handle infinite lists with different item types?

Yes. Omit itemCount and return null from itemBuilder when no more data exists; the framework will stop requesting new items. Ensure your data source can provide type information for each loaded batch so you continue returning the correct widget variants as the user scrolls.

How does Flutter optimize memory when mixing item types in the same list?

The SliverChildBuilderDelegate (defined in packages/flutter/lib/src/widgets/sliver.dart) creates a limited number of widget objects—only those near the viewport plus a cache extent. When items scroll far off-screen, their associated Element trees are eligible for garbage collection unless you explicitly wrap them in AutomaticKeepAliveClientMixin.

Should I use ListView or Column with SingleChildScrollView for heterogeneous lists?

Use ListView.builder for lists exceeding a few dozen items or when item heights vary significantly. Column inside SingleChildScrollView builds all children eagerly, causing jank and high memory usage with large datasets, whereas ListView.builder instantiates widgets on demand regardless of type diversity.

How do I add animations when items scroll into view with different layouts?

Wrap each returned widget in a FadeTransition or SlideTransition driven by an AnimationController initialized in the widget's initState. Because ListView.builder recycles widgets via the SliverChildBuilderDelegate's keep-alive mechanism, ensure you dispose controllers properly or use AnimatedBuilder to prevent memory leaks when items scroll off-screen.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →