Flutter

Lists / RecyclerView

ListView.builder, GridView.builder, RefreshIndicator pull to refresh, infinite scroll with ScrollController, and ListTile widget.

ListView.builder

ListView.builder lazily builds list items on demand — similar to Android's RecyclerView. Use it for any list longer than a handful of items.

Dart
class UserListPage extends StatelessWidget {
  final List<Map<String, String>> users = const [
    {'name': 'Alice', 'role': 'Admin'},
    {'name': 'Bob',   'role': 'Editor'},
    {'name': 'Carol', 'role': 'Viewer'},
  ];

  const UserListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List')),
      body: ListView.builder(
        padding: const EdgeInsets.symmetric(vertical: 8),
        itemCount: users.length,
        itemBuilder: (context, index) {
          final user = users[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            child: ListTile(
              leading: CircleAvatar(child: Text(user['name']![0])),
              title: Text(user['name']!),
              subtitle: Text(user['role']!),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => print('Tapped ${user['name']}'),
            ),
          );
        },
      ),
    );
  }
}

GridView.builder

GridView.builder renders a lazy grid. Use SliverGridDelegateWithFixedCrossAxisCount to control column count and spacing.

Dart
class PhotoGridPage extends StatelessWidget {
  final List<String> imageUrls;
  const PhotoGridPage({super.key, required this.imageUrls});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Photo Grid')),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,       // 3 columns
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
          childAspectRatio: 1,     // square cells
        ),
        itemCount: imageUrls.length,
        itemBuilder: (context, index) {
          return ClipRRect(
            borderRadius: BorderRadius.circular(8),
            child: Image.network(
              imageUrls[index],
              fit: BoxFit.cover,
              loadingBuilder: (_, child, progress) => progress == null
                  ? child
                  : const Center(child: CircularProgressIndicator()),
            ),
          );
        },
      ),
    );
  }
}

RefreshIndicator (Pull to Refresh)

Wrap a scrollable with RefreshIndicator and provide an onRefresh async callback. The callback must return a Future.

Dart
class RefreshableList extends StatefulWidget {
  const RefreshableList({super.key});
  @override
  State<RefreshableList> createState() => _RefreshableListState();
}

class _RefreshableListState extends State<RefreshableList> {
  List<String> _items = ['Item A', 'Item B', 'Item C'];

  Future<void> _onRefresh() async {
    // Simulate a network call
    await Future.delayed(const Duration(seconds: 2));
    setState(() {
      _items = ['Fresh Item 1', 'Fresh Item 2', 'Fresh Item 3', 'Fresh Item 4'];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Pull to Refresh')),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        color: Colors.blue,
        backgroundColor: Colors.white,
        child: ListView.builder(
          // Important: always use physics that allows overscroll
          physics: const AlwaysScrollableScrollPhysics(),
          itemCount: _items.length,
          itemBuilder: (_, index) => ListTile(
            title: Text(_items[index]),
            leading: const Icon(Icons.circle, size: 12),
          ),
        ),
      ),
    );
  }
}

Infinite Scroll with ScrollController

Attach a ScrollController and listen for when the user reaches near the bottom of the list to automatically load the next page.

Dart
class InfiniteScrollList extends StatefulWidget {
  const InfiniteScrollList({super.key});
  @override
  State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}

class _InfiniteScrollListState extends State<InfiniteScrollList> {
  final ScrollController _scrollController = ScrollController();
  final List<String> _items = List.generate(20, (i) => 'Item ${i + 1}');
  bool _isLoading = false;
  int _page = 1;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.position.pixels;
    // Trigger load when 200px from bottom
    if (currentScroll >= maxScroll - 200 && !_isLoading) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 1)); // simulate API
    _page++;
    final newItems = List.generate(10, (i) => 'Page $_page – Item ${i + 1}');
    setState(() {
      _items.addAll(newItems);
      _isLoading = false;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Infinite Scroll')),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _items.length + (_isLoading ? 1 : 0),
        itemBuilder: (_, index) {
          if (index == _items.length) {
            return const Padding(
              padding: EdgeInsets.all(16),
              child: Center(child: CircularProgressIndicator()),
            );
          }
          return ListTile(title: Text(_items[index]));
        },
      ),
    );
  }
}

ListTile Widget

ListTile is a convenient Material widget for row-based lists. It supports leading/trailing icons, title, subtitle, and tap/long-press events.

Dart
// Basic ListTile
ListTile(
  leading: const CircleAvatar(
    backgroundImage: NetworkImage('https://i.pravatar.cc/150'),
  ),
  title: const Text('John Doe'),
  subtitle: const Text('john@example.com'),
  trailing: const Icon(Icons.arrow_forward_ios, size: 16),
  onTap: () => print('Tapped'),
  onLongPress: () => print('Long pressed'),
)

// ListTile with checkbox (CheckboxListTile)
CheckboxListTile(
  value: true,
  onChanged: (val) {},
  title: const Text('Enable Notifications'),
  subtitle: const Text('Receive push alerts'),
  secondary: const Icon(Icons.notifications),
)

// ListTile with switch (SwitchListTile)
SwitchListTile(
  value: false,
  onChanged: (val) {},
  title: const Text('Dark Mode'),
  subtitle: const Text('Switch app theme'),
)

// Divider between tiles
const Divider(height: 1, thickness: 1, indent: 72)