Answer
Overview
A list with persistent headers (sticky headers) can be implemented using Slivers in Flutter — specifically
text
SliverListtext
SliverPersistentHeadertext
CustomScrollViewUsing SliverList + SliverPersistentHeader
dartclass StickyHeaderList extends StatelessWidget { final Map<String, List<String>> sections = { 'A': ['Alice', 'Aaron', 'Amy'], 'B': ['Bob', 'Brian', 'Betty'], 'C': ['Carol', 'Chris', 'Clara'], }; Widget build(BuildContext context) { return CustomScrollView( slivers: [ for (final entry in sections.entries) ...[ // Sticky Header SliverPersistentHeader( pinned: true, delegate: _SectionHeaderDelegate(entry.key), ), // Items under this header SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile(title: Text(entry.value[index])), childCount: entry.value.length, ), ), ], ], ); } } class _SectionHeaderDelegate extends SliverPersistentHeaderDelegate { final String title; _SectionHeaderDelegate(this.title); Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( color: Colors.blue[100], alignment: Alignment.centerLeft, padding: EdgeInsets.symmetric(horizontal: 16), child: Text( title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), ); } double get maxExtent => 48; double get minExtent => 48; bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => false; }
Using sticky_headers Package (Easier)
dartimport 'package:sticky_headers/sticky_headers.dart'; ListView.builder( itemCount: sections.length, itemBuilder: (context, index) { final section = sections[index]; return StickyHeader( header: Container( height: 50, color: Colors.blueGrey[700], alignment: Alignment.centerLeft, padding: EdgeInsets.symmetric(horizontal: 16), child: Text(section.title, style: TextStyle(color: Colors.white)), ), content: Column( children: section.items.map((item) => ListTile(title: Text(item))).toList(), ), ); }, )
Pinned vs Floating Header
dartSliverPersistentHeader( pinned: true, // Header stays at top while scrolling past it floating: false, // Header comes back only when scrolled to its position delegate: MyHeaderDelegate(), )
| Option | Behavior |
|---|---|
text | Sticks at top of viewport |
text | Reappears when scrolling back up |
text | Sticks + reappears quickly |
Recommendation: For most contact-list or alphabetical list UIs, use
withtextpinned: true. For a simpler solution, use thetextSliverPersistentHeaderpackage.textsticky_headers