Irbisa · Cheatsheet
Slivers & Scrolling
CustomScrollView, SliverAppBar, ScrollController, ScrollPhysics, NestedScrollView, keep-alives
What are slivers, and when do you build a screen with
CustomScrollViewinstead of aListView?What happens when this
CustomScrollViewis laid out, and how do you fix it?CustomScrollView( slivers: [ const SliverAppBar(title: Text('Orders'), pinned: true), const Padding( padding: EdgeInsets.all(16), child: Text('Recent orders'), ), ListView.builder( itemCount: orders.length, itemBuilder: (_, i) => OrderTile(order: orders[i]), ), ],)How do you add pull-to-refresh, and why does
RefreshIndicatorsometimes not trigger when the list is shorter than the screen?RefreshIndicator( onRefresh: () => feed.reload(), // spinner stays until this completes child: ListView.builder( controller: _scroll, // own controller: no longer primary, physics: const AlwaysScrollableScrollPhysics(), // so restore the drag itemCount: posts.length,…How does
SliverAppBarbuild a collapsing header: what dopinned,floating,snap,expandedHeightandFlexibleSpaceBareach do?// inside DefaultTabController(length: 2, child: Scaffold(body: ...))CustomScrollView( slivers: [ SliverAppBar( pinned: true, // toolbar + TabBar stay on screen floating: true, // reappears on any upward scroll…How does
SliverPersistentHeadershrink a section header from 180 px to 56 px and keep it pinned, and what doesshouldRebuilddecide?class _SectionHeader extends SliverPersistentHeaderDelegate { const _SectionHeader({required this.title, required this.unread}); final String title; final int unread; @override…What does
shrinkWrap: truedo, why is it a performance smell, and how do you put a list inside another scrollable correctly?// wrong: every order is built and laid out in one frame, plus a second// Scrollable that never scrollsSingleChildScrollView( child: Column(children: [ const SummaryCard(), ListView.builder(…How do you use a
ScrollController(listen to the offset,jumpTo/animateTo, trigger load-more), and why does readingoffsetininitStatethrow?class _FeedState extends State<Feed> { final _scroll = ScrollController(); @override void initState() { super.initState();…What is wrong with this infinite-scroll code, and why does it request the same page several times?
class _FeedState extends State<Feed> { @override Widget build(BuildContext context) { final controller = ScrollController(); controller.addListener(() { if (controller.position.extentAfter < 500) { widget.onLoadMore(); } }); return ListView.builder( controller: controller, itemCount: widget.items.length, itemBuilder: (_, i) => ItemTile(item: widget.items[i]), ); }}Find the bug: after the user sends a message, the newest message is hidden just below the visible area.
void _send(Message m) { setState(() => _messages.add(m)); _scroll.animateTo( _scroll.position.maxScrollExtent, duration: const Duration(milliseconds: 250), curve: Curves.easeOut, );} @overrideWidget build(BuildContext context) => ListView.builder( controller: _scroll, itemCount: _messages.length, itemBuilder: (_, i) => Bubble(message: _messages[i]), );When do you use
NotificationListener<ScrollNotification>instead of aScrollController, and why does a nested carousel trigger your feed's load-more?bool _onScroll(ScrollNotification n) { // wrong without this line: the carousel below also triggers load-more if (n.depth != 0) return false; if (n is ScrollUpdateNotification && n.metrics.extentAfter < 600) { loader.loadNextPage(); }…Find the bug: on tablets the feed shows the first page of 10 posts and never loads the next one; on phones paging works.
@overridevoid initState() { super.initState(); _scroll.addListener(_onScroll); // the parent has already loaded page 1} void _onScroll() { if (_scroll.position.extentAfter < 400) widget.onLoadMore(); // guarded} @overrideWidget build(BuildContext context) => ListView.builder( controller: _scroll, itemCount: widget.posts.length, itemBuilder: (_, i) => PostTile(post: widget.posts[i]), );What do
ScrollPhysicscontrol (Bouncing,Clamping,NeverScrollable,AlwaysScrollable), and how do you change the scroll feel for the whole app?// Layered on top of the platform default, not instead of it.ListView.builder( physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), // still draggable when short ), itemCount: items.length,…How many rows beyond the visible window does
ListView.builderactually build, and what does that mean for a row that fires a request ininitState?How do you scroll a lazy list to item 500 when that row has never been built?
const rowHeight = 72.0; ListView.builder( controller: _scroll, itemExtent: rowHeight, // offset of row i is i * rowHeight itemCount: items.length,…In this
ReorderableListView, a row dragged downward lands one slot below the drop point, and row state ends up on the wrong task. What are the two bugs?ReorderableListView.builder( itemCount: tasks.length, onReorder: (oldIndex, newIndex) { setState(() { final task = tasks.removeAt(oldIndex); tasks.insert(newIndex, task); }); }, itemBuilder: (context, i) => TaskTile( key: ValueKey(i), task: tasks[i], ),)How does
AutomaticKeepAliveClientMixinkeep a list row alive after it scrolls away, how does it fail silently, and what does it cost?class _ClipRowState extends State<ClipRow> with AutomaticKeepAliveClientMixin<ClipRow> { late final VideoPlayerController _video = VideoPlayerController.networkUrl(Uri.parse(widget.clip.url)) ..initialize();…When do you need
NestedScrollView, and why do the first rows of each tab hide behind its pinnedSliverAppBar?// inside DefaultTabController(length: 2, child: Scaffold(body: ...))NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar(…Switching tabs resets the list to the top, and after Android kills the app in the background it also starts at the top. What fixes each case?
// 1. Tabs: an in-memory offset per tab, for the life of the routeTabBarView(children: [ ListView.builder( key: const PageStorageKey<String>('tab.inbox'), // unique per tab itemCount: inbox.length, itemBuilder: (_, i) => MailTile(mail: inbox[i]),…Ten thousand rows of varying height: the scrollbar thumb keeps resizing as you scroll and
jumpTo(maxScrollExtent)never reaches the end. Why?// height is a pure function of the data: no widget needed to know itdouble extentOf(Msg m) => 16 + m.lines * 22.0; // wrong: heights are discovered by building, maxScrollExtent is an estimateListView.builder( itemCount: msgs.length,…A chat loads older messages upward and new ones downward, and every history page shoves the thread down a screenful. How do you keep the anchor still?
class _ChatState extends State<Chat> { static const _centerKey = ValueKey<String>('live'); final List<Message> _older = []; // index 0 = nearest the anchor final List<Message> _live = []; // index 0 = the anchor, grows downward Future<void> _loadHistory() async {…A table needs its first column pinned while the rest scrolls, and the two halves must never drift a pixel apart. How do you link them?
class _PinnedTableState extends State<PinnedTable> { final _left = ScrollController(); final _right = ScrollController(); bool _syncing = false; @override…A custom
RenderSliverpaints in the right place but the next sliver starts too low. What mustperformLayoutset, and when isSliverLayoutBuilderenough?// Reserves 200 px of scroll but pins its last 56 px:// the three extents deliberately disagree.class RenderPinnedBanner extends RenderSliver { static const double _full = 200; static const double _pinned = 56;…