feat(ui, core): add remaining v4 list-views

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2022-04-19 12:38:21 +05:30
committed by xsahil03x
parent 2323c59ed4
commit f074bccdc5
20 changed files with 2227 additions and 259 deletions
@@ -0,0 +1,297 @@
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<T> = Widget Function(
BuildContext context,
List<T> 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<K, V> 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<K, V> 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<V> itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueListViewIndexedWidgetBuilder<V> 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<PagedValueListView<K, V>> createState() =>
_PagedValueListViewState<K, V>();
}
class _PagedValueListViewState<K, V> extends State<PagedValueListView<K, V>> {
PagedValueNotifier<K, V> get _controller => widget.controller;
// Avoids duplicate requests on rebuilds.
bool _hasRequestedNextPage = false;
@override
void initState() {
super.initState();
_controller.doInitialLoad();
}
@override
void didUpdateWidget(covariant PagedValueListView<K, V> oldWidget) {
super.didUpdateWidget(oldWidget);
if (_controller != oldWidget.controller) {
// reset duplicate requests flag
_hasRequestedNextPage = false;
_controller.doInitialLoad();
}
}
@override
Widget build(BuildContext context) => PagedValueListenableBuilder<K, V>(
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),
),
);
}
@@ -0,0 +1,205 @@
import 'dart:async';
import 'dart:math';
import 'package:stream_chat/stream_chat.dart' hide Success;
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
/// The default channel page limit to load.
const defaultMessageSearchPagedLimit = 10;
const _kDefaultBackendPaginationLimit = 30;
/// A controller for a user list.
///
/// This class lets you perform tasks such as:
/// * Load initial data.
/// * Load more data using [loadMore].
/// * Replace the previously loaded users.
class StreamMessageSearchListController
extends PagedValueNotifier<String, GetMessageResponse> {
/// Creates a Stream user list controller.
///
/// * `client` is the Stream chat client to use for the channels list.
///
/// * `filter` is the query filters to use.
///
/// * `sort` is the sorting used for the users matching the filters.
///
/// * `presence` sets whether you'll receive user presence updates via the
/// websocket events.
///
/// * `limit` is the limit to apply to the user list.
StreamMessageSearchListController({
required this.client,
required this.filter,
this.messageFilter,
this.searchQuery,
this.sort,
this.limit = defaultMessageSearchPagedLimit,
}) : assert(
messageFilter != null || searchQuery != null,
'Either messageFilter or searchQuery must be provided',
),
assert(
messageFilter == null || searchQuery == null,
'Only one of messageFilter or searchQuery can be provided',
),
_activeFilter = filter,
_activeMessageFilter = messageFilter,
_activeSearchQuery = searchQuery,
_activeSort = sort,
super(const PagedValue.loading());
/// Creates a [StreamUserListController] from the passed [value].
StreamMessageSearchListController.fromValue(
PagedValue<String, GetMessageResponse> value, {
required this.client,
required this.filter,
this.messageFilter,
this.searchQuery,
this.sort,
this.limit = defaultMessageSearchPagedLimit,
}) : assert(
messageFilter != null || searchQuery != null,
'Either messageFilter or searchQuery must be provided',
),
assert(
messageFilter == null || searchQuery == null,
'Only one of messageFilter or searchQuery can be provided',
),
_activeFilter = filter,
_activeMessageFilter = messageFilter,
_activeSearchQuery = searchQuery,
_activeSort = sort,
super(value);
/// The client to use for the channels list.
final StreamChatClient client;
/// The query filters to use.
///
/// You can query on any of the custom fields you've defined on the [User].
///
/// You can also filter other built-in channel fields.
final Filter filter;
Filter _activeFilter;
/// The message query filters to use.
///
/// You can query on any of the custom fields you've defined on the [Channel].
///
/// You can also filter other built-in channel fields.
final Filter? messageFilter;
Filter? _activeMessageFilter;
/// Message String to search on.
final String? searchQuery;
String? _activeSearchQuery;
/// The sorting used for the users matching the filters.
///
/// Sorting is based on field and direction, multiple sorting options
/// can be provided.
///
/// Direction can be ascending or descending.
final List<SortOption>? sort;
List<SortOption>? _activeSort;
/// The limit to apply to the user list. The default is set to
/// [defaultUserPagedLimit].
final int limit;
/// Allows for the change of filters used for user queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set filter(Filter value) => _activeFilter = value;
/// Allows for the change of message filters used for user queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set messageFilter(Filter? value) => _activeMessageFilter = value;
/// Allows for the change of filters used for user queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set searchQuery(String? value) => _activeSearchQuery = value;
/// Allows for the change of the query sort used for user queries.
///
/// Use this if you need to support runtime sort changes,
/// through custom sort UI.
set sort(List<SortOption>? value) => _activeSort = value;
@override
Future<void> doInitialLoad() async {
final limit = min(
this.limit * defaultInitialPagedLimitMultiplier,
_kDefaultBackendPaginationLimit,
);
try {
final response = await client.search(
_activeFilter,
sort: _activeSort,
query: _activeSearchQuery,
messageFilters: _activeMessageFilter,
paginationParams: PaginationParams(limit: limit),
);
final results = response.results;
final nextKey = response.next;
value = PagedValue(
items: results,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = PagedValue.error(chatError);
}
}
@override
Future<void> loadMore(String nextPageKey) async {
final previousValue = value.asSuccess;
try {
final response = await client.search(
_activeFilter,
sort: _activeSort,
query: _activeSearchQuery,
messageFilters: _activeMessageFilter,
paginationParams: PaginationParams(limit: limit, next: nextPageKey),
);
final results = response.results;
final previousItems = previousValue.items;
final newItems = previousItems + results;
final next = response.next;
final nextKey = next != null && next.isNotEmpty ? next : null;
value = PagedValue(
items: newItems,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = previousValue.copyWith(error: error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = previousValue.copyWith(error: chatError);
}
}
@override
Future<void> refresh({bool resetValue = true}) {
if (resetValue) {
_activeFilter = filter;
_activeMessageFilter = messageFilter;
_activeSearchQuery = searchQuery;
_activeSort = sort;
}
return super.refresh(resetValue: resetValue);
}
}
@@ -0,0 +1,165 @@
import 'dart:async';
import 'dart:math';
import 'package:stream_chat/stream_chat.dart' hide Success;
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
/// The default channel page limit to load.
const defaultUserPagedLimit = 10;
const _kDefaultBackendPaginationLimit = 30;
/// A controller for a user list.
///
/// This class lets you perform tasks such as:
/// * Load initial data.
/// * Load more data using [loadMore].
/// * Replace the previously loaded users.
class StreamUserListController extends PagedValueNotifier<int, User> {
/// Creates a Stream user list controller.
///
/// * `client` is the Stream chat client to use for the channels list.
///
/// * `filter` is the query filters to use.
///
/// * `sort` is the sorting used for the users matching the filters.
///
/// * `presence` sets whether you'll receive user presence updates via the
/// websocket events.
///
/// * `limit` is the limit to apply to the user list.
StreamUserListController({
required this.client,
this.filter,
this.sort,
this.presence = true,
this.limit = defaultUserPagedLimit,
}) : _activeFilter = filter,
_activeSort = sort,
super(const PagedValue.loading());
/// Creates a [StreamUserListController] from the passed [value].
StreamUserListController.fromValue(
PagedValue<int, User> value, {
required this.client,
this.filter,
this.sort,
this.presence = true,
this.limit = defaultUserPagedLimit,
}) : _activeFilter = filter,
_activeSort = sort,
super(value);
/// The client to use for the channels list.
final StreamChatClient client;
/// The query filters to use.
///
/// You can query on any of the custom fields you've defined on the [User].
///
/// You can also filter other built-in channel fields.
final Filter? filter;
Filter? _activeFilter;
/// The sorting used for the users matching the filters.
///
/// Sorting is based on field and direction, multiple sorting options
/// can be provided.
///
/// Direction can be ascending or descending.
final List<SortOption>? sort;
List<SortOption>? _activeSort;
/// If true youll receive user presence updates via the websocket events
final bool presence;
/// The limit to apply to the user list. The default is set to
/// [defaultUserPagedLimit].
final int limit;
/// Allows for the change of filters used for user queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set filter(Filter? value) => _activeFilter = value;
/// Allows for the change of the query sort used for user queries.
///
/// Use this if you need to support runtime sort changes,
/// through custom sort UI.
set sort(List<SortOption>? value) => _activeSort = value;
@override
Future<void> doInitialLoad() async {
final limit = min(
this.limit * defaultInitialPagedLimitMultiplier,
_kDefaultBackendPaginationLimit,
);
try {
final userResponse = await client.queryUsers(
filter: _activeFilter,
sort: _activeSort,
presence: presence,
pagination: PaginationParams(limit: limit),
);
final users = userResponse.users;
final nextKey = users.length < limit ? null : users.length;
value = PagedValue(
items: users,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = PagedValue.error(chatError);
}
}
@override
Future<void> loadMore(int nextPageKey) async {
final previousValue = value.asSuccess;
try {
final userResponse = await client.queryUsers(
filter: _activeFilter,
sort: _activeSort,
presence: presence,
pagination: PaginationParams(limit: limit, offset: nextPageKey),
);
final users = userResponse.users;
final previousItems = previousValue.items;
final newItems = previousItems + users;
final nextKey = users.length < limit ? null : newItems.length;
value = PagedValue(
items: newItems,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = previousValue.copyWith(error: error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = previousValue.copyWith(error: chatError);
}
}
@override
Future<void> refresh({bool resetValue = true}) {
if (resetValue) {
_activeFilter = filter;
_activeSort = sort;
}
return super.refresh(resetValue: resetValue);
}
/// Replaces the previously loaded users with [users] and updates
/// the nextPageKey.
set users(List<User> users) {
value = PagedValue(
items: users,
nextPageKey: users.length,
);
}
}
@@ -12,11 +12,14 @@ 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/stream_channel.dart';
export 'src/stream_channel_list_controller.dart';
export 'src/stream_channel_list_event_handler.dart';
export 'src/stream_chat_core.dart';
export 'src/stream_message_search_list_controller.dart';
export 'src/stream_user_list_controller.dart';
export 'src/typedef.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';