From 5b83a62a4f1c2b6da1439ebd2048a52996dd4029 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 31 Jul 2026 06:59:40 +0200 Subject: [PATCH] improve bottom sheet fps --- .../widgets/browser_menu_sheet.dart | 83 +++--- .../widgets/draggable_scrollable_header.dart | 48 +--- .../presentation/widgets/sheets/view_tab.dart | 77 +++--- .../widgets/tab_view/tab_grid_view.dart | 183 ++++++------- .../widgets/tab_view/tab_list_view.dart | 251 +++++++++--------- .../widgets/tab_view/tab_tree_view.dart | 29 +- 6 files changed, 299 insertions(+), 372 deletions(-) diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index e0c8ff6c..02287362 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -21,7 +21,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:fading_scroll/fading_scroll.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -134,59 +133,51 @@ class _BrowserMenuSheet extends HookConsumerWidget { // Scrollable content Expanded( - child: FadingScroll( + child: ListView( controller: scrollController, - fadingSize: 25, - builder: (context, controller) { - return ListView( - controller: controller, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - children: [ - // Quick toggles (Desktop / Reader / Gestures) - if (selectedTabId != null) ...[ - _QuickTogglesGrid(selectedTabId: selectedTabId), - const SizedBox(height: 16), - ], + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + children: [ + // Quick toggles (Desktop / Reader / Gestures) + if (selectedTabId != null) ...[ + _QuickTogglesGrid(selectedTabId: selectedTabId), + const SizedBox(height: 16), + ], - // Page actions - if (selectedTabId != null) ...[ - _PageActionsCard(selectedTabId: selectedTabId), - const SizedBox(height: 16), - ], + // Page actions + if (selectedTabId != null) ...[ + _PageActionsCard(selectedTabId: selectedTabId), + const SizedBox(height: 16), + ], - // Extensions - _ExtensionsCard(), - const SizedBox(height: 16), + // Extensions + _ExtensionsCard(), + const SizedBox(height: 16), - // Tab actions - if (selectedTabId != null) ...[ - _TabActionsCard(selectedTabId: selectedTabId), - const SizedBox(height: 16), - ], + // Tab actions + if (selectedTabId != null) ...[ + _TabActionsCard(selectedTabId: selectedTabId), + const SizedBox(height: 16), + ], - // Quick links grid - _QuickLinksGrid( - showContainerUi: settings.showContainerUi, - ), - const SizedBox(height: 16), + // Quick links grid + _QuickLinksGrid(showContainerUi: settings.showContainerUi), + const SizedBox(height: 16), - // Connection (Tor + proxy profiles) - const _ConnectionCard(), - const SizedBox(height: 16), + // Connection (Tor + proxy profiles) + const _ConnectionCard(), + const SizedBox(height: 16), - // Profile - _ProfileCard(), - const SizedBox(height: 16), + // Profile + _ProfileCard(), + const SizedBox(height: 16), - // App - const _SettingsCard(), - const SizedBox(height: 24), - ], - ); - }, + // App + const _SettingsCard(), + const SizedBox(height: 24), + ], ), ), diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart index 979c2d10..cbc52b14 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart @@ -50,26 +50,7 @@ class DraggableScrollableHeader extends StatefulWidget { _DraggableScrollableHeaderState(); } -class _DraggableScrollableHeaderState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation? _animation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: widget.animationDuration, - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - +class _DraggableScrollableHeaderState extends State { Future _animateToPosition( double targetSize, { Duration? customDuration, @@ -79,34 +60,17 @@ class _DraggableScrollableHeaderState extends State return; } - if (customDuration != null) { - _animationController.duration = customDuration; - } else { - _animationController.duration = widget.animationDuration; - } - - _animation = Tween(begin: widget.controller.size, end: targetSize) - .animate( - CurvedAnimation( - parent: _animationController, - curve: widget.animationCurve, - ), - ); - - _animationController.reset(); - await _animationController.forward(); - - _animation!.addListener(() { - widget.controller.jumpTo(_animation!.value); - }); + await widget.controller.animateTo( + targetSize, + duration: customDuration ?? widget.animationDuration, + curve: widget.animationCurve, + ); } @override Widget build(BuildContext context) { return GestureDetector( onVerticalDragUpdate: (details) { - _animationController.stop(); - // Apply drag sensitivity final adjustedDelta = details.delta.dy * widget.dragSensitivity; diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index dd79dd8e..8492d864 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -18,7 +18,6 @@ * along with this program. If not, see . */ -import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -144,48 +143,42 @@ class ViewTabSheetWidget extends HookConsumerWidget { ), ), ], - body: FadingScroll( - fadingSize: 15, + body: ListView( + padding: EdgeInsets.zero, controller: sheetScrollController, - builder: (context, controller) { - return ListView( - padding: EdgeInsets.zero, - controller: controller, - physics: const ClampingScrollPhysicsWithoutImplicit(), - children: [ - // Tracking Protection Section - TrackingProtectionSection(tabId: initialTabState.id), - const Divider(), - // Gesture Exclusion Section - GestureExclusionSection(url: initialTabState.url), - const Divider(), - // Desktop Mode Section - DesktopModeSection( - tabId: initialTabState.id, - url: initialTabState.url, - ), - const Divider(), - // App Link Section - AppLinkSection( - url: initialTabState.url, - contextId: initialTabState.contextId, - ), - const Divider(), - // Permissions Section - PermissionsSection( - origin: initialTabState.url.origin, - isPrivate: initialTabState.tabMode is PrivateTabMode, - ), - const Divider(), - // Clear Site Data Section - ClearSiteDataSection( - url: initialTabState.url, - onExpandedChanged: onClearSiteDataExpandedChanged, - ), - const SizedBox(height: 16.0), - ], - ); - }, + physics: const ClampingScrollPhysicsWithoutImplicit(), + children: [ + // Tracking Protection Section + TrackingProtectionSection(tabId: initialTabState.id), + const Divider(), + // Gesture Exclusion Section + GestureExclusionSection(url: initialTabState.url), + const Divider(), + // Desktop Mode Section + DesktopModeSection( + tabId: initialTabState.id, + url: initialTabState.url, + ), + const Divider(), + // App Link Section + AppLinkSection( + url: initialTabState.url, + contextId: initialTabState.contextId, + ), + const Divider(), + // Permissions Section + PermissionsSection( + origin: initialTabState.url.origin, + isPrivate: initialTabState.tabMode is PrivateTabMode, + ), + const Divider(), + // Clear Site Data Section + ClearSiteDataSection( + url: initialTabState.url, + onExpandedChanged: onClearSiteDataExpandedChanged, + ), + const SizedBox(height: 16.0), + ], ), ); } diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart index eee504b0..0bfe9080 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart @@ -20,7 +20,6 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:fading_scroll/fading_scroll.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -328,113 +327,107 @@ class _TabGridView extends HookConsumerWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FadingScroll( - fadingSize: 5, - controller: scrollController, - builder: (context, controller) { - return !reorderEnabled - ? _TabGrid( - key: ValueKey(crossAxisCount), + child: !reorderEnabled + ? _TabGrid( + key: ValueKey(crossAxisCount), + crossAxisCount: crossAxisCount, + itemCount: displayItemCount, + scrollController: scrollController, + itemBuilder: (widget, _) { + if (widget is CustomDraggable) { + if (widget.data case final TabDragData dragData) { + return TabContextMenuDraggable( + tabId: dragData.tabId, + data: dragData, + feedbackSize: itemSize, + child: widget.child, + ); + } + } + + return widget; + }, + suggestedContainerId: containerId, + primaryRows: primaryRows, + suggestedTabEntities: suggestedTabEntities, + onClose: onClose, + ) + : ReorderableBuilder.builder( + //Rebuild when cross axis count changes + key: ValueKey(crossAxisCount), + scrollController: scrollController, + itemCount: displayItemCount, + onDragStarted: (index) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onReorderPositions: (positions) async { + assert( + positions.length == 1, + 'Not ready for multiple reorders', + ); + + final oldIndex = positions.first.oldIndex; + final newIndex = positions.first.newIndex; + + //Suggestions are at the end and not reorderable, so skip + if (oldIndex >= primaryRows.length) { + return; + } + + final result = buildTabViewReorderResult( + visibleItems: primaryRows, + treeRows: treeRows, + collapsedGroups: collapsedGroups, + pinnedTabIds: pinnedTabIds, + oldIndex: oldIndex, + newIndex: newIndex, + tabListDirection: tabListDirection, + hierarchical: showHierarchicalTabs && !hasActiveSearch, + sortPinnedFirst: filterOptions.sortPinnedFirst, + ); + + if (result == null) return; + + await ref + .read(tabDataRepositoryProvider.notifier) + .reorderTabs( + movingTabIds: result.movingTabIds, + previousTabId: result.previousTabId, + nextTabId: result.nextTabId, + parentChange: result.parentChange, + ); + }, + childBuilder: (reorderableItemBuilder) { + return _TabGrid( crossAxisCount: crossAxisCount, itemCount: displayItemCount, - scrollController: controller, - itemBuilder: (widget, _) { + scrollController: scrollController, + itemBuilder: (widget, index) { + // Wrap with context menu before passing to reorderable + Widget wrapped = widget; if (widget is CustomDraggable) { if (widget.data case final TabDragData dragData) { - return TabContextMenuDraggable( - tabId: dragData.tabId, - data: dragData, - feedbackSize: itemSize, - child: widget.child, + wrapped = CustomDraggable( + key: widget.key!, + data: widget.data, + child: TabContextMenuDraggable( + tabId: dragData.tabId, + feedbackSize: Size.zero, + externalDrag: true, + child: widget.child, + ), ); } } - - return widget; + return reorderableItemBuilder(wrapped, index); }, suggestedContainerId: containerId, primaryRows: primaryRows, suggestedTabEntities: suggestedTabEntities, onClose: onClose, - ) - : ReorderableBuilder.builder( - //Rebuild when cross axis count changes - key: ValueKey(crossAxisCount), - scrollController: controller, - itemCount: displayItemCount, - onDragStarted: (index) { - ref.read(willAcceptDropProvider.notifier).clear(); - }, - onReorderPositions: (positions) async { - assert( - positions.length == 1, - 'Not ready for multiple reorders', - ); - - final oldIndex = positions.first.oldIndex; - final newIndex = positions.first.newIndex; - - //Suggestions are at the end and not reorderable, so skip - if (oldIndex >= primaryRows.length) { - return; - } - - final result = buildTabViewReorderResult( - visibleItems: primaryRows, - treeRows: treeRows, - collapsedGroups: collapsedGroups, - pinnedTabIds: pinnedTabIds, - oldIndex: oldIndex, - newIndex: newIndex, - tabListDirection: tabListDirection, - hierarchical: showHierarchicalTabs && !hasActiveSearch, - sortPinnedFirst: filterOptions.sortPinnedFirst, - ); - - if (result == null) return; - - await ref - .read(tabDataRepositoryProvider.notifier) - .reorderTabs( - movingTabIds: result.movingTabIds, - previousTabId: result.previousTabId, - nextTabId: result.nextTabId, - parentChange: result.parentChange, - ); - }, - childBuilder: (reorderableItemBuilder) { - return _TabGrid( - crossAxisCount: crossAxisCount, - itemCount: displayItemCount, - scrollController: controller, - itemBuilder: (widget, index) { - // Wrap with context menu before passing to reorderable - Widget wrapped = widget; - if (widget is CustomDraggable) { - if (widget.data case final TabDragData dragData) { - wrapped = CustomDraggable( - key: widget.key!, - data: widget.data, - child: TabContextMenuDraggable( - tabId: dragData.tabId, - feedbackSize: Size.zero, - externalDrag: true, - child: widget.child, - ), - ); - } - } - return reorderableItemBuilder(wrapped, index); - }, - suggestedContainerId: containerId, - primaryRows: primaryRows, - suggestedTabEntities: suggestedTabEntities, - onClose: onClose, - ); - }, ); - }, - ), + }, + ), ); } } diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart index f8a9225a..ab00a5f2 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart @@ -20,7 +20,6 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:fading_scroll/fading_scroll.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -366,139 +365,133 @@ class _TabListView extends HookConsumerWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FadingScroll( - fadingSize: 5, - controller: scrollController, - builder: (context, controller) { - return !reorderEnabled - ? ListView.builder( - padding: const EdgeInsets.only(bottom: 56), - controller: scrollController, - itemCount: displayItemCount, - itemExtent: _itemHeight, - itemBuilder: (context, index) { - if (index < primaryRows.length) { - final row = primaryRows[index]; - final tab = CustomDraggable( - key: Key(row.tabId), - data: TabDragData(row.tabId), - child: _TabDraggable( - tabId: row.tabId, - onClose: onClose, - sourceSearchQuery: row.sourceSearchQuery, - height: _itemHeight, - groupToggle: _listGroupToggleFor(row), - depth: _depthFor(row), - ), - ); + child: !reorderEnabled + ? ListView.builder( + padding: const EdgeInsets.only(bottom: 56), + controller: scrollController, + itemCount: displayItemCount, + itemExtent: _itemHeight, + itemBuilder: (context, index) { + if (index < primaryRows.length) { + final row = primaryRows[index]; + final tab = CustomDraggable( + key: Key(row.tabId), + data: TabDragData(row.tabId), + child: _TabDraggable( + tabId: row.tabId, + onClose: onClose, + sourceSearchQuery: row.sourceSearchQuery, + height: _itemHeight, + groupToggle: _listGroupToggleFor(row), + depth: _depthFor(row), + ), + ); - return TabDropTarget( - targetTabId: row.tabId, - child: TabContextMenuDraggable( - tabId: row.tabId, - data: tab.data! as TabDragData, - feedbackSize: Size( - MediaQuery.of(context).size.width, - _itemHeight, - ), - child: tab.child, - ), - ); - } - - final suggestedIndex = index - primaryRows.length; - final entity = suggestedTabEntities.value[suggestedIndex]; - final tab = CustomDraggable( - key: Key('suggested_${entity.tabId}'), - child: _TabDraggable( - tabId: entity.tabId, - onClose: onClose, - suggestedContainerId: containerId, - height: _itemHeight, + return TabDropTarget( + targetTabId: row.tabId, + child: TabContextMenuDraggable( + tabId: row.tabId, + data: tab.data! as TabDragData, + feedbackSize: Size( + MediaQuery.of(context).size.width, + _itemHeight, ), - ); - return TabDropTarget( - targetTabId: entity.tabId, - enabled: false, child: tab.child, - ); - }, - ) - : ReorderableListView.builder( - scrollController: controller, - padding: const EdgeInsets.only(bottom: 56), - itemCount: displayItemCount, - itemExtent: _itemHeight, - onReorderStart: (index) { - ref.read(willAcceptDropProvider.notifier).clear(); - }, - onReorderItem: (oldIndex, newIndex) async { - //Suggestions are at the end and not reorderable, so skip - if (oldIndex >= primaryRows.length) { - return; - } + ), + ); + } - final result = buildTabViewReorderResult( - visibleItems: primaryRows, - treeRows: treeRows, - collapsedGroups: collapsedGroups, - pinnedTabIds: pinnedTabIds, - oldIndex: oldIndex, - newIndex: newIndex, - tabListDirection: tabListDirection, - hierarchical: showHierarchicalTabs && !hasActiveSearch, - sortPinnedFirst: filterOptions.sortPinnedFirst, - ); - - if (result == null) return; - - await ref - .read(tabDataRepositoryProvider.notifier) - .reorderTabs( - movingTabIds: result.movingTabIds, - previousTabId: result.previousTabId, - nextTabId: result.nextTabId, - parentChange: result.parentChange, - ); - }, - itemBuilder: (context, index) { - if (index < primaryRows.length) { - final row = primaryRows[index]; - return CustomDraggable( - key: Key(row.tabId), - data: TabDragData(row.tabId), - child: TabContextMenuDraggable( - tabId: row.tabId, - feedbackSize: Size.zero, - externalDrag: true, - child: _TabDraggable( - tabId: row.tabId, - onClose: onClose, - sourceSearchQuery: row.sourceSearchQuery, - height: _itemHeight, - groupToggle: _listGroupToggleFor(row), - depth: _depthFor(row), - ), - ), - ); - } else { - final suggestedIndex = index - primaryRows.length; - final entity = suggestedTabEntities.value[suggestedIndex]; - - return CustomDraggable( - key: Key('suggested_${entity.tabId}'), - child: _TabDraggable( - tabId: entity.tabId, - onClose: onClose, - suggestedContainerId: containerId, - height: _itemHeight, - ), - ); - } - }, + final suggestedIndex = index - primaryRows.length; + final entity = suggestedTabEntities.value[suggestedIndex]; + final tab = CustomDraggable( + key: Key('suggested_${entity.tabId}'), + child: _TabDraggable( + tabId: entity.tabId, + onClose: onClose, + suggestedContainerId: containerId, + height: _itemHeight, + ), ); - }, - ), + return TabDropTarget( + targetTabId: entity.tabId, + enabled: false, + child: tab.child, + ); + }, + ) + : ReorderableListView.builder( + scrollController: scrollController, + padding: const EdgeInsets.only(bottom: 56), + itemCount: displayItemCount, + itemExtent: _itemHeight, + onReorderStart: (index) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onReorderItem: (oldIndex, newIndex) async { + //Suggestions are at the end and not reorderable, so skip + if (oldIndex >= primaryRows.length) { + return; + } + + final result = buildTabViewReorderResult( + visibleItems: primaryRows, + treeRows: treeRows, + collapsedGroups: collapsedGroups, + pinnedTabIds: pinnedTabIds, + oldIndex: oldIndex, + newIndex: newIndex, + tabListDirection: tabListDirection, + hierarchical: showHierarchicalTabs && !hasActiveSearch, + sortPinnedFirst: filterOptions.sortPinnedFirst, + ); + + if (result == null) return; + + await ref + .read(tabDataRepositoryProvider.notifier) + .reorderTabs( + movingTabIds: result.movingTabIds, + previousTabId: result.previousTabId, + nextTabId: result.nextTabId, + parentChange: result.parentChange, + ); + }, + itemBuilder: (context, index) { + if (index < primaryRows.length) { + final row = primaryRows[index]; + return CustomDraggable( + key: Key(row.tabId), + data: TabDragData(row.tabId), + child: TabContextMenuDraggable( + tabId: row.tabId, + feedbackSize: Size.zero, + externalDrag: true, + child: _TabDraggable( + tabId: row.tabId, + onClose: onClose, + sourceSearchQuery: row.sourceSearchQuery, + height: _itemHeight, + groupToggle: _listGroupToggleFor(row), + depth: _depthFor(row), + ), + ), + ); + } else { + final suggestedIndex = index - primaryRows.length; + final entity = suggestedTabEntities.value[suggestedIndex]; + + return CustomDraggable( + key: Key('suggested_${entity.tabId}'), + child: _TabDraggable( + tabId: entity.tabId, + onClose: onClose, + suggestedContainerId: containerId, + height: _itemHeight, + ), + ); + } + }, + ), ); } } diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart index 8750cabe..584ae03f 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart @@ -20,7 +20,6 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -335,24 +334,18 @@ class _TabTreesGrid extends HookConsumerWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FadingScroll( + child: GridView.builder( controller: scrollController, - fadingSize: 5, - builder: (context, controller) { - return GridView.builder( - controller: controller, - padding: const EdgeInsets.only(bottom: 56), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - //Sync values for itemHeight calculation _calculateItemHeight - childAspectRatio: 0.75, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: crossAxisCount, - ), - itemCount: tabs.length, - itemBuilder: (context, index) => tabs[index], - ); - }, + padding: const EdgeInsets.only(bottom: 56), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //Sync values for itemHeight calculation _calculateItemHeight + childAspectRatio: 0.75, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: crossAxisCount, + ), + itemCount: tabs.length, + itemBuilder: (context, index) => tabs[index], ), ); }