diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index f0a32f7d..1e76a25b 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -57,17 +57,21 @@ class StreamChannelListView extends StatelessWidget { this.onChannelTap, this.onChannelLongPress, this.loadMoreTriggerIndex = 3, - this.padding, - this.physics, + this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, this.primary, - this.scrollBehavior, + this.physics, this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : super(key: key); /// The [StreamChannelListController] used to control the list of channels. @@ -81,7 +85,7 @@ class StreamChannelListView extends StatelessWidget { final StreamChannelListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. - final PagedValueListViewIndexedWidgetBuilder separatorBuilder; + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. /// @@ -107,9 +111,56 @@ class StreamChannelListView extends StatelessWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + /// {@template flutter.widgets.scroll_view.reverse} /// Whether the scroll view scrolls in the reading direction. /// @@ -157,14 +208,6 @@ class StreamChannelListView extends StatelessWidget { /// Defaults to true when [scrollController] is null. final bool? primary; - /// {@macro flutter.widgets.shadow.scrollBehavior} - /// - /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit - /// [ScrollPhysics] is provided in [physics], it will take precedence, - /// followed by [scrollBehavior], and then the inherited ancestor - /// [ScrollBehavior]. - final ScrollBehavior? scrollBehavior; - /// {@template flutter.widgets.scroll_view.shrinkWrap} /// Whether the extent of the scroll view in the [scrollDirection] should be /// determined by the contents being viewed. @@ -240,18 +283,29 @@ class StreamChannelListView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + @override Widget build(BuildContext context) => PagedValueListView( + scrollDirection: scrollDirection, padding: padding, physics: physics, reverse: reverse, controller: controller, + scrollController: scrollController, primary: primary, shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, dragStartBehavior: dragStartBehavior, cacheExtent: cacheExtent, + clipBehavior: clipBehavior, loadMoreTriggerIndex: loadMoreTriggerIndex, separatorBuilder: separatorBuilder, itemBuilder: (context, channels, index) { diff --git a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart index d7423ef5..0b23c331 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamMessageSearchListView]. @@ -54,17 +53,21 @@ class StreamMessageSearchListView extends StatelessWidget { this.onMessageTap, this.onMessageLongPress, this.loadMoreTriggerIndex = 3, - this.padding, - this.physics, + this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, this.primary, - this.scrollBehavior, + this.physics, this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : super(key: key); /// The [StreamUserListController] used to control the list of @@ -79,7 +82,7 @@ class StreamMessageSearchListView extends StatelessWidget { final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. - final PagedValueListViewIndexedWidgetBuilder + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. @@ -106,9 +109,56 @@ class StreamMessageSearchListView extends StatelessWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + /// {@template flutter.widgets.scroll_view.reverse} /// Whether the scroll view scrolls in the reading direction. /// @@ -156,14 +206,6 @@ class StreamMessageSearchListView extends StatelessWidget { /// Defaults to true when [scrollController] is null. final bool? primary; - /// {@macro flutter.widgets.shadow.scrollBehavior} - /// - /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit - /// [ScrollPhysics] is provided in [physics], it will take precedence, - /// followed by [scrollBehavior], and then the inherited ancestor - /// [ScrollBehavior]. - final ScrollBehavior? scrollBehavior; - /// {@template flutter.widgets.scroll_view.shrinkWrap} /// Whether the extent of the scroll view in the [scrollDirection] should be /// determined by the contents being viewed. @@ -239,19 +281,31 @@ class StreamMessageSearchListView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override Widget build(BuildContext context) => PagedValueListView( + scrollDirection: scrollDirection, padding: padding, physics: physics, reverse: reverse, controller: controller, + scrollController: scrollController, primary: primary, shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, dragStartBehavior: dragStartBehavior, cacheExtent: cacheExtent, + clipBehavior: clipBehavior, loadMoreTriggerIndex: loadMoreTriggerIndex, separatorBuilder: separatorBuilder, itemBuilder: (context, messageResponses, index) { diff --git a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart index 3f715193..45fd37bb 100644 --- a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart @@ -1,8 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart'; -import 'package:stream_chat_flutter/src/v4/user_list_view/stream_user_list_tile.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamUserListView]. @@ -54,17 +52,21 @@ class StreamUserListView extends StatelessWidget { this.onUserTap, this.onUserLongPress, this.loadMoreTriggerIndex = 3, - this.padding, - this.physics, + this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, this.primary, - this.scrollBehavior, + this.physics, this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, + this.clipBehavior = Clip.hardEdge, }) : super(key: key); /// The [StreamUserListController] used to control the list of users. @@ -78,7 +80,7 @@ class StreamUserListView extends StatelessWidget { final StreamUserListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. - final PagedValueListViewIndexedWidgetBuilder separatorBuilder; + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. /// @@ -104,9 +106,56 @@ class StreamUserListView extends StatelessWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + /// {@template flutter.widgets.scroll_view.reverse} /// Whether the scroll view scrolls in the reading direction. /// @@ -154,14 +203,6 @@ class StreamUserListView extends StatelessWidget { /// Defaults to true when [scrollController] is null. final bool? primary; - /// {@macro flutter.widgets.shadow.scrollBehavior} - /// - /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit - /// [ScrollPhysics] is provided in [physics], it will take precedence, - /// followed by [scrollBehavior], and then the inherited ancestor - /// [ScrollBehavior]. - final ScrollBehavior? scrollBehavior; - /// {@template flutter.widgets.scroll_view.shrinkWrap} /// Whether the extent of the scroll view in the [scrollDirection] should be /// determined by the contents being viewed. @@ -237,18 +278,29 @@ class StreamUserListView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + @override Widget build(BuildContext context) => PagedValueListView( + scrollDirection: scrollDirection, padding: padding, physics: physics, reverse: reverse, controller: controller, + scrollController: scrollController, primary: primary, shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, keyboardDismissBehavior: keyboardDismissBehavior, restorationId: restorationId, dragStartBehavior: dragStartBehavior, cacheExtent: cacheExtent, + clipBehavior: clipBehavior, loadMoreTriggerIndex: loadMoreTriggerIndex, separatorBuilder: separatorBuilder, itemBuilder: (context, users, index) { diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_list_view.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_list_view.dart deleted file mode 100644 index 75708589..00000000 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_list_view.dart +++ /dev/null @@ -1,297 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; - -/// Signature for a function that creates a widget for a given index, e.g., in a -/// [PagedValueListView]. -typedef PagedValueListViewIndexedWidgetBuilder = Widget Function( - BuildContext context, - List values, - int index, -); - -/// Signature for the item builder that creates the children of the -/// [PagedValueListView]. -typedef PagedValueListViewLoadMoreErrorBuilder = Widget Function( - BuildContext context, - StreamChatError error, -); - -/// A [ListView] that loads more pages when the user scrolls to the end of the -/// list. -/// -/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the -/// loading of the next page. -class PagedValueListView extends StatefulWidget { - /// Creates a new instance of [PagedValueListView] widget. - const PagedValueListView({ - Key? key, - required this.controller, - required this.itemBuilder, - required this.separatorBuilder, - required this.emptyBuilder, - required this.loadMoreErrorBuilder, - required this.loadMoreIndicatorBuilder, - required this.loadingBuilder, - required this.errorBuilder, - this.loadMoreTriggerIndex = 3, - this.padding, - this.physics, - this.reverse = false, - this.scrollController, - this.primary, - this.scrollBehavior, - this.shrinkWrap = false, - this.cacheExtent, - this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, - this.restorationId, - }) : super(key: key); - - /// The [PagedValueNotifier] used to control the list of items. - final PagedValueNotifier controller; - - /// A builder that is called to build items in the [ListView]. - /// - /// The `value` parameter is the [V] at this position in the list. - final PagedValueListViewIndexedWidgetBuilder itemBuilder; - - /// A builder that is called to build the list separator. - final PagedValueListViewIndexedWidgetBuilder separatorBuilder; - - /// A builder that is called to build the empty state of the list. - final WidgetBuilder emptyBuilder; - - /// A builder that is called to build the load more error state of the list. - final PagedValueListViewLoadMoreErrorBuilder loadMoreErrorBuilder; - - /// A builder that is called to build the load more indicator of the list. - final WidgetBuilder loadMoreIndicatorBuilder; - - /// A builder that is called to build the loading state of the list. - final WidgetBuilder loadingBuilder; - - /// A builder that is called to build the error state of the list. - final Widget Function(BuildContext, StreamChatError) errorBuilder; - - /// The index to take into account when triggering [controller.loadMore]. - final int loadMoreTriggerIndex; - - /// The amount of space by which to inset the children. - final EdgeInsetsGeometry? padding; - - /// {@template flutter.widgets.scroll_view.reverse} - /// Whether the scroll view scrolls in the reading direction. - /// - /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view - /// scrolls from top to bottom when [reverse] is false and from bottom to top - /// when [reverse] is true. - /// - /// Defaults to false. - /// {@endtemplate} - final bool reverse; - - /// {@template flutter.widgets.scroll_view.controller} - /// An object that can be used to control the position to which this scroll - /// view is scrolled. - /// - /// Must be null if [primary] is true. - /// - /// A [ScrollController] serves several purposes. It can be used to control - /// the initial scroll position (see [ScrollController.initialScrollOffset]). - /// It can be used to control whether the scroll view should automatically - /// save and restore its scroll position in the [PageStorage] (see - /// [ScrollController.keepScrollOffset]). It can be used to read the current - /// scroll position (see [ScrollController.offset]), or change it (see - /// [ScrollController.animateTo]). - /// {@endtemplate} - final ScrollController? scrollController; - - /// {@template flutter.widgets.scroll_view.primary} - /// Whether this is the primary scroll view associated with the parent - /// [PrimaryScrollController]. - /// - /// When this is true, the scroll view is scrollable even if it does not have - /// sufficient content to actually scroll. Otherwise, by default the user can - /// only scroll the view if it has sufficient content. See [physics]. - /// - /// Also when true, the scroll view is used for default [ScrollAction]s. If a - /// ScrollAction is not handled by an otherwise focused part of the - /// application, the ScrollAction will be evaluated using this scroll view, - /// for example, when executing [Shortcuts] key events like page up and down. - /// - /// On iOS, this also identifies the scroll view that will scroll to top in - /// response to a tap in the status bar. - /// {@endtemplate} - /// - /// Defaults to true when [scrollController] is null. - final bool? primary; - - /// {@macro flutter.widgets.shadow.scrollBehavior} - /// - /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit - /// [ScrollPhysics] is provided in [physics], it will take precedence, - /// followed by [scrollBehavior], and then the inherited ancestor - /// [ScrollBehavior]. - final ScrollBehavior? scrollBehavior; - - /// {@template flutter.widgets.scroll_view.shrinkWrap} - /// Whether the extent of the scroll view in the [scrollDirection] should be - /// determined by the contents being viewed. - /// - /// If the scroll view does not shrink wrap, then the scroll view will expand - /// to the maximum allowed size in the [scrollDirection]. If the scroll view - /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must - /// be true. - /// - /// Shrink wrapping the content of the scroll view is significantly more - /// expensive than expanding to the maximum allowed size because the content - /// can expand and contract during scrolling, which means the size of the - /// scroll view needs to be recomputed whenever the scroll position changes. - /// - /// Defaults to false. - /// {@endtemplate} - final bool shrinkWrap; - - /// {@template flutter.widgets.scroll_view.physics} - /// How the scroll view should respond to user input. - /// - /// For example, determines how the scroll view continues to animate after the - /// user stops dragging the scroll view. - /// - /// Defaults to matching platform conventions. Furthermore, if [primary] is - /// false, then the user cannot scroll if there is insufficient content to - /// scroll, while if [primary] is true, they can always attempt to scroll. - /// - /// To force the scroll view to always be scrollable even if there is - /// insufficient content, as if [primary] was true but without necessarily - /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics - /// object, as in: - /// - /// ```dart - /// physics: const AlwaysScrollableScrollPhysics(), - /// ``` - /// - /// To force the scroll view to use the default platform conventions and not - /// be scrollable if there is insufficient content, regardless of the value of - /// [primary], provide an explicit [ScrollPhysics] object, as in: - /// - /// ```dart - /// physics: const ScrollPhysics(), - /// ``` - /// - /// The physics can be changed dynamically (by providing a new object in a - /// subsequent build), but new physics will only take effect if the _class_ of - /// the provided object changes. Merely constructing a new instance with a - /// different configuration is insufficient to cause the physics to be - /// reapplied. (This is because the final object used is generated - /// dynamically, which can be relatively expensive, and it would be - /// inefficient to speculatively create this object each frame to see if the - /// physics should be updated.) - /// {@endtemplate} - /// - /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the - /// [ScrollPhysics] provided by that behavior will take precedence after - /// [physics]. - final ScrollPhysics? physics; - - /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} - final double? cacheExtent; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} - /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will - /// dismiss the keyboard automatically. - /// {@endtemplate} - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - - /// {@macro flutter.widgets.scrollable.restorationId} - final String? restorationId; - - @override - State> createState() => - _PagedValueListViewState(); -} - -class _PagedValueListViewState extends State> { - PagedValueNotifier get _controller => widget.controller; - - // Avoids duplicate requests on rebuilds. - bool _hasRequestedNextPage = false; - - @override - void initState() { - super.initState(); - _controller.doInitialLoad(); - } - - @override - void didUpdateWidget(covariant PagedValueListView oldWidget) { - super.didUpdateWidget(oldWidget); - if (_controller != oldWidget.controller) { - // reset duplicate requests flag - _hasRequestedNextPage = false; - _controller.doInitialLoad(); - } - } - - @override - Widget build(BuildContext context) => PagedValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, _) => value.when( - (items, nextPageKey, error) { - if (items.isEmpty) { - return widget.emptyBuilder(context); - } - - return ListView.separated( - padding: widget.padding, - physics: widget.physics, - reverse: widget.reverse, - controller: widget.scrollController, - primary: widget.primary, - shrinkWrap: widget.shrinkWrap, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - restorationId: widget.restorationId, - dragStartBehavior: widget.dragStartBehavior, - cacheExtent: widget.cacheExtent, - itemCount: value.itemCount, - separatorBuilder: (context, index) => - widget.separatorBuilder(context, items, index), - itemBuilder: (context, index) { - if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = - items.length - widget.loadMoreTriggerIndex; - final isBuildingTriggerIndexItem = - index == newPageRequestTriggerIndex; - if (nextPageKey != null && isBuildingTriggerIndexItem) { - // Schedules the request for the end of this frame. - WidgetsBinding.instance?.addPostFrameCallback((_) async { - if (error == null) { - await _controller.loadMore(nextPageKey); - } - _hasRequestedNextPage = false; - }); - _hasRequestedNextPage = true; - } - } - - if (index == items.length) { - if (error != null) { - return widget.loadMoreErrorBuilder(context, error); - } - return widget.loadMoreIndicatorBuilder(context); - } - - return widget.itemBuilder(context, items, index); - }, - ); - }, - loading: () => widget.loadingBuilder(context), - error: (error) => widget.errorBuilder(context, error), - ), - ); -} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart new file mode 100644 index 00000000..3990dc5f --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart @@ -0,0 +1,707 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// Signature for a function that creates a widget for a given index, e.g., in a +/// [PagedValueListView] and [PagedValueGridView]. +typedef PagedValueScrollViewIndexedWidgetBuilder = Widget Function( + BuildContext context, + List values, + int index, +); + +/// Signature for the item builder that creates the children of the +/// [PagedValueListView] and [PagedValueGridView]. +typedef PagedValueScrollViewLoadMoreErrorBuilder = Widget Function( + BuildContext context, + StreamChatError error, +); + +/// A [ListView] that loads more pages when the user scrolls to the end of the +/// list. +/// +/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the +/// loading of the next page. +class PagedValueListView extends StatefulWidget { + /// Creates a new instance of [PagedValueListView] widget. + const PagedValueListView({ + Key? key, + required this.controller, + required this.itemBuilder, + required this.separatorBuilder, + required this.emptyBuilder, + required this.loadMoreErrorBuilder, + required this.loadMoreIndicatorBuilder, + required this.loadingBuilder, + required this.errorBuilder, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [PagedValueNotifier] used to control the list of items. + final PagedValueNotifier controller; + + /// A builder that is called to build items in the [ListView]. + /// + /// The `value` parameter is the [V] at this position in the list. + final PagedValueScrollViewIndexedWidgetBuilder itemBuilder; + + /// A builder that is called to build the list separator. + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; + + /// A builder that is called to build the empty state of the list. + final WidgetBuilder emptyBuilder; + + /// A builder that is called to build the load more error state of the list. + final PagedValueScrollViewLoadMoreErrorBuilder loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the list. + final WidgetBuilder loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError) errorBuilder; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the + /// application, the ScrollAction will be evaluated using this scroll view, + /// for example, when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollController] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + State> createState() => + _PagedValueListViewState(); +} + +class _PagedValueListViewState extends State> { + PagedValueNotifier get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant PagedValueListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => PagedValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return ListView.separated( + scrollDirection: widget.scrollDirection, + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount, + separatorBuilder: (context, index) => + widget.separatorBuilder(context, items, index), + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = + items.length - widget.loadMoreTriggerIndex; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } + + return widget.itemBuilder(context, items, index); + }, + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); +} + +/// A [GridView] that loads more pages when the user scrolls to the end of the +/// grid. +/// +/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the +/// loading of the next page. +class PagedValueGridView extends StatefulWidget { + /// Creates a new instance of [PagedValueGridView] widget. + const PagedValueGridView({ + Key? key, + required this.controller, + required this.gridDelegate, + required this.itemBuilder, + required this.emptyBuilder, + required this.loadMoreErrorBuilder, + required this.loadMoreIndicatorBuilder, + required this.loadingBuilder, + required this.errorBuilder, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [PagedValueNotifier] used to control the list of items. + final PagedValueNotifier controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + /// + /// The `value` parameter is the [V] at this position in the list. + final PagedValueScrollViewIndexedWidgetBuilder itemBuilder; + + /// A builder that is called to build the empty state of the list. + final WidgetBuilder emptyBuilder; + + /// A builder that is called to build the load more error state of the list. + final PagedValueScrollViewLoadMoreErrorBuilder loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the list. + final WidgetBuilder loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError) errorBuilder; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + State> createState() => + _PagedValueGridViewState(); +} + +class _PagedValueGridViewState extends State> { + PagedValueNotifier get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant PagedValueGridView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => PagedValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return GridView.builder( + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + cacheExtent: widget.cacheExtent, + semanticChildCount: widget.semanticChildCount, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount, + gridDelegate: widget.gridDelegate, + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = + items.length - widget.loadMoreTriggerIndex; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } + + return widget.itemBuilder(context, items, index); + }, + // gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // crossAxisCount: widget.crossAxisCount, + // ), + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); +} diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index b5e61d7c..cddae515 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -12,8 +12,8 @@ export 'src/message_list_core.dart' hide MessageListCoreState; export 'src/message_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; export 'src/message_text_field_controller.dart'; -export 'src/paged_value_list_view.dart'; export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder; +export 'src/paged_value_scroll_view.dart'; export 'src/stream_channel.dart'; export 'src/stream_channel_list_controller.dart'; export 'src/stream_channel_list_event_handler.dart';