experiment with streambuilder

This commit is contained in:
Salvatore Giordano
2021-06-01 17:24:26 +02:00
parent 06bf2cdfe5
commit c10166dbff
13 changed files with 342 additions and 234 deletions
@@ -83,14 +83,14 @@ class ChannelImage extends StatelessWidget {
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
final channel = this.channel ?? StreamChannel.of(context).channel;
return StreamBuilder<Map<String, dynamic>>(
return BetterStreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, snapshot) {
String? image;
final chatThemeData = StreamChatTheme.of(context);
if (snapshot.data!.containsKey('image') == true) {
image = snapshot.data!['image'];
if (snapshot.containsKey('image') == true) {
image = snapshot['image'];
} else if (channel.state?.members.length == 2) {
final otherMember = channel.state?.members
.firstWhere((member) => member.user?.id != streamChat.user?.id);
@@ -153,9 +153,7 @@ class ChannelImage extends StatelessWidget {
imageUrl: image,
errorWidget: (_, __, ___) => Center(
child: Text(
snapshot.data?.containsKey('name') ?? false
? snapshot.data!['name'][0]
: '',
snapshot.containsKey('name') ? snapshot['name'][0] : '',
style: TextStyle(
color: chatThemeData.colorTheme.white,
fontWeight: FontWeight.bold,
@@ -25,13 +25,14 @@ class ChannelInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return StreamBuilder<List<Member>>(
stream: channel.state?.membersStream,
return BetterStreamBuilder<List<Member>>(
stream: channel.state!.membersStream,
initialData: channel.state!.members,
builder: (context, snapshot) => ConnectionStatusBuilder(
statusBuilder: (context, status) {
switch (status) {
case ConnectionStatus.connected:
return _buildConnectedTitleState(context, snapshot.data);
return _buildConnectedTitleState(context, snapshot);
case ConnectionStatus.connecting:
return _buildConnectingTitleState(context);
case ConnectionStatus.disconnected:
@@ -26,11 +26,14 @@ class ChannelName extends StatelessWidget {
final client = StreamChat.of(context);
final channel = StreamChannel.of(context).channel;
return StreamBuilder<Map<String, dynamic>>(
return BetterStreamBuilder<Map<String, Object?>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, snapshot) =>
_buildName(snapshot.data!, channel.state?.members, client),
builder: (context, snapshot) => _buildName(
snapshot,
channel.state?.members,
client,
),
);
}
@@ -13,14 +13,14 @@ class ConnectionStatusBuilder extends StatelessWidget {
const ConnectionStatusBuilder({
Key? key,
required this.statusBuilder,
this.initialStatus = ConnectionStatus.disconnected,
this.initialStatus,
this.connectionStatusStream,
this.errorBuilder,
this.loadingBuilder,
}) : super(key: key);
/// The connection status that will be used to create the initial snapshot.
final ConnectionStatus initialStatus;
final ConnectionStatus? initialStatus;
/// The asynchronous computation to which this builder is currently connected.
final Stream<ConnectionStatus>? connectionStatusStream;
@@ -37,23 +37,19 @@ class ConnectionStatusBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
final stream = connectionStatusStream ??
StreamChat.of(context).client.wsConnectionStatusStream;
return StreamBuilder<ConnectionStatus>(
final client = StreamChat.of(context).client;
final stream = connectionStatusStream ?? client.wsConnectionStatusStream;
return BetterStreamBuilder<ConnectionStatus>(
initialData: initialStatus ?? client.wsConnectionStatus,
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (errorBuilder != null) {
return errorBuilder!(context, snapshot.error);
}
return const Offstage();
loadingBuilder: loadingBuilder,
errorBuilder: (context, error) {
if (errorBuilder != null) {
return errorBuilder!(context, error);
}
if (!snapshot.hasData) {
if (loadingBuilder != null) return loadingBuilder!(context);
return const Offstage();
}
return statusBuilder(context, snapshot.data!);
return const Offstage();
},
builder: statusBuilder,
);
}
}
@@ -606,8 +606,8 @@ class _MessageListViewState extends State<MessageListView> {
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2(
streamChannel!.channel.state!.isUpToDateStream,
streamChannel!.channel.state!.unreadCountStream,
streamChannel!.channel.state!.isUpToDateStream.distinct(),
streamChannel!.channel.state!.unreadCountStream.distinct(),
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
),
builder: (_, snapshot) {
@@ -689,23 +689,18 @@ class _MessageListViewState extends State<MessageListView> {
final stream = direction == QueryDirection.top
? streamChannel.queryTopMessages
: streamChannel.queryBottomMessages;
return StreamBuilder<bool>(
return BetterStreamBuilder<bool>(
key: Key('LOADING-INDICATOR $direction'),
stream: stream,
initialData: false,
errorBuilder: (context, error) => Container(
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2),
child: const Center(
child: Text('Error loading messages'),
),
),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: const Center(
child: Text('Error loading messages'),
),
);
}
if (!snapshot.data!) {
if (!snapshot) {
if (!_isThreadConversation && direction == QueryDirection.top) {
return const SizedBox(
height: 52,
@@ -1163,14 +1158,14 @@ class _MessageListViewState extends State<MessageListView> {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamBuilder<Message>(
builder: (_) => BetterStreamBuilder<Message>(
stream: streamChannel!.channel.state!.messagesStream.map(
(messages) =>
messages!.firstWhere((m) => m.id == message.id)),
initialData: message,
builder: (_, snapshot) => StreamChannel(
channel: streamChannel!.channel,
child: widget.threadBuilder!(context, snapshot.data),
child: widget.threadBuilder!(context, snapshot),
),
),
),
@@ -449,182 +449,189 @@ class _MessageWidgetState extends State<MessageWidget>
final leftPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
return Portal(
child: GestureDetector(
onTap: () {
widget.onMessageTap!(widget.message);
},
onLongPress: widget.message.isDeleted && !isFailedState
? null
: () => onLongPress(context),
child: Padding(
padding: widget.padding ?? const EdgeInsets.all(8),
child: FractionallySizedBox(
alignment:
widget.reverse ? Alignment.centerRight : Alignment.centerLeft,
widthFactor: 0.78,
child: Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Stack(
clipBehavior: Clip.none,
alignment: widget.reverse
? AlignmentDirectional.bottomEnd
: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show &&
widget.message.user != null) ...[
_buildUserAvatar(),
const SizedBox(width: 4),
],
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(width: avatarWidth + 4),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0),
constraints:
const BoxConstraints(maxWidth: 22 * 6.0),
child: _buildReactionIndicator(context),
),
portalAnchor:
Alignment(widget.reverse ? 1 : -1, -1),
childAnchor:
Alignment(widget.reverse ? -1 : 1, -1),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget.message.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric(
horizontal:
return Material(
type: MaterialType.transparency,
child: Portal(
child: InkWell(
onTap: () {
widget.onMessageTap!(widget.message);
},
onLongPress: widget.message.isDeleted && !isFailedState
? null
: () => onLongPress(context),
child: Padding(
padding: widget.padding ?? const EdgeInsets.all(8),
child: FractionallySizedBox(
alignment:
widget.reverse ? Alignment.centerRight : Alignment.centerLeft,
widthFactor: 0.78,
child: Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Stack(
clipBehavior: Clip.none,
alignment: widget.reverse
? AlignmentDirectional.bottomEnd
: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show &&
widget.message.user != null) ...[
_buildUserAvatar(),
const SizedBox(width: 4),
],
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(width: avatarWidth + 4),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0),
constraints: const BoxConstraints(
maxWidth: 22 * 6.0),
child: _buildReactionIndicator(context),
),
portalAnchor:
Alignment(widget.reverse ? 1 : -1, -1),
childAnchor:
Alignment(widget.reverse ? -1 : 1, -1),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget
.message
.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric(
horizontal:
// ignore: lines_longer_than_80_chars
widget.showUserAvatar ==
// ignore: lines_longer_than_80_chars
DisplayWidget
.gone
? 0
: 4.0),
child: DeletedMessage(
borderRadiusGeometry: widget
.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Card(
clipBehavior: Clip.antiAlias,
elevation: 0,
margin: EdgeInsets.symmetric(
horizontal: (isFailedState
? 15.0
: 0.0) +
// ignore: lines_longer_than_80_chars
widget.showUserAvatar ==
// ignore: lines_longer_than_80_chars
(widget.showUserAvatar ==
DisplayWidget.gone
? 0
: 4.0),
child: DeletedMessage(
borderRadiusGeometry:
widget.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Card(
clipBehavior: Clip.antiAlias,
elevation: 0,
margin: EdgeInsets.symmetric(
horizontal: (isFailedState
? 15.0
: 0.0) +
// ignore: lines_longer_than_80_chars
(widget.showUserAvatar ==
DisplayWidget.gone
? 0
: 4.0),
),
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: widget
// ignore: lines_longer_than_80_chars
.messageTheme
// ignore: lines_longer_than_80_chars
.messageBorderColor ??
Colors.grey,
),
borderRadius: widget
// ignore: lines_longer_than_80_chars
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (hasQuotedMessage)
_buildQuotedMessage(),
if (hasNonUrlAttachments)
_parseAttachments(),
if (!isGiphy)
_buildTextBubble(),
],
),
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: widget
// ignore: lines_longer_than_80_chars
.messageTheme
// ignore: lines_longer_than_80_chars
.messageBorderColor ??
Colors.grey,
),
borderRadius: widget
// ignore: lines_longer_than_80_chars
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
if (hasQuotedMessage)
_buildQuotedMessage(),
if (hasNonUrlAttachments)
_parseAttachments(),
if (!isGiphy)
_buildTextBubble(),
],
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: widget.reverse ? null : 4,
left: widget.reverse ? 4 : null,
top: -8,
child: CustomPaint(
painter: ReactionBubblePainter(
StreamChatTheme.of(context)
.colorTheme
.white,
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: widget.reverse ? null : 4,
left: widget.reverse ? 4 : null,
top: -8,
child: CustomPaint(
painter: ReactionBubblePainter(
StreamChatTheme.of(context)
.colorTheme
.white,
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
),
),
),
],
],
),
),
),
),
],
],
),
if (showBottomRow)
SizedBox(height: context.textScaleFactor * 18.0),
],
),
if (showBottomRow)
Padding(
padding: EdgeInsets.only(left: leftPadding),
child: _bottomRow,
),
if (showBottomRow)
SizedBox(height: context.textScaleFactor * 18.0),
],
),
if (showBottomRow)
Padding(
padding: EdgeInsets.only(left: leftPadding),
child: _bottomRow,
),
if (isFailedState)
Positioned(
left: widget.reverse ? 0 : null,
right: widget.reverse ? null : 0,
bottom: showBottomRow ? 18 : -2,
child: StreamSvgIcon.error(size: 20),
),
],
),
],
if (isFailedState)
Positioned(
left: widget.reverse ? 0 : null,
right: widget.reverse ? null : 0,
bottom: showBottomRow ? 18 : -2,
child: StreamSvgIcon.error(size: 20),
),
],
),
],
),
),
),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget to show the current list of typing users
@@ -41,11 +42,12 @@ class TypingIndicator extends StatelessWidget {
child: alternativeWidget ?? const Offstage(),
),
);
return StreamBuilder<List<User>>(
return BetterStreamBuilder<List<User>>(
initialData: channelState.typingEvents,
stream: channelState.typingEventsStream,
builder: (context, snapshot) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: snapshot.data?.isNotEmpty == true
child: snapshot.isNotEmpty == true
? Padding(
padding: padding,
child: Align(
@@ -61,7 +63,7 @@ class TypingIndicator extends StatelessWidget {
),
Text(
// ignore: lines_longer_than_80_chars
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing',
maxLines: 1,
style: style,
),
@@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget {
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return IgnorePointer(
child: StreamBuilder<int?>(
child: BetterStreamBuilder<int?>(
stream: cid != null
? client.state.channels[cid]?.state?.unreadCountStream
: client.state.totalUnreadCountStream,
@@ -25,8 +25,8 @@ class UnreadIndicator extends StatelessWidget {
? client.state.channels[cid]?.state?.unreadCount
: client.state.totalUnreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return const SizedBox();
if (snapshot == null || snapshot == 0) {
return const Offstage();
}
return Material(
borderRadius: BorderRadius.circular(8),
@@ -42,7 +42,7 @@ class UnreadIndicator extends StatelessWidget {
),
child: Center(
child: Text(
'${snapshot.data! > 99 ? '99+' : snapshot.data}',
'${snapshot > 99 ? '99+' : snapshot}',
style: const TextStyle(
fontSize: 11,
color: Colors.white,
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';