iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

ListView

ListView renders a scrollable list. ListView.builder is the lazy variant — only builds widgets for items currently on screen. The right choice for any list with more than ~20 items.

Builder, separated, infinite scroll, pull-to-refresh

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

class FeedPage extends StatefulWidget {
    const FeedPage({super.key});
    @override
    State<FeedPage> createState() => _FeedPageState();
}

class _FeedPageState extends State<FeedPage> {
    final _posts = <Post>[];
    final _scroll = ScrollController();
    bool _loading = false;
    bool _hasMore = true;

    @override
    void initState() {
        super.initState();
        _loadMore();
        _scroll.addListener(() {
            if (_scroll.position.pixels > _scroll.position.maxScrollExtent - 200 && !_loading) {
                _loadMore();
            }
        });
    }

    Future<void> _loadMore() async {
        if (!_hasMore || _loading) return;
        setState(() => _loading = true);
        final batch = await api.fetchPosts(after: _posts.lastOrNull?.id);
        setState(() {
            _posts.addAll(batch);
            _hasMore = batch.length == 20;
            _loading = false;
        });
    }

    @override
    Widget build(BuildContext context) {
        // 1) Pull-to-refresh + lazy builder
        return RefreshIndicator(
            onRefresh: () async {
                _posts.clear();
                _hasMore = true;
                await _loadMore();
            },
            child: ListView.separated(
                controller: _scroll,
                itemCount: _posts.length + (_hasMore ? 1 : 0),
                separatorBuilder: (_, __) => const Divider(height: 1),
                itemBuilder: (context, i) {
                    if (i >= _posts.length) {
                        return const Padding(
                            padding: EdgeInsets.all(16),
                            child: Center(child: CircularProgressIndicator()),
                        );
                    }
                    final p = _posts[i];
                    return ListTile(
                        leading: CircleAvatar(backgroundImage: NetworkImage(p.avatarUrl)),
                        title:   Text(p.title, maxLines: 1, overflow: TextOverflow.ellipsis),
                        subtitle: Text(p.summary, maxLines: 2, overflow: TextOverflow.ellipsis),
                        trailing: Text(p.timeAgo),
                        onTap: () => Navigator.pushNamed(context, '/post', arguments: p.id),
                    );
                },
            ),
        );
    }

    @override
    void dispose() {
        _scroll.dispose();
        super.dispose();
    }
}

// 2) Static list — fine for a fixed small list
ListView(
    children: const [
        ListTile(title: Text('One')),
        ListTile(title: Text('Two')),
        ListTile(title: Text('Three')),
    ],
);

// 3) Horizontal list
SizedBox(
    height: 120,
    child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: items.length,
        itemBuilder: (_, i) => Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: CategoryCard(item: items[i]),
        ),
    ),
);

// 4) SliverList in a CustomScrollView — when you mix lists with other content
CustomScrollView(
    slivers: [
        const SliverAppBar(title: Text('Feed'), pinned: true),
        SliverList.builder(
            itemCount: posts.length,
            itemBuilder: (_, i) => ListTile(title: Text(posts[i].title)),
        ),
    ],
);

// 5) Performance tips
//   • Use ListView.builder for >20 items — never the eager constructor
//   • Give items a Key when reordering — Flutter can preserve state
//   • If items have fixed height, set itemExtent for big lists
//   • Avoid building heavy widgets inside itemBuilder — use const where possible

Why it matters

For real feeds, always: ListView.builder (lazy), RefreshIndicator (pull-to-refresh), a scroll listener for infinite scroll, and SliverAppBar when you want collapsing headers above the list.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
ListView.builder(
    itemCount: users.length,
    itemBuilder: (_, i) => ListTile(
        title: Text(users[i].name),
        subtitle: Text(users[i].email),
    ),
)
Try it Yourself »

Exercise

Build list rows lazily.

ListView. (itemCount: 100, itemBuilder: (_, i) => Text('row $i'))

Discussion

Loading…