feat(ui): add channel list event handler in channel list controller.

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2021-12-06 14:04:13 +05:30
committed by xsahil03x
parent fcb9e308c7
commit 2cab266c90
4 changed files with 628 additions and 57 deletions
@@ -10,6 +10,14 @@ const defaultInitialPagedLimitMultiplier = 3;
typedef PagedValueListenableBuilder<Key, Value> typedef PagedValueListenableBuilder<Key, Value>
= ValueListenableBuilder<PagedValue<Key, Value>>; = ValueListenableBuilder<PagedValue<Key, Value>>;
/// 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<Key, Value> abstract class PagedValueNotifier<Key, Value>
extends ValueNotifier<PagedValue<Key, Value>> { extends ValueNotifier<PagedValue<Key, Value>> {
/// Creates a [PagedValueNotifier] /// Creates a [PagedValueNotifier]
@@ -18,14 +26,33 @@ abstract class PagedValueNotifier<Key, Value>
/// Stores initialValue in case we need to call [refresh]. /// Stores initialValue in case we need to call [refresh].
final PagedValue<Key, Value> _initialValue; final PagedValue<Key, Value> _initialValue;
/// Returns the currently loaded items
List<Value> get currentItems => value.asSuccess.items;
/// Appends [newItems] to the previously loaded ones and replaces
/// the next page's key.
void appendPage({
required List<Value> 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<Value> newItems) {
final updatedItems = currentItems + newItems;
value = PagedValue(items: updatedItems);
}
/// Retry any failed load requests. /// Retry any failed load requests.
/// ///
/// Unlike [refresh], this does not resets the whole [value], /// Unlike [refresh], this does not resets the whole [value],
/// it only retries the last failed load request. /// it only retries the last failed load request.
Future<void> retry() { Future<void> retry() {
var lastValue = value; final lastValue = value.asSuccess;
assert(lastValue.hasError, ''); assert(lastValue.hasError, '');
lastValue = lastValue as Success<Key, Value>;
final nextPageKey = lastValue.nextPageKey; final nextPageKey = lastValue.nextPageKey;
// resetting the error // resetting the error
@@ -53,7 +80,7 @@ abstract class PagedValueNotifier<Key, Value>
abstract class PagedValue<Key, Value> with _$PagedValue<Key, Value> { abstract class PagedValue<Key, Value> with _$PagedValue<Key, Value> {
const PagedValue._(); const PagedValue._();
/// Creates a new instance of [PagedValue] with the given [key] and [value]. /// Represents the success state of the [PagedValue]
// @Assert( // @Assert(
// 'nextPageKey != null', // 'nextPageKey != null',
// 'Cannot set an error if all the pages are already fetched', // 'Cannot set an error if all the pages are already fetched',
@@ -69,24 +96,35 @@ abstract class PagedValue<Key, Value> with _$PagedValue<Key, Value> {
StreamChatError? error, StreamChatError? error,
}) = Success<Key, Value>; }) = Success<Key, Value>;
bool get hasNextPage { /// Represents the loading state of the [PagedValue].
assert(this is Success<Key, Value>, ''); const factory PagedValue.loading() = Loading;
return (this as Success<Key, Value>).nextPageKey != null;
/// 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<Key, Value>;
/// Returns the [PagedValue] as [Success].
Success<Key, Value> get asSuccess {
assert(
isSuccess,
'Cannot get asSuccess if the PagedValue is not in the Success state',
);
return this as Success<Key, Value>;
} }
bool get hasError { /// Returns `true` if the [PagedValue] is [Success]
assert(this is Success<Key, Value>, ''); /// and has more items to load.
return (this as Success<Key, Value>).error != null; 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 { int get itemCount {
assert(this is Success<Key, Value>, ''); final count = asSuccess.items.length;
final count = (this as Success<Key, Value>).items.length;
if (hasNextPage || hasError) return count + 1; if (hasNextPage || hasError) return count + 1;
return count; return count;
} }
const factory PagedValue.loading() = Loading;
const factory PagedValue.error(StreamChatError error) = Error;
} }
@@ -1,15 +1,40 @@
import 'dart:async';
import 'package:stream_chat/stream_chat.dart' hide Success; 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/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<int, Channel> { class StreamChannelListController extends PagedValueNotifier<int, Channel> {
/// Creates a [StreamChannelListController]. /// Creates a [StreamChannelListController].
StreamChannelListController({ StreamChannelListController({
required this.client, required this.client,
this.filter, this.filter,
this.sort, this.sort,
this.limit = 2, this.limit = defaultChannelPagedLimit,
this.messageLimit, this.messageLimit,
this.memberLimit, 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()); }) : super(const PagedValue.loading());
/// Creates a [StreamChannelListController] from the passed [value]. /// Creates a [StreamChannelListController] from the passed [value].
@@ -18,9 +43,22 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
required this.client, required this.client,
this.filter, this.filter,
this.sort, this.sort,
this.limit = 2, this.limit = defaultChannelPagedLimit,
this.messageLimit, this.messageLimit,
this.memberLimit, 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); }) : super(value);
/// The client to use for the channel list. /// The client to use for the channel list.
@@ -41,6 +79,82 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
/// The limit to apply to the member list. /// The limit to apply to the member list.
final int? memberLimit; 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 @override
Future<void> doInitialLoad() async { Future<void> doInitialLoad() async {
final limit = this.limit * defaultInitialPagedLimitMultiplier; final limit = this.limit * defaultInitialPagedLimitMultiplier;
@@ -58,15 +172,16 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
nextPageKey: nextKey, nextPageKey: nextKey,
); );
} }
} catch (error) { // start listening events
value = PagedValue.error(StreamChatError('error')); _subscribeToChannelListEvents();
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} }
} }
@override @override
Future<void> loadMore(int nextPageKey) async { Future<void> loadMore(int nextPageKey) async {
assert(value is Success<int, Channel>, ''); final previousValue = value.asSuccess;
final previousValue = value as Success<int, Channel>;
try { try {
await for (final channels in client.queryChannels( await for (final channels in client.queryChannels(
@@ -84,8 +199,90 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
nextPageKey: nextKey, nextPageKey: nextKey,
); );
} }
} catch (error) { } on StreamChatError catch (error) {
value = previousValue.copyWith(error: StreamChatError('error')); value = previousValue.copyWith(error: error);
} }
} }
/// Replaces the previously loaded channels with [channels] and updates
/// the nextPageKey.
set channels(List<Channel> channels) {
value = PagedValue(
items: channels,
nextPageKey: channels.length,
);
}
/// Returns/Creates a new Channel and starts watching it.
Future<Channel> getChannel({
required String id,
required String type,
}) async {
final channel = client.channel(type, id: id);
await channel.watch();
return channel;
}
StreamSubscription<Event>? _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<void>? resumeSignal]) {
_channelEventSubscription?.pause(resumeSignal);
}
/// Resumes all subscriptions added to this composite.
void resumeEventsSubscription() {
_channelEventSubscription?.resume();
}
@override
void dispose() {
_unsubscribeFromChannelListEvents();
super.dispose();
}
} }
@@ -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];
}
@@ -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/src/v4/channel_list_view/stream_channel_list_tile.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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 Widget defaultSeparatorBuilder(context, index) =>
/// list. const StreamChannelListSeparator();
///
/// 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.
typedef StreamChannelListViewItemBuilder = Widget Function( typedef StreamChannelListViewItemBuilder = Widget Function(
BuildContext context, BuildContext context,
Channel channel, Channel channel,
); );
typedef StreamChannelTapCallback = void Function(Channel); /// A [ListView] that shows a list of [Channel]s,
/// it uses [StreamChannelListTile] as a default item.
Widget _defaultSeparatorBuilder(context, index) => ///
const _ChannelListSeparator(); /// 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 { class StreamChannelListView extends StatefulWidget {
/// Creates a new instance of [StreamChannelListView].
const StreamChannelListView({ const StreamChannelListView({
Key? key, Key? key,
required this.controller, required this.controller,
this.itemBuilder, this.itemBuilder,
this.separatorBuilder = _defaultSeparatorBuilder, this.separatorBuilder = defaultSeparatorBuilder,
this.onChannelTap, this.onChannelTap,
this.onChannelLongPress, this.onChannelLongPress,
this.padding, this.padding,
@@ -51,21 +63,23 @@ class StreamChannelListView extends StatefulWidget {
this.restorationId, this.restorationId,
}) : super(key: key); }) : super(key: key);
/// The [StreamChannelListController] used to control the list of channels.
final StreamChannelListController controller; 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; final StreamChannelListViewItemBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final IndexedWidgetBuilder separatorBuilder; final IndexedWidgetBuilder separatorBuilder;
/// Called when the user taps this list tile. /// Called when the user taps this list tile.
/// final void Function(Channel)? onChannelTap;
/// Inoperative if [enabled] is false.
final StreamChannelTapCallback? onChannelTap;
/// Called when the user long-presses on this list tile. /// Called when the user long-presses on this list tile.
/// final void Function(Channel)? onChannelLongPress;
/// Inoperative if [enabled] is false.
final StreamChannelTapCallback? onChannelLongPress;
/// The amount of space by which to inset the children. /// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
@@ -254,11 +268,11 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
final newPageRequestTriggerIndex = value.itemCount - 3; final newPageRequestTriggerIndex = value.itemCount - 3;
final isBuildingTriggerIndexItem = final isBuildingTriggerIndexItem =
index == newPageRequestTriggerIndex; index == newPageRequestTriggerIndex;
if (value.hasNextPage && isBuildingTriggerIndexItem) { if (nextPageKey != null && isBuildingTriggerIndexItem) {
// Schedules the request for the end of this frame. // Schedules the request for the end of this frame.
WidgetsBinding.instance?.addPostFrameCallback((_) async { WidgetsBinding.instance?.addPostFrameCallback((_) async {
if (!value.hasError) { if (!value.hasError) {
await _controller.loadMore(nextPageKey!); await _controller.loadMore(nextPageKey);
} }
_hasRequestedNextPage = false; _hasRequestedNextPage = false;
}); });
@@ -267,15 +281,15 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
} }
if (index == channels.length) { if (index == channels.length) {
if (value.hasError) { if (error != null) {
return _ChannelListLoadMoreError( return ChannelListLoadMoreError(
onTap: _controller.retry, onTap: _controller.retry,
); );
} }
return const Center( return const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: _ChannelListLoadMoreIndicator(), child: ChannelListLoadMoreIndicator(),
), ),
); );
} }
@@ -309,8 +323,12 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
); );
} }
class _ChannelListLoadMoreIndicator extends StatelessWidget { /// A [StreamChannelListTile] that can be used in a [ListView] to show a
const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); /// 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 @override
Widget build(BuildContext context) => const SizedBox( Widget build(BuildContext context) => const SizedBox(
@@ -320,12 +338,16 @@ class _ChannelListLoadMoreIndicator extends StatelessWidget {
); );
} }
class _ChannelListLoadMoreError extends StatelessWidget { /// A [StreamChannelListTile] that is used to display the error indicator when
const _ChannelListLoadMoreError({ /// loading more channels fails.
class ChannelListLoadMoreError extends StatelessWidget {
/// Creates a new instance of [ChannelListLoadMoreError].
const ChannelListLoadMoreError({
Key? key, Key? key,
required this.onTap, required this.onTap,
}) : super(key: key); }) : super(key: key);
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback onTap; final GestureTapCallback onTap;
@override @override
@@ -355,8 +377,11 @@ class _ChannelListLoadMoreError extends StatelessWidget {
} }
} }
class _ChannelListSeparator extends StatelessWidget { /// A widget that is used to display a separator between
const _ChannelListSeparator({Key? key}) : super(key: key); /// [StreamChannelListTile] items.
class StreamChannelListSeparator extends StatelessWidget {
/// Creates a new instance of [StreamChannelListSeparator].
const StreamChannelListSeparator({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {