From ff6aef1e82a3911266f2e647a2f09dc60a6c5e0a Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 14 Mar 2021 12:22:01 +0900 Subject: [PATCH] rebuild performance review --- ...l_collection.dart => collection_grid.dart} | 169 ++++++++------ lib/widgets/collection/collection_page.dart | 11 +- lib/widgets/filter_grids/album_pick.dart | 6 +- .../filter_grids/common/filter_grid_page.dart | 217 +++++++++++++----- 4 files changed, 268 insertions(+), 135 deletions(-) rename lib/widgets/collection/{thumbnail_collection.dart => collection_grid.dart} (79%) diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/collection_grid.dart similarity index 79% rename from lib/widgets/collection/thumbnail_collection.dart rename to lib/widgets/collection/collection_grid.dart index 702195288..cc7771cb1 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -32,12 +32,12 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -class ThumbnailCollection extends StatefulWidget { +class CollectionGrid extends StatefulWidget { @override - _ThumbnailCollectionState createState() => _ThumbnailCollectionState(); + _CollectionGridState createState() => _CollectionGridState(); } -class _ThumbnailCollectionState extends State { +class _CollectionGridState extends State { TileExtentController _tileExtentController; @override @@ -48,54 +48,20 @@ class _ThumbnailCollectionState extends State { extentMin: 46, spacing: 0, ); - return SafeArea( - bottom: false, - child: TileExtentControllerProvider( - controller: _tileExtentController, - child: _ThumbnailCollectionContent(), - ), + return TileExtentControllerProvider( + controller: _tileExtentController, + child: _CollectionGridContent(), ); } } -class _ThumbnailCollectionContent extends StatelessWidget { - final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); +class _CollectionGridContent extends StatelessWidget { final ValueNotifier _isScrollingNotifier = ValueNotifier(false); - final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); @override Widget build(BuildContext context) { - final scrollController = PrimaryScrollController.of(context); return Consumer( builder: (context, collection, child) { - final scrollView = AnimationLimiter( - child: CollectionScrollView( - scrollableKey: _scrollableKey, - collection: collection, - appBar: CollectionAppBar( - appBarHeightNotifier: _appBarHeightNotifier, - collection: collection, - ), - appBarHeightNotifier: _appBarHeightNotifier, - isScrollingNotifier: _isScrollingNotifier, - scrollController: scrollController, - ), - ); - - final scaler = _ThumbnailGridScaleGestureDetector( - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, - child: scrollView, - ); - - final selector = GridSelectionGestureDetector( - selectable: AvesApp.mode == AppMode.main, - collection: collection, - scrollController: scrollController, - appBarHeightNotifier: _appBarHeightNotifier, - child: scaler, - ); - final sectionedListLayoutProvider = ValueListenableBuilder( valueListenable: context.select>((controller) => controller.extentNotifier), builder: (context, tileExtent, child) { @@ -111,7 +77,11 @@ class _ThumbnailCollectionContent extends StatelessWidget { tileExtent: tileExtent, isScrollingNotifier: _isScrollingNotifier, ), - child: selector, + child: _CollectionSectionedContent( + collection: collection, + isScrollingNotifier: _isScrollingNotifier, + scrollController: PrimaryScrollController.of(context), + ), ); }, ); @@ -121,12 +91,69 @@ class _ThumbnailCollectionContent extends StatelessWidget { } } -class _ThumbnailGridScaleGestureDetector extends StatelessWidget { +class _CollectionSectionedContent extends StatefulWidget { + final CollectionLens collection; + final ValueNotifier isScrollingNotifier; + final ScrollController scrollController; + + const _CollectionSectionedContent({ + @required this.collection, + @required this.isScrollingNotifier, + @required this.scrollController, + }); + + @override + _CollectionSectionedContentState createState() => _CollectionSectionedContentState(); +} + +class _CollectionSectionedContentState extends State<_CollectionSectionedContent> { + CollectionLens get collection => widget.collection; + + ScrollController get scrollController => widget.scrollController; + + final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); + + @override + Widget build(BuildContext context) { + final scrollView = AnimationLimiter( + child: _CollectionScrollView( + scrollableKey: _scrollableKey, + collection: collection, + appBar: CollectionAppBar( + appBarHeightNotifier: _appBarHeightNotifier, + collection: collection, + ), + appBarHeightNotifier: _appBarHeightNotifier, + isScrollingNotifier: widget.isScrollingNotifier, + scrollController: scrollController, + ), + ); + + final scaler = _CollectionScaler( + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + child: scrollView, + ); + + final selector = GridSelectionGestureDetector( + selectable: AvesApp.mode == AppMode.main, + collection: collection, + scrollController: scrollController, + appBarHeightNotifier: _appBarHeightNotifier, + child: scaler, + ); + + return selector; + } +} + +class _CollectionScaler extends StatelessWidget { final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; final Widget child; - const _ThumbnailGridScaleGestureDetector({ + const _CollectionScaler({ @required this.scrollableKey, @required this.appBarHeightNotifier, @required this.child, @@ -166,7 +193,7 @@ class _ThumbnailGridScaleGestureDetector extends StatelessWidget { } } -class CollectionScrollView extends StatefulWidget { +class _CollectionScrollView extends StatefulWidget { final GlobalKey scrollableKey; final CollectionLens collection; final Widget appBar; @@ -174,7 +201,7 @@ class CollectionScrollView extends StatefulWidget { final ValueNotifier isScrollingNotifier; final ScrollController scrollController; - const CollectionScrollView({ + const _CollectionScrollView({ @required this.scrollableKey, @required this.collection, @required this.appBar, @@ -187,7 +214,7 @@ class CollectionScrollView extends StatefulWidget { _CollectionScrollViewState createState() => _CollectionScrollViewState(); } -class _CollectionScrollViewState extends State { +class _CollectionScrollViewState extends State<_CollectionScrollView> { Timer _scrollMonitoringTimer; @override @@ -197,7 +224,7 @@ class _CollectionScrollViewState extends State { } @override - void didUpdateWidget(covariant CollectionScrollView oldWidget) { + void didUpdateWidget(covariant _CollectionScrollView oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -210,13 +237,13 @@ class _CollectionScrollViewState extends State { super.dispose(); } - void _registerWidget(CollectionScrollView widget) { + void _registerWidget(_CollectionScrollView widget) { widget.collection.filterChangeNotifier.addListener(_scrollToTop); widget.collection.sortGroupChangeNotifier.addListener(_scrollToTop); widget.scrollController.addListener(_onScrollChange); } - void _unregisterWidget(CollectionScrollView widget) { + void _unregisterWidget(_CollectionScrollView widget) { widget.collection.filterChangeNotifier.removeListener(_scrollToTop); widget.collection.sortGroupChangeNotifier.removeListener(_scrollToTop); widget.scrollController.removeListener(_onScrollChange); @@ -228,27 +255,6 @@ class _CollectionScrollViewState extends State { return _buildDraggableScrollView(scrollView); } - ScrollView _buildScrollView(Widget appBar, CollectionLens collection) { - return CustomScrollView( - key: widget.scrollableKey, - primary: true, - // workaround to prevent scrolling the app bar away - // when there is no content and we use `SliverFillRemaining` - physics: collection.isEmpty ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - cacheExtent: context.select((controller) => controller.effectiveExtentMax * 2), - slivers: [ - appBar, - collection.isEmpty - ? SliverFillRemaining( - hasScrollBody: false, - child: _buildEmptyCollectionPlaceholder(collection), - ) - : SectionedListSliver(), - BottomPaddingSliver(), - ], - ); - } - Widget _buildDraggableScrollView(ScrollView scrollView) { return ValueListenableBuilder( valueListenable: widget.appBarHeightNotifier, @@ -274,6 +280,27 @@ class _CollectionScrollViewState extends State { ); } + ScrollView _buildScrollView(Widget appBar, CollectionLens collection) { + return CustomScrollView( + key: widget.scrollableKey, + primary: true, + // workaround to prevent scrolling the app bar away + // when there is no content and we use `SliverFillRemaining` + physics: collection.isEmpty ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + cacheExtent: context.select((controller) => controller.effectiveExtentMax * 2), + slivers: [ + appBar, + collection.isEmpty + ? SliverFillRemaining( + hasScrollBody: false, + child: _buildEmptyCollectionPlaceholder(collection), + ) + : SectionedListSliver(), + BottomPaddingSliver(), + ], + ); + } + Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) { return ValueListenableBuilder( valueListenable: collection.source.stateNotifier, diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 1ca238fd7..6c8704dc7 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,5 +1,5 @@ import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/widgets/collection/thumbnail_collection.dart'; +import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -42,9 +42,12 @@ class _CollectionPageState extends State { }, child: DoubleBackPopScope( child: GestureAreaProtectorStack( - child: ChangeNotifierProvider.value( - value: collection, - child: ThumbnailCollection(), + child: SafeArea( + bottom: false, + child: ChangeNotifierProvider.value( + value: collection, + child: CollectionGrid(), + ), ), ), ), diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index d9006329c..f53813f83 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -58,21 +58,21 @@ class _AlbumPickPageState extends State { return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) => FilterGridPage( + settingsRouteKey: AlbumListPage.routeName, appBar: appBar, + appBarHeight: AlbumPickAppBar.preferredHeight, filterSections: AlbumListPage.getAlbumEntries(context, source), showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, + queryNotifier: _queryNotifier, applyQuery: (filters, query) { if (query == null || query.isEmpty) return filters; query = query.toUpperCase(); return filters.where((item) => item.filter.uniqueName.toUpperCase().contains(query)).toList(); }, - queryNotifier: _queryNotifier, emptyBuilder: () => EmptyContent( icon: AIcons.album, text: context.l10n.albumEmpty, ), - settingsRouteKey: AlbumListPage.routeName, - appBarHeight: AlbumPickAppBar.preferredHeight, onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ), ); diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 5610731e4..34f64ef8d 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -29,27 +29,27 @@ import 'package:provider/provider.dart'; typedef QueryTest = Iterable> Function(Iterable> filters, String query); class FilterGridPage extends StatelessWidget { + final String settingsRouteKey; final Widget appBar; + final double appBarHeight; final Map>> filterSections; final bool showHeaders; final ValueNotifier queryNotifier; - final Widget Function() emptyBuilder; - final String settingsRouteKey; - final double appBarHeight; final QueryTest applyQuery; + final Widget Function() emptyBuilder; final FilterCallback onTap; final OffsetFilterCallback onLongPress; const FilterGridPage({ Key key, + this.settingsRouteKey, @required this.appBar, + this.appBarHeight = kToolbarHeight, @required this.filterSections, - this.showHeaders = false, + @required this.showHeaders, @required this.queryNotifier, this.applyQuery, @required this.emptyBuilder, - this.settingsRouteKey, - this.appBarHeight = kToolbarHeight, @required this.onTap, this.onLongPress, }) : super(key: key); @@ -64,24 +64,17 @@ class FilterGridPage extends StatelessWidget { child: GestureAreaProtectorStack( child: SafeArea( bottom: false, - child: TileExtentControllerProvider( - controller: TileExtentController( - settingsRouteKey: settingsRouteKey ?? context.currentRouteName, - columnCountDefault: 2, - extentMin: 60, - spacing: 8, - ), - child: _FilterGridPageContent( - appBar: appBar, - filterSections: filterSections, - showHeaders: showHeaders, - queryNotifier: queryNotifier, - applyQuery: applyQuery, - emptyBuilder: emptyBuilder, - appBarHeight: appBarHeight, - onTap: onTap, - onLongPress: onLongPress, - ), + child: FilterGrid( + settingsRouteKey: settingsRouteKey, + appBar: appBar, + appBarHeight: appBarHeight, + filterSections: filterSections, + showHeaders: showHeaders, + queryNotifier: queryNotifier, + applyQuery: applyQuery, + emptyBuilder: emptyBuilder, + onTap: onTap, + onLongPress: onLongPress, ), ), ), @@ -93,7 +86,65 @@ class FilterGridPage extends StatelessWidget { } } -class _FilterGridPageContent extends StatelessWidget { +class FilterGrid extends StatefulWidget { + final String settingsRouteKey; + final Widget appBar; + final double appBarHeight; + final Map>> filterSections; + final bool showHeaders; + final ValueNotifier queryNotifier; + final QueryTest applyQuery; + final Widget Function() emptyBuilder; + final FilterCallback onTap; + final OffsetFilterCallback onLongPress; + + const FilterGrid({ + Key key, + @required this.settingsRouteKey, + @required this.appBar, + @required this.appBarHeight, + @required this.filterSections, + @required this.showHeaders, + @required this.queryNotifier, + @required this.applyQuery, + @required this.emptyBuilder, + @required this.onTap, + @required this.onLongPress, + }) : super(key: key); + + @override + _FilterGridState createState() => _FilterGridState(); +} + +class _FilterGridState extends State> { + TileExtentController _tileExtentController; + + @override + Widget build(BuildContext context) { + _tileExtentController ??= TileExtentController( + settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName, + columnCountDefault: 2, + extentMin: 60, + spacing: 8, + ); + return TileExtentControllerProvider( + controller: _tileExtentController, + child: _FilterGridContent( + appBar: widget.appBar, + appBarHeight: widget.appBarHeight, + filterSections: widget.filterSections, + showHeaders: widget.showHeaders, + queryNotifier: widget.queryNotifier, + applyQuery: widget.applyQuery, + emptyBuilder: widget.emptyBuilder, + onTap: widget.onTap, + onLongPress: widget.onLongPress, + ), + ); + } +} + +class _FilterGridContent extends StatelessWidget { final Widget appBar; final Map>> filterSections; final bool showHeaders; @@ -105,15 +156,15 @@ class _FilterGridPageContent extends StatelessWidget final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); - _FilterGridPageContent({ + _FilterGridContent({ Key key, @required this.appBar, + @required double appBarHeight, @required this.filterSections, @required this.showHeaders, @required this.queryNotifier, @required this.applyQuery, @required this.emptyBuilder, - @required double appBarHeight, @required this.onTap, @required this.onLongPress, }) : super(key: key) { @@ -166,7 +217,7 @@ class _FilterGridPageContent extends StatelessWidget ), ); }, - child: _SectionedContent( + child: _FilterSectionedContent( appBar: appBar, appBarHeightNotifier: _appBarHeightNotifier, visibleFilterSections: visibleFilterSections, @@ -182,14 +233,14 @@ class _FilterGridPageContent extends StatelessWidget } } -class _SectionedContent extends StatefulWidget { +class _FilterSectionedContent extends StatefulWidget { final Widget appBar; final ValueNotifier appBarHeightNotifier; final Map>> visibleFilterSections; final Widget Function() emptyBuilder; final ScrollController scrollController; - const _SectionedContent({ + const _FilterSectionedContent({ @required this.appBar, @required this.appBarHeightNotifier, @required this.visibleFilterSections, @@ -198,10 +249,10 @@ class _SectionedContent extends StatefulWidget { }); @override - _SectionedContentState createState() => _SectionedContentState(); + _FilterSectionedContentState createState() => _FilterSectionedContentState(); } -class _SectionedContentState extends State<_SectionedContent> { +class _FilterSectionedContentState extends State<_FilterSectionedContent> { Widget get appBar => widget.appBar; ValueNotifier get appBarHeightNotifier => widget.appBarHeightNotifier; @@ -220,6 +271,27 @@ class _SectionedContentState extends State<_Sectione WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); } + @override + Widget build(BuildContext context) { + final scrollView = AnimationLimiter( + child: _FilterScrollView( + scrollableKey: _scrollableKey, + appBar: appBar, + appBarHeightNotifier: appBarHeightNotifier, + emptyBuilder: emptyBuilder, + scrollController: scrollController, + ), + ); + + final scaler = _FilterScaler( + scrollableKey: _scrollableKey, + appBarHeightNotifier: appBarHeightNotifier, + child: scrollView, + ); + + return scaler; + } + Future _checkInitHighlight() async { final highlightInfo = context.read(); final filter = highlightInfo.clear(); @@ -252,13 +324,25 @@ class _SectionedContentState extends State<_Sectione ); } } +} + +class _FilterScaler extends StatelessWidget { + final GlobalKey scrollableKey; + final ValueNotifier appBarHeightNotifier; + final Widget child; + + const _FilterScaler({ + @required this.scrollableKey, + @required this.appBarHeightNotifier, + @required this.child, + }); @override Widget build(BuildContext context) { final pinnedFilters = settings.pinnedFilters; final tileSpacing = context.select((controller) => controller.spacing); return GridScaleGestureDetector>( - scrollableKey: _scrollableKey, + scrollableKey: scrollableKey, appBarHeightNotifier: appBarHeightNotifier, gridBuilder: (center, extent, child) => CustomPaint( painter: GridPainter( @@ -283,11 +367,31 @@ class _SectionedContentState extends State<_Sectione return sectionedListLayout.getTileRect(item) ?? Rect.zero; }, onScaled: (item) => context.read().set(item.filter), - child: AnimationLimiter( - child: _buildDraggableScrollView(_buildScrollView(context, visibleFilterSections.isEmpty)), - ), + child: child, ); } +} + +class _FilterScrollView extends StatelessWidget { + final GlobalKey scrollableKey; + final Widget appBar; + final ValueNotifier appBarHeightNotifier; + final Widget Function() emptyBuilder; + final ScrollController scrollController; + + const _FilterScrollView({ + @required this.scrollableKey, + @required this.appBar, + @required this.appBarHeightNotifier, + @required this.emptyBuilder, + @required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + final scrollView = _buildScrollView(context); + return _buildDraggableScrollView(scrollView); + } Widget _buildDraggableScrollView(ScrollView scrollView) { return Selector( @@ -310,31 +414,30 @@ class _SectionedContentState extends State<_Sectione ); } - ScrollView _buildScrollView(BuildContext context, bool empty) { - Widget content; - if (empty) { - content = SliverFillRemaining( - hasScrollBody: false, - child: Selector( - selector: (context, mq) => mq.effectiveBottomPadding, - builder: (context, mqPaddingBottom, child) { - return Padding( - padding: EdgeInsets.only(bottom: mqPaddingBottom), - child: emptyBuilder(), - ); - }, - ), - ); - } else { - content = SectionedListSliver>(); - } - + ScrollView _buildScrollView(BuildContext context) { return CustomScrollView( - key: _scrollableKey, + key: scrollableKey, controller: scrollController, slivers: [ appBar, - content, + Selector>, bool>( + selector: (context, layout) => layout.sections.isEmpty, + builder: (context, empty, child) { + return empty + ? SliverFillRemaining( + hasScrollBody: false, + child: Selector( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { + return Padding( + padding: EdgeInsets.only(bottom: mqPaddingBottom), + child: emptyBuilder(), + ); + }, + ), + ) + : SectionedListSliver>(); + }), BottomPaddingSliver(), ], );