From 2cab266c90ccd173edd3711994de2b957d25c99d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Nov 2021 17:33:19 +0530 Subject: [PATCH] feat(ui): add channel list event handler in channel list controller. Signed-off-by: xsahil03x --- .../lib/src/paged_value_notifier.dart | 70 +++- .../stream_channel_list_controller.dart | 213 +++++++++++- .../stream_channel_list_event_handler.dart | 311 ++++++++++++++++++ .../stream_channel_list_view.dart | 91 +++-- 4 files changed, 628 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index d71db24d..d2f5b373 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -10,6 +10,14 @@ const defaultInitialPagedLimitMultiplier = 3; typedef PagedValueListenableBuilder = ValueListenableBuilder>; +/// A [PagedValueNotifier] that uses a [PagedListenable] to load data. +/// +/// This class is useful when you need to load data from a server +/// using a [PagedListenable] and want to keep the UI-driven refresh +/// signals in the [PagedListenable]. +/// +/// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] +/// whenever the data is loaded or an error occurs. abstract class PagedValueNotifier extends ValueNotifier> { /// Creates a [PagedValueNotifier] @@ -18,14 +26,33 @@ abstract class PagedValueNotifier /// Stores initialValue in case we need to call [refresh]. final PagedValue _initialValue; + /// Returns the currently loaded items + List get currentItems => value.asSuccess.items; + + /// Appends [newItems] to the previously loaded ones and replaces + /// the next page's key. + void appendPage({ + required List newItems, + required Key nextPageKey, + }) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems, nextPageKey: nextPageKey); + } + + /// Appends [newItems] to the previously loaded ones and sets the next page + /// key to `null`. + void appendLastPage(List newItems) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems); + } + /// Retry any failed load requests. /// /// Unlike [refresh], this does not resets the whole [value], /// it only retries the last failed load request. Future retry() { - var lastValue = value; + final lastValue = value.asSuccess; assert(lastValue.hasError, ''); - lastValue = lastValue as Success; final nextPageKey = lastValue.nextPageKey; // resetting the error @@ -53,7 +80,7 @@ abstract class PagedValueNotifier abstract class PagedValue with _$PagedValue { const PagedValue._(); - /// Creates a new instance of [PagedValue] with the given [key] and [value]. + /// Represents the success state of the [PagedValue] // @Assert( // 'nextPageKey != null', // 'Cannot set an error if all the pages are already fetched', @@ -69,24 +96,35 @@ abstract class PagedValue with _$PagedValue { StreamChatError? error, }) = Success; - bool get hasNextPage { - assert(this is Success, ''); - return (this as Success).nextPageKey != null; + /// Represents the loading state of the [PagedValue]. + const factory PagedValue.loading() = Loading; + + /// Represents the error state of the [PagedValue]. + const factory PagedValue.error(StreamChatError error) = Error; + + /// Returns `true` if the [PagedValue] is [Success]. + bool get isSuccess => this is Success; + + /// Returns the [PagedValue] as [Success]. + Success get asSuccess { + assert( + isSuccess, + 'Cannot get asSuccess if the PagedValue is not in the Success state', + ); + return this as Success; } - bool get hasError { - assert(this is Success, ''); - return (this as Success).error != null; - } + /// Returns `true` if the [PagedValue] is [Success] + /// and has more items to load. + bool get hasNextPage => asSuccess.nextPageKey != null; + /// Returns `true` if the [PagedValue] is [Success] and has an error. + bool get hasError => asSuccess.error != null; + + /// int get itemCount { - assert(this is Success, ''); - final count = (this as Success).items.length; + final count = asSuccess.items.length; if (hasNextPage || hasError) return count + 1; return count; } - - const factory PagedValue.loading() = Loading; - - const factory PagedValue.error(StreamChatError error) = Error; } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 4bfc7130..d4715619 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -1,15 +1,40 @@ +import 'dart:async'; + import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' + as event_handler; +const defaultChannelPagedLimit = 10; + +typedef ChannelListEventHandler = void Function( + Event event, + StreamChannelListController controller, +); + +/// A controller for the channel list view. class StreamChannelListController extends PagedValueNotifier { /// Creates a [StreamChannelListController]. StreamChannelListController({ required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. @@ -18,9 +43,22 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(value); /// The client to use for the channel list. @@ -41,6 +79,82 @@ class StreamChannelListController extends PagedValueNotifier { /// The limit to apply to the member list. final int? memberLimit; + /// Callback function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// By default, calls [event_handler.onChannelDeleted] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelDeleted; + + /// Callback function which gets called for the event + /// [EventType.channelHidden]. + /// + /// By default, calls [event_handler.onChannelHidden] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelHidden; + + /// Callback function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// By default, calls [event_handler.onChannelTruncated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelTruncated; + + /// Callback function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// By default, calls [event_handler.onChannelUpdated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelUpdated; + + /// Callback function which gets called for the event + /// [EventType.channelVisible]. + /// + /// By default, calls [event_handler.onChannelVisible] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelVisible; + + /// Callback function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// By default, calls [event_handler.onConnectionRecovered] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onConnectionRecovered; + + /// Callback function which gets called for the event [EventType.messageNew]. + /// + /// By default, calls [event_handler.onMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// By default, calls [event_handler.onNotificationAddedToChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationAddedToChannel; + + /// Callback function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// By default, calls [event_handler.onNotificationMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// By default, calls [event_handler.onNotificationRemovedFromChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationRemovedFromChannel; + + /// Callback function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// By default, calls [event_handler.onUserPresenceChanged] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onUserPresenceChanged; + @override Future doInitialLoad() async { final limit = this.limit * defaultInitialPagedLimitMultiplier; @@ -58,15 +172,16 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = PagedValue.error(StreamChatError('error')); + // start listening events + _subscribeToChannelListEvents(); + } on StreamChatError catch (error) { + value = PagedValue.error(error); } } @override Future loadMore(int nextPageKey) async { - assert(value is Success, ''); - final previousValue = value as Success; + final previousValue = value.asSuccess; try { await for (final channels in client.queryChannels( @@ -84,8 +199,90 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = previousValue.copyWith(error: StreamChatError('error')); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); } } + + /// Replaces the previously loaded channels with [channels] and updates + /// the nextPageKey. + set channels(List channels) { + value = PagedValue( + items: channels, + nextPageKey: channels.length, + ); + } + + /// Returns/Creates a new Channel and starts watching it. + Future getChannel({ + required String id, + required String type, + }) async { + final channel = client.channel(type, id: id); + await channel.watch(); + return channel; + } + + StreamSubscription? _channelEventSubscription; + + // Subscribes to the channel list events. + void _subscribeToChannelListEvents() { + if (_channelEventSubscription != null) { + _unsubscribeFromChannelListEvents(); + } + + _channelEventSubscription = client.on().listen((event) { + final eventType = event.type; + if (eventType == EventType.channelDeleted) { + onChannelDeleted(event, this); + } else if (eventType == EventType.channelHidden) { + onChannelHidden(event, this); + } else if (eventType == EventType.channelTruncated) { + onChannelTruncated(event, this); + } else if (eventType == EventType.channelUpdated) { + onChannelUpdated(event, this); + } else if (eventType == EventType.channelVisible) { + onChannelVisible(event, this); + } else if (eventType == EventType.connectionRecovered) { + onConnectionRecovered(event, this); + } else if (eventType == EventType.connectionChanged) { + if (event.online != null) onConnectionRecovered(event, this); + } else if (eventType == EventType.messageNew) { + onMessageNew(event, this); + } else if (eventType == EventType.notificationAddedToChannel) { + onNotificationAddedToChannel(event, this); + } else if (eventType == EventType.notificationMessageNew) { + onNotificationMessageNew(event, this); + } else if (eventType == EventType.notificationRemovedFromChannel) { + onNotificationRemovedFromChannel(event, this); + } else if (eventType == 'user.presence.changed' || + eventType == EventType.userUpdated) { + onUserPresenceChanged(event, this); + } + }); + } + + // Unsubscribes from all channel list events. + void _unsubscribeFromChannelListEvents() { + if (_channelEventSubscription != null) { + _channelEventSubscription!.cancel(); + _channelEventSubscription = null; + } + } + + /// Pauses all subscriptions added to this composite. + void pauseEventsSubscription([Future? resumeSignal]) { + _channelEventSubscription?.pause(resumeSignal); + } + + /// Resumes all subscriptions added to this composite. + void resumeEventsSubscription() { + _channelEventSubscription?.resume(); + } + + @override + void dispose() { + _unsubscribeFromChannelListEvents(); + super.dispose(); + } } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart new file mode 100644 index 00000000..e35549a9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -0,0 +1,311 @@ +import 'package:stream_chat/stream_chat.dart' show Event; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Handles [EventType.channelDeleted] event. +/// +/// This event is fired when a channel is deleted. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelDeleted: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelDeleted( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); + + controller.channels = updatedChannels; +} + +/// Handles [EventType.channelHidden] event. +/// +/// This event is fired when a channel is hidden. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelHidden: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelHidden( + Event event, + StreamChannelListController controller, +) { + onChannelDeleted(event, controller); +} + +/// Handles [EventType.channelTruncated] event. +/// +/// This event is fired when a channel is truncated. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelTruncated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelTruncated( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.channelUpdated] event. +/// +/// This event is fired when a channel is updated. +/// +/// By default, this updates the channel received in the event. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelUpdated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelUpdated( + Event event, + StreamChannelListController controller, +) { + final eventChannel = event.channel; + if (eventChannel == null) return; + + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), + ); + + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } + + controller.channels = channels; +} + +/// Handles [EventType.channelVisible] event. +/// +/// This event is fired when a channel is made visible. +/// +/// By default, this adds the channel to the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelVisible: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelVisible( + Event event, + StreamChannelListController controller, +) async { + final channelId = event.channelId; + final channelType = event.channelType; + + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; +} + +/// Handles [EventType.connectionRecovered] event. +/// +/// This event is fired when the client web-socket connection recovers. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onConnectionRecovered: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onConnectionRecovered( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.messageNew] event. +/// +/// This event is fired when a new message is created in one of the channels +/// we are currently watching. +/// +/// By default, this moves the channel to the top of the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onMessageNew( + Event event, + StreamChannelListController controller, +) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; +} + +/// Handles [EventType.notificationAddedToChannel] event. +/// +/// This event is fired when a channel is added which we are not watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationAddedToChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationMessageNew] event. +/// +/// This event is fired when a new message is created in a channel which we are +/// not currently watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationMessageNew( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationRemovedFromChannel] event. +/// +/// This event is fired when a user is removed from a channel which we are +/// not currently watching. +/// +/// By default, this removes the event channel from the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationRemovedFromChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; +} + +/// Handles 'user.presence.changed' and [EventType.userUpdated] event. +/// +/// This event is fired when a user's presence changes or gets updated. +/// +/// By default, this updates the channel member with the event user. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onUserPresenceChanged: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onUserPresenceChanged( + Event event, + StreamChannelListController controller, +) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; +} 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 ea9f5c1e..b774eaea 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 @@ -10,32 +10,44 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Signature for a function that creates a widget for a given index, e.g., in a -/// list. -/// -/// Used by [GridView.builder] and other APIs that use lazily-generated widgets. -/// -/// See also: -/// -/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. -/// * [TransitionBuilder], which is similar but also takes a child. -/// * [NullableIndexedWidgetBuilder], which is similar but may return null. +Widget defaultSeparatorBuilder(context, index) => + const StreamChannelListSeparator(); + typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, ); -typedef StreamChannelTapCallback = void Function(Channel); - -Widget _defaultSeparatorBuilder(context, index) => - const _ChannelListSeparator(); - +/// A [ListView] that shows a list of [Channel]s, +/// it uses [StreamChannelListTile] as a default item. +/// +/// This is the new version of [ChannelListView] that uses +/// [StreamChannelListController]. +/// +/// Example: +/// +/// ```dart +/// StreamChannelListView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelListTile] +/// * [StreamChannelListController] class StreamChannelListView extends StatefulWidget { + /// Creates a new instance of [StreamChannelListView]. const StreamChannelListView({ Key? key, required this.controller, this.itemBuilder, - this.separatorBuilder = _defaultSeparatorBuilder, + this.separatorBuilder = defaultSeparatorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -51,21 +63,23 @@ class StreamChannelListView extends StatefulWidget { this.restorationId, }) : super(key: key); + /// The [StreamChannelListController] used to control the list of channels. final StreamChannelListController controller; + /// A builder that is called to build items in the [ListView]. + /// + /// The `index` parameter is the index of the list tile in the list and the + /// `channel` parameter is the [Channel] at that position. final StreamChannelListViewItemBuilder? itemBuilder; + /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; /// Called when the user taps this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelTap; + final void Function(Channel)? onChannelTap; /// Called when the user long-presses on this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelLongPress; + final void Function(Channel)? onChannelLongPress; /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; @@ -254,11 +268,11 @@ class _StreamChannelListViewState extends State { final newPageRequestTriggerIndex = value.itemCount - 3; final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; - if (value.hasNextPage && isBuildingTriggerIndexItem) { + if (nextPageKey != null && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance?.addPostFrameCallback((_) async { if (!value.hasError) { - await _controller.loadMore(nextPageKey!); + await _controller.loadMore(nextPageKey); } _hasRequestedNextPage = false; }); @@ -267,15 +281,15 @@ class _StreamChannelListViewState extends State { } if (index == channels.length) { - if (value.hasError) { - return _ChannelListLoadMoreError( + if (error != null) { + return ChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: _ChannelListLoadMoreIndicator(), + child: ChannelListLoadMoreIndicator(), ), ); } @@ -309,8 +323,12 @@ class _StreamChannelListViewState extends State { ); } -class _ChannelListLoadMoreIndicator extends StatelessWidget { - const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +/// A [StreamChannelListTile] that can be used in a [ListView] to show a +/// loading tile while waiting for the [StreamChannelListController] to load +/// more channels. +class ChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreIndicator]. + const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -320,12 +338,16 @@ class _ChannelListLoadMoreIndicator extends StatelessWidget { ); } -class _ChannelListLoadMoreError extends StatelessWidget { - const _ChannelListLoadMoreError({ +/// A [StreamChannelListTile] that is used to display the error indicator when +/// loading more channels fails. +class ChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreError]. + const ChannelListLoadMoreError({ Key? key, required this.onTap, }) : super(key: key); + /// The callback to invoke when the user taps on the error indicator. final GestureTapCallback onTap; @override @@ -355,8 +377,11 @@ class _ChannelListLoadMoreError extends StatelessWidget { } } -class _ChannelListSeparator extends StatelessWidget { - const _ChannelListSeparator({Key? key}) : super(key: key); +/// A widget that is used to display a separator between +/// [StreamChannelListTile] items. +class StreamChannelListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSeparator]. + const StreamChannelListSeparator({Key? key}) : super(key: key); @override Widget build(BuildContext context) {