Skip to content

Optimize scroll performance #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/dropdown_button2/lib/src/dropdown_button2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'seperated_sliver_child_builder_delegate.dart';

part 'dropdown_style_data.dart';
part 'dropdown_route.dart';
part 'dropdown_menu.dart';
Expand Down
54 changes: 41 additions & 13 deletions packages/dropdown_button2/lib/src/dropdown_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
bool get _iOSThumbVisibility =>
_scrollbarTheme?.thumbVisibility?.resolve(_states) ?? true;

DropdownSeparator<T>? get separator => widget.route.dropdownSeparator;
bool get _hasIntrinsicHeight =>
widget.route.items.any((item) => item.intrinsicHeight) ||
(widget.route.dropdownSeparator != null &&
widget.route.dropdownSeparator!.intrinsicHeight);

@override
Widget build(BuildContext context) {
Expand All @@ -142,6 +145,8 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
MaterialLocalizations.of(context);
final _DropdownRoute<T> route = widget.route;

final separator = widget.route.dropdownSeparator;

final Widget dropdownMenu = Material(
type: MaterialType.transparency,
textStyle: route.style,
Expand Down Expand Up @@ -179,23 +184,46 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
? _scrollbarTheme?.thickness?.resolve(_states)
: null,
radius: _isIOS ? _scrollbarTheme?.radius : null,
child: ListView.separated(
child: ListView.custom(
// Ensure this always inherits the PrimaryScrollController
primary: true,
shrinkWrap: true,
padding:
dropdownStyle.padding ?? kMaterialListPadding,
itemCount: _children.length,
itemBuilder: (context, index) => _children[index],
separatorBuilder: (context, index) =>
separator != null
? SizedBox(
height: separator!.intrinsicHeight
? null
: separator!.height,
child: separator,
)
: const SizedBox.shrink(),
itemExtentBuilder: _hasIntrinsicHeight
? null
: (index, dimensions) {
final childrenLength = separator == null
? _children.length
: SeparatedSliverChildBuilderDelegate
.computeActualChildCount(
_children.length);
// TODO(Ahmed): Remove this when https://github.com/flutter/flutter/pull/142428
// is supported by the min version of the package [Flutter>=3.22.0].
if (index >= childrenLength) {
return 100;
}
return separator != null && index.isOdd
? separator.height
: route.itemHeights[index];
},
childrenDelegate: separator == null
? SliverChildBuilderDelegate(
(context, index) => _children[index],
childCount: _children.length,
)
: SeparatedSliverChildBuilderDelegate(
itemCount: _children.length,
itemBuilder: (context, index) =>
_children[index],
separatorBuilder: (context, index) =>
SizedBox(
height: separator.intrinsicHeight
? null
: separator.height,
child: separator,
),
),
),
),
),
Expand Down
3 changes: 3 additions & 0 deletions packages/dropdown_button2/lib/src/dropdown_menu_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ class _DropdownMenuItemContainer extends StatelessWidget {
/// If set to true, then this item's height will vary according to its
/// intrinsic height instead of using [height] property.
///
/// It is highly recommended to keep this value as false when dealing with
/// a significantly large items list in order to optimize performance.
///
/// Note: If set to true and there isn't enough vertical room for the menu, there's
/// no way to know the item's intrinsic height in-advance to properly scroll to
/// the selected item. Instead, the provided [height] value will be used, which means
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'dart:math' as math;

import 'package:flutter/cupertino.dart';

/// A specialized [SliverChildBuilderDelegate] that builds children for slivers
/// with separators between them.
class SeparatedSliverChildBuilderDelegate extends SliverChildBuilderDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// [itemBuilder] callback. The children are separated by separators,
/// built using the given [separatorBuilder] callback.
///
/// The `itemBuilder` callback will be called with indices greater than
/// or equal to zero and less than [itemCount].
///
/// Separators only appear between children built by the `itemBuilder`:
/// The first separator appears after the first item
/// and the last separator appears before the last item.
///
/// The `separatorBuilder` callback will be called with indices greater than
/// or equal to zero and less than `itemCount - 1`.
///
/// The `itemBuilder`, `itemCount`, `separatorBuilder`, [addAutomaticKeepAlives],
/// [addRepaintBoundaries], [addSemanticIndexes], and [semanticIndexCallback]
/// arguments must not be null.
///
/// If the order in which `itemBuilder` returns children ever changes, consider
/// providing a [findChildIndexCallback]. This allows the delegate to find the
/// new index for a child that was previously located at a different index to
/// attach the existing state to the [Widget] at its new location.
///
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
/// `addSemanticIndexes` argument corresponds to the
/// [SliverChildBuilderDelegate.addSemanticIndexes] property. None may be
/// null.
SeparatedSliverChildBuilderDelegate({
required IndexedWidgetBuilder itemBuilder,
required int itemCount,
required IndexedWidgetBuilder separatorBuilder,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
ChildIndexGetter? findChildIndexCallback,
}) : super(
(BuildContext context, int index) {
final itemIndex = index ~/ 2;

if (index.isEven) {
return itemBuilder(context, itemIndex);
}

final widget = separatorBuilder(context, itemIndex);

return widget;
},
findChildIndexCallback: findChildIndexCallback,
childCount: computeActualChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
semanticIndexCallback: (Widget _, int index) {
return index.isEven ? index ~/ 2 : null;
},
);

// Helper method to compute the actual child count.
static int computeActualChildCount(int itemCount) {
return math.max(0, itemCount * 2 - 1);
}
}
Loading