rebuild performance review

This commit is contained in:
Thibault Deckers 2021-03-14 12:22:01 +09:00
parent 64d80eb4be
commit ff6aef1e82
4 changed files with 268 additions and 135 deletions

View file

@ -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<ThumbnailCollection> {
class _CollectionGridState extends State<CollectionGrid> {
TileExtentController _tileExtentController;
@override
@ -48,54 +48,20 @@ class _ThumbnailCollectionState extends State<ThumbnailCollection> {
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<double> _appBarHeightNotifier = ValueNotifier(0);
class _CollectionGridContent extends StatelessWidget {
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
@override
Widget build(BuildContext context) {
final scrollController = PrimaryScrollController.of(context);
return Consumer<CollectionLens>(
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<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((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<bool> 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<double> _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<double> 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<bool> 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<CollectionScrollView> {
class _CollectionScrollViewState extends State<_CollectionScrollView> {
Timer _scrollMonitoringTimer;
@override
@ -197,7 +224,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
}
@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<CollectionScrollView> {
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<CollectionScrollView> {
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<TileExtentController, double>((controller) => controller.effectiveExtentMax * 2),
slivers: [
appBar,
collection.isEmpty
? SliverFillRemaining(
hasScrollBody: false,
child: _buildEmptyCollectionPlaceholder(collection),
)
: SectionedListSliver<AvesEntry>(),
BottomPaddingSliver(),
],
);
}
Widget _buildDraggableScrollView(ScrollView scrollView) {
return ValueListenableBuilder<double>(
valueListenable: widget.appBarHeightNotifier,
@ -274,6 +280,27 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
);
}
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<TileExtentController, double>((controller) => controller.effectiveExtentMax * 2),
slivers: [
appBar,
collection.isEmpty
? SliverFillRemaining(
hasScrollBody: false,
child: _buildEmptyCollectionPlaceholder(collection),
)
: SectionedListSliver<AvesEntry>(),
BottomPaddingSliver(),
],
);
}
Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) {
return ValueListenableBuilder<SourceState>(
valueListenable: collection.source.stateNotifier,

View file

@ -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<CollectionPage> {
},
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: ThumbnailCollection(),
child: SafeArea(
bottom: false,
child: ChangeNotifierProvider<CollectionLens>.value(
value: collection,
child: CollectionGrid(),
),
),
),
),

View file

@ -58,21 +58,21 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
return StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) => FilterGridPage<AlbumFilter>(
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<String>(context, (filter as AlbumFilter)?.album),
),
);

View file

@ -29,27 +29,27 @@ import 'package:provider/provider.dart';
typedef QueryTest<T extends CollectionFilter> = Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query);
class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final String settingsRouteKey;
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final bool showHeaders;
final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder;
final String settingsRouteKey;
final double appBarHeight;
final QueryTest<T> 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<T extends CollectionFilter> extends StatelessWidget {
child: GestureAreaProtectorStack(
child: SafeArea(
bottom: false,
child: TileExtentControllerProvider(
controller: TileExtentController(
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
columnCountDefault: 2,
extentMin: 60,
spacing: 8,
),
child: _FilterGridPageContent<T>(
appBar: appBar,
filterSections: filterSections,
showHeaders: showHeaders,
queryNotifier: queryNotifier,
applyQuery: applyQuery,
emptyBuilder: emptyBuilder,
appBarHeight: appBarHeight,
onTap: onTap,
onLongPress: onLongPress,
),
child: FilterGrid<T>(
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<T extends CollectionFilter> extends StatelessWidget {
}
}
class _FilterGridPageContent<T extends CollectionFilter> extends StatelessWidget {
class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final String settingsRouteKey;
final Widget appBar;
final double appBarHeight;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final bool showHeaders;
final ValueNotifier<String> queryNotifier;
final QueryTest<T> 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<T>();
}
class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>> {
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<T>(
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<T extends CollectionFilter> extends StatelessWidget {
final Widget appBar;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final bool showHeaders;
@ -105,15 +156,15 @@ class _FilterGridPageContent<T extends CollectionFilter> extends StatelessWidget
final ValueNotifier<double> _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<T extends CollectionFilter> extends StatelessWidget
),
);
},
child: _SectionedContent<T>(
child: _FilterSectionedContent<T>(
appBar: appBar,
appBarHeightNotifier: _appBarHeightNotifier,
visibleFilterSections: visibleFilterSections,
@ -182,14 +233,14 @@ class _FilterGridPageContent<T extends CollectionFilter> extends StatelessWidget
}
}
class _SectionedContent<T extends CollectionFilter> extends StatefulWidget {
class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget {
final Widget appBar;
final ValueNotifier<double> appBarHeightNotifier;
final Map<ChipSectionKey, List<FilterGridItem<T>>> 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<T extends CollectionFilter> extends StatefulWidget {
});
@override
_SectionedContentState createState() => _SectionedContentState<T>();
_FilterSectionedContentState createState() => _FilterSectionedContentState<T>();
}
class _SectionedContentState<T extends CollectionFilter> extends State<_SectionedContent<T>> {
class _FilterSectionedContentState<T extends CollectionFilter> extends State<_FilterSectionedContent<T>> {
Widget get appBar => widget.appBar;
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
@ -220,6 +271,27 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight());
}
@override
Widget build(BuildContext context) {
final scrollView = AnimationLimiter(
child: _FilterScrollView<T>(
scrollableKey: _scrollableKey,
appBar: appBar,
appBarHeightNotifier: appBarHeightNotifier,
emptyBuilder: emptyBuilder,
scrollController: scrollController,
),
);
final scaler = _FilterScaler<T>(
scrollableKey: _scrollableKey,
appBarHeightNotifier: appBarHeightNotifier,
child: scrollView,
);
return scaler;
}
Future<void> _checkInitHighlight() async {
final highlightInfo = context.read<HighlightInfo>();
final filter = highlightInfo.clear();
@ -252,13 +324,25 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
);
}
}
}
class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
final GlobalKey scrollableKey;
final ValueNotifier<double> 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<TileExtentController, double>((controller) => controller.spacing);
return GridScaleGestureDetector<FilterGridItem<T>>(
scrollableKey: _scrollableKey,
scrollableKey: scrollableKey,
appBarHeightNotifier: appBarHeightNotifier,
gridBuilder: (center, extent, child) => CustomPaint(
painter: GridPainter(
@ -283,11 +367,31 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
return sectionedListLayout.getTileRect(item) ?? Rect.zero;
},
onScaled: (item) => context.read<HighlightInfo>().set(item.filter),
child: AnimationLimiter(
child: _buildDraggableScrollView(_buildScrollView(context, visibleFilterSections.isEmpty)),
),
child: child,
);
}
}
class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
final GlobalKey scrollableKey;
final Widget appBar;
final ValueNotifier<double> 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<MediaQueryData, double>(
@ -310,31 +414,30 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
);
}
ScrollView _buildScrollView(BuildContext context, bool empty) {
Widget content;
if (empty) {
content = SliverFillRemaining(
hasScrollBody: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) {
return Padding(
padding: EdgeInsets.only(bottom: mqPaddingBottom),
child: emptyBuilder(),
);
},
),
);
} else {
content = SectionedListSliver<FilterGridItem<T>>();
}
ScrollView _buildScrollView(BuildContext context) {
return CustomScrollView(
key: _scrollableKey,
key: scrollableKey,
controller: scrollController,
slivers: [
appBar,
content,
Selector<SectionedListLayout<FilterGridItem<T>>, bool>(
selector: (context, layout) => layout.sections.isEmpty,
builder: (context, empty, child) {
return empty
? SliverFillRemaining(
hasScrollBody: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) {
return Padding(
padding: EdgeInsets.only(bottom: mqPaddingBottom),
child: emptyBuilder(),
);
},
),
)
: SectionedListSliver<FilterGridItem<T>>();
}),
BottomPaddingSliver(),
],
);