Irbisa · Шпаргалка
Sliver-ы и скроллинг
CustomScrollView, SliverAppBar, ScrollController, physics, NestedScrollView, keep-alive
Что такое slivers и когда экран стоит строить на
CustomScrollView, а не наListView?Что произойдёт при раскладке этого
CustomScrollViewи как это исправить?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]), ), ],)Как добавить pull-to-refresh и почему
RefreshIndicatorиногда не срабатывает, когда список короче экрана?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,…Как
SliverAppBarстроит схлопывающийся заголовок: за что отвечаютpinned,floating,snap,expandedHeightиFlexibleSpaceBar?// inside DefaultTabController(length: 2, child: Scaffold(body: ...))CustomScrollView( slivers: [ SliverAppBar( pinned: true, // toolbar + TabBar stay on screen floating: true, // reappears on any upward scroll…Как
SliverPersistentHeaderсжимает заголовок секции со 180 px до 56 px и держит его закреплённым, и что решаетshouldRebuild?class _SectionHeader extends SliverPersistentHeaderDelegate { const _SectionHeader({required this.title, required this.unread}); final String title; final int unread; @override…Что делает
shrinkWrap: true, почему он бьёт по производительности и как правильно положить список внутрь другого скролла?// 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(…Как пользоваться
ScrollController(слушать смещение,jumpTo/animateTo, запускать догрузку) и почему чтениеoffsetвinitStateпадает?class _FeedState extends State<Feed> { final _scroll = ScrollController(); @override void initState() { super.initState();…Что не так с этим кодом бесконечного скролла и почему он запрашивает одну и ту же страницу несколько раз?
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]), ); }}Найдите баг: после отправки сообщения самое новое сообщение остаётся чуть ниже видимой области.
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]), );Когда использовать
NotificationListener<ScrollNotification>вместоScrollControllerи почему вложенная карусель запускает догрузку ленты?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(); }…Найдите баг: на планшетах лента показывает первую страницу из 10 постов и не грузит следующую; на телефонах пагинация работает.
@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]), );За что отвечают
ScrollPhysics(Bouncing,Clamping,NeverScrollable,AlwaysScrollable) и как поменять ощущение скролла во всём приложении?// 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,…Сколько строк за пределами видимой области на самом деле строит
ListView.builderи что это значит для строки, которая делает запрос вinitState?Как прокрутить ленивый список к элементу 500, если эта строка ещё ни разу не строилась?
const rowHeight = 72.0; ListView.builder( controller: _scroll, itemExtent: rowHeight, // offset of row i is i * rowHeight itemCount: items.length,…В этом
ReorderableListViewстрока, перетянутая вниз, встаёт на слот ниже точки броска, а стейт строк оказывается у чужих задач. Какие тут два бага?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], ),)Как
AutomaticKeepAliveClientMixinсохраняет строку списка живой после того, как она уехала за экран, как он тихо ломается и чего это стоит?class _ClipRowState extends State<ClipRow> with AutomaticKeepAliveClientMixin<ClipRow> { late final VideoPlayerController _video = VideoPlayerController.networkUrl(Uri.parse(widget.clip.url)) ..initialize();…Когда нужен
NestedScrollViewи почему первые строки каждой вкладки прячутся за его закреплённымSliverAppBar?// inside DefaultTabController(length: 2, child: Scaffold(body: ...))NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar(…Переключение вкладок сбрасывает список наверх, и после того как Android убил приложение в фоне, оно тоже открывается наверху. Что чинит каждый случай?
// 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]),…Десять тысяч строк разной высоты: ползунок скроллбара всё время меняет размер, а
jumpTo(maxScrollExtent)не доезжает до конца. Почему?// 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,…Чат подгружает старые сообщения вверх, а новые приходят вниз, и каждая страница истории сдвигает ленту на целый экран. Как удержать якорь на месте?
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 {…В таблице первая колонка должна быть закреплена, остальное скроллится, и половинки не должны разъехаться ни на пиксель. Как их связать?
class _PinnedTableState extends State<PinnedTable> { final _left = ScrollController(); final _right = ScrollController(); bool _syncing = false; @override…Кастомный
RenderSliverрисуется где надо, но следующий sliver начинается слишком низко. Что обязан выставитьperformLayoutи когда хватаетSliverLayoutBuilder?// 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;…