I've made my own social media app with Flutter and it has been awesome, however, I'm struggling to stop the chat messages from rebuilding with my paginated firebase list. I've added print statements and refactored everything to stateless widgets or stateful widgets with AutomaticKeepAliveClientMixins, but to no avail.
The problem is worst with link previews and video thumbnails as these disappear and reappear constantly while scrolling, leading to an awful experience.
Paginated List Code Below:
#override
Widget build(BuildContext context) {
return BlocBuilder<PaginationCubit, PaginationState>(
bloc: _cubit,
builder: (context, state) {
if (state is PaginationInitial) {
return widget.initialLoader;
} else if (state is PaginationError) {
return (widget.onError != null)
? widget.onError!(state.error)
: Container();
} else {
final loadedState = state as PaginationLoaded;
if (widget.onLoaded != null) {
widget.onLoaded!(loadedState);
}
if (loadedState.hasReachedEnd && widget.onReachedEnd != null) {
widget.onReachedEnd!(loadedState);
}
if (loadedState.messages.isEmpty) {
return widget.emptyDisplay;
}
return _buildListView(loadedState);
}
},
);
}
Widget _buildListView(PaginationLoaded loadedState) {
return Padding(
padding: widget.padding,
child: ListView.builder(
itemCount: max(
0,
(loadedState.hasReachedEnd
? loadedState.messages.length
: loadedState.messages.length + 1) *
2 -
1),
itemBuilder: (context, index) {
final itemIndex = index ~/ 2;
if (index.isEven) {
if (itemIndex >= loadedState.messages.length) {
_cubit.fetchPaginatedList();
return widget.bottomLoader;
}
return widget.itemBuilder(
itemIndex,
context,
loadedState.messages[itemIndex], // current message
loadedState.messages[itemIndex == loadedState.messages.length - 1
? loadedState.messages.length - 1
: (itemIndex + 1)], // previous message
loadedState.messages[
itemIndex == 0 ? 0 : (itemIndex - 1)], // next message
loadedState.messages.length,
);
}
return widget.separator!(
itemIndex,
context,
loadedState.messages[itemIndex],
);
},
),
);
}
The main message widget
class MessageItem extends StatefulWidget {
final Message message;
final Message previousMessage;
final Message previousUserMessage;
final Message nextMessage;
final Chat chat;
final bool showReactionPickerIndicator;
final bool showReaction;
final Function? onMoveScroll;
const MessageItem({
Key? key,
required this.message,
required this.previousMessage,
required this.previousUserMessage,
required this.nextMessage,
required this.chat,
this.showReaction = true,
this.showReactionPickerIndicator = false,
this.onMoveScroll,
}) : super(key: key);
MessageItem copyWith(
{Key? key,
Message? message,
bool? showReactionPickerIndicator,
bool? showReaction}) {
return MessageItem(
key: key ?? this.key,
message: message ?? this.message,
previousMessage: previousMessage,
previousUserMessage: previousUserMessage,
nextMessage: nextMessage,
showReaction: showReaction ?? this.showReaction,
showReactionPickerIndicator:
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
chat: chat);
}
#override
State<MessageItem> createState() => _MessageItemState();
}
class _MessageItemState extends State<MessageItem>
with AutomaticKeepAliveClientMixin {
final messageController = MessageController.to;
#override
bool get wantKeepAlive => true;
late final bool currentUserIsAuthor;
late final bool nextMessageIsOwned;
late final bool lastMessageIsOwned;
late final bool isLastMessage;
late final bool showReactionWidget;
late final bool showAuthorName;
late final bool showNip;
late final bool showAvatar;
late final bool alignWithAvatar;
#override
void initState() {
debugPrint('[MessageItem] - initState');
if (widget.onMoveScroll != null) {
widget.onMoveScroll!();
}
final currentUser = getCurrentUser()!;
// Message setup booleans
currentUserIsAuthor = widget.message.authorId == currentUser.uid;
nextMessageIsOwned = widget.message.authorId == widget.nextMessage.authorId;
lastMessageIsOwned =
widget.message.authorId == widget.previousMessage.authorId;
isLastMessage = widget.nextMessage.id == widget.message.id;
// Message ui booleans
showReactionWidget =
widget.showReaction && widget.message.reactions.isNotEmpty;
showAuthorName = widget.chat.chatType == ChatType.GROUP &&
!currentUserIsAuthor &&
!lastMessageIsOwned;
showNip = widget.message == widget.nextMessage ||
widget.nextMessage.authorId != widget.message.authorId;
showAvatar = showNip && widget.chat.chatType == ChatType.GROUP;
alignWithAvatar = widget.chat.chatType == ChatType.GROUP && !showNip;
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return SwipeTo(
iconOnRightSwipe: CupertinoIcons.reply_thick_solid,
onRightSwipe: () {
messageController.replyMessage.value = widget.message;
},
child: Material(
type: MaterialType.transparency,
child: Portal(
child: FocusedMenuWrapper(
items: messageMenuItems(
context,
widget.message,
widget.previousUserMessage,
isLastMessage,
currentUserIsAuthor,
widget.chat,
widget.copyWith(
key: const Key('MessageWidget'),
showReactionPickerIndicator: true,
showReaction: false),
),
child: Padding(
padding: _calculateMessagePadding(
lastMessageIsOwned, nextMessageIsOwned),
child: FractionallySizedBox(
alignment: currentUserIsAuthor
? Alignment.centerRight
: Alignment.centerLeft,
widthFactor: 0.8,
child: Column(
crossAxisAlignment: currentUserIsAuthor
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (showAvatar && !currentUserIsAuthor)
MessageUserAvatar(
userName: widget.message.authorName,
userProfile: widget.message.authorProfile),
Flexible(
child: Padding(
padding: EdgeInsets.only(
left: !currentUserIsAuthor && alignWithAvatar
? 26.w
: 0,
right: currentUserIsAuthor && alignWithAvatar
? 26.w
: 0),
child: ReactionPortalWrapper(
showReactionWidget: showReactionWidget,
showReaction: widget.showReaction,
message: widget.message,
chat: widget.chat,
isCurrentUserAuthor: currentUserIsAuthor,
messageWidget: widget,
showAuthorName: showAuthorName,
showNip: showNip,
showReactionPickerWidget:
widget.showReactionPickerIndicator,
),
)),
if (showAvatar && currentUserIsAuthor)
MessageUserAvatar(
userName: widget.message.authorName,
userProfile: getCurrentUser()!.profileImg),
],
)
],
)),
),
),
),
),
);
}
}
The Message Bubble (where I suspect something is going wrong)
class MessageBubble extends StatelessWidget {
final bool showNip;
final bool isCurrentUserAuthor;
final Message message;
final Chat chat;
const MessageBubble(
{Key? key,
required this.showNip,
required this.isCurrentUserAuthor,
required this.message,
required this.chat})
: super(key: key);
#override
Widget build(BuildContext context) {
return Bubble(
elevation: 0,
nip: showNip
? isCurrentUserAuthor
? BubbleNip.rightBottom
: BubbleNip.leftBottom
: BubbleNip.no,
radius: Radius.circular(24.r),
nipHeight: 12.h,
nipOffset: 5.h,
nipRadius: 2,
child: message.replyMessage != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReplyMessageBody(context, isCurrentUserAuthor, message),
MessageBubbleBody(message: message, chat: chat)
],
)
: MessageBubbleBody(message: message, chat: chat));
}
Widget _buildReplyMessageBody(
BuildContext context, bool currentUserIsAuthor, Message? message) {
return Container(
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.white54
: Colors.black54,
borderRadius: BorderRadius.circular(8.r),
),
child: ReplyMessage(chat: chat, message: message!),
);
}
}
And the bubble body
class MessageBodyWithTimestamp extends StatelessWidget {
final Widget body;
final Widget timestamp;
const MessageBodyWithTimestamp(
{Key? key, required this.body, required this.timestamp})
: super(key: key);
#override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
body,
timestamp,
],
),
);
}
}
class MessageBubbleBody extends StatelessWidget {
final Message message;
final Chat chat;
const MessageBubbleBody({Key? key, required this.message, required this.chat})
: super(key: key);
#override
Widget build(BuildContext context) {
if (message.sendStatus == SendStatus.ERROR) {
return const SizedBox.shrink();
}
if (message.messageType == MessageType.MEDIA) {
if (message.duration != null) {
return MediaTimerMessage(message: message, chatId: chat.id);
} else {
return Stack(
fit: StackFit.loose,
children: [
MediaMessage(message: message, chatId: chat.id),
Positioned(
bottom: 15.h,
right: 15.w,
child: _buildTimestampReadContent(context))
],
);
}
} else if (message.messageType == MessageType.STICKER) {
return Stack(
fit: StackFit.loose,
children: [
StickerMessage(message: message, chatId: chat.id),
Positioned(
bottom: 15.h,
right: 15.w,
child: _buildTimestampReadContent(context)),
],
);
} else if (message.messageType == MessageType.AUDIO) {
if (message.mediaStatus == MediaStatus.UPLOADING) {
return MessageBodyWithTimestamp(
body: const AudioUploadingMessage(),
timestamp: _buildTimestampReadContent(context));
} else {
return MessageBodyWithTimestamp(
body: AudioMessage(message: message),
timestamp: _buildTimestampReadContent(context));
}
} else if (message.messageType == MessageType.FILE) {
return MessageBodyWithTimestamp(
body: FileMessage(message: message, chatId: chat.id),
timestamp: _buildTimestampReadContent(context));
} else {
return MessageBodyWithTimestamp(
body: TextMessage(message: message, chatId: chat.id),
timestamp: _buildTimestampReadContent(context));
}
}
}
And finally (sorry I know this is getting very long)... the text message with link preview
class TextMessage extends StatefulWidget {
final Message message;
final String chatId;
const TextMessage({Key? key, required this.message, required this.chatId})
: super(key: key);
#override
State<TextMessage> createState() => _TextMessageState();
}
class _TextMessageState extends State<TextMessage>
with AutomaticKeepAliveClientMixin {
late final List<LinkifyElement> elements;
late final List<LinkifyElement> links;
#override
bool get wantKeepAlive => true;
#override
void initState() {
print('Building text message');
elements = linkify(
widget.message.body.body,
options: const LinkifyOptions(humanize: false),
);
links = elements
.where((element) =>
element is LinkableElement &&
!element.text.contains('media.giphy.com'))
.toList();
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
bool isUserSender = widget.message.authorId == getCurrentUser()!.uid;
if (links.isNotEmpty) {
return TextMessageWithLink(
elements: elements, isUserSender: isUserSender);
} else {
return Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 25.w),
child: Padding(
padding: const EdgeInsets.all(3),
child: Align(
alignment: Alignment.topLeft,
child: RichText(
textScaleFactor: MediaQuery.of(context).textScaleFactor,
text: buildTextSpan(
elements,
style: AppTextStyles.messageTextStyle(context, isUserSender),
onOpen: (link) async {
_launch(link.url);
},
linkStyle: AppTextStyles.linkTextStyle(context, isUserSender),
),
),
),
),
),
);
}
}
}
With the preview...
class TextMessageWithLink extends StatelessWidget {
final List<LinkifyElement> elements;
final bool isUserSender;
const TextMessageWithLink(
{Key? key, required this.elements, required this.isUserSender})
: super(key: key);
#override
Widget build(BuildContext context) {
return Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 25.w),
child: Padding(
padding: const EdgeInsets.all(3),
child: Align(
alignment: Alignment.topLeft,
child: Column(
children: List.generate(elements.length, (index) {
if (elements[index] is LinkableElement &&
elements[index].text.contains('media.giphy.com') == false) {
return Column(
children: [
_buildUrlPreview(context, isUserSender, index),
GestureDetector(
onTap: () async {
_launch(elements[index].text);
},
child: Text(elements[index].text,
style: AppTextStyles.linkTextStyle(
context, isUserSender)),
)
],
);
} else if (elements[index].text.contains('media.giphy.com')) {
return GestureDetector(
onTap: () async {
_launch(elements[index].text);
},
child: Text(elements[index].text,
style:
AppTextStyles.linkTextStyle(context, isUserSender)),
);
} else {
return Text(elements[index].text,
style: AppTextStyles.messageTextStyle(
context, isUserSender));
}
}),
),
),
),
),
);
}
Widget _buildUrlPreview(BuildContext context, bool isUserSender, int index) {
return SimpleUrlPreview(
url: elements[index].text,
titleStyle: AppTextStyles.messageTextStyle(context, isUserSender)
.copyWith(fontWeight: FontWeight.w600),
descriptionStyle: AppTextStyles.messageTextStyle(context, isUserSender),
bgColor: CupertinoDynamicColor.resolve(
CupertinoColors.tertiarySystemGroupedBackground, context),
isClosable: false,
imageLoaderColor:
CupertinoDynamicColor.resolve(CupertinoColors.label, context),
previewHeight: 130,
);
}
}
Thanks in advance if you can figure it out ;)
Related
I'm new to Flutter and followed this tutorial https://suragch.medium.com/background-audio-in-flutter-with-audio-service-and-just-audio-3cce17b4a7d to setup Just Audio and audioservice. I'm building my app for Android at the moment.
I'm having 3 issues:
The main problem is that when I use context.go (from GoRouter) to move to the player screen, audio should start playing, but it doesn't, I have to manually press the Play button.
Additionaly when I load a playlist and press Next, it also doesn't start playing automatically.
Finally, sometimes if I leave the player for other screen and come back, I can't press the Play button.
Here are the 3 files I'm using:
Player screen:
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:podfic_app/screens/chapterlist_screen.dart';
import 'package:podfic_app/utils/api_service.dart';
import './just_audio/notifiers/play_button_notifier.dart';
import './just_audio/notifiers/progress_notifier.dart';
import './just_audio/page_manager.dart';
import './just_audio/services/service_locator.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
class Player extends StatefulWidget {
const Player({super.key});
#override
State<Player> createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
#override
void initState() {
super.initState();
getIt<PageManager>().init();
}
#override
void dispose() {
getIt<PageManager>().dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// startPlaying();
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: [
// const SizedBox(height: 10),
// back button and menu button
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop()),
const Text('Listening to'),
IconButton(
icon: const Icon(Icons.playlist_play_outlined),
onPressed: () => context.push('/chapters')),
],
),
const SizedBox(height: 25),
// cover art, artist name, song name
const CurrentMetadata(),
const SizedBox(height: 30),
const SizedBox(height: 20),
// linear bar
const AudioProgressBar(),
const SizedBox(height: 30),
// previous song, pause play, skip next song
Align(
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(0),
child: Stack(children: [
Container(
child: const ChangeSpeedButton(),
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
),
]),
),
),
],
),
),
),
);
}
}
// class CurrentSongTitle extends StatelessWidget {
// const CurrentSongTitle({Key? key}) : super(key: key);
// #override
// Widget build(BuildContext context) {
// final pageManager = getIt<PageManager>();
// return ValueListenableBuilder<String>(
// valueListenable: pageManager.currentSongTitleNotifier,
// builder: (_, title, __) {
// return Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Text(title, style: const TextStyle(fontSize: 40)),
// );
// },
// );
// }
// }
class AddRemoveSongButtons extends StatelessWidget {
const AddRemoveSongButtons({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
onPressed: pageManager.add,
child: const Icon(Icons.add),
),
FloatingActionButton(
onPressed: pageManager.remove,
child: const Icon(Icons.remove),
),
],
),
);
}
}
class AudioProgressBar extends StatelessWidget {
const AudioProgressBar({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ProgressBarState>(
valueListenable: pageManager.progressNotifier,
builder: (_, value, __) {
return ProgressBar(
progress: value.current,
buffered: value.buffered,
total: value.total,
onSeek: pageManager.seek,
);
},
);
}
}
class AudioControlButtons extends StatelessWidget {
const AudioControlButtons({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
);
}
}
class PreviousSongButton extends StatelessWidget {
const PreviousSongButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isFirstSongNotifier,
builder: (_, isFirst, __) {
return IconButton(
icon: const Icon(Icons.skip_previous),
iconSize: 32,
onPressed: (isFirst) ? null : pageManager.previous,
);
},
);
}
}
class PlayButton extends StatelessWidget {
const PlayButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ButtonState>(
valueListenable: pageManager.playButtonNotifier,
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return Container(
margin: const EdgeInsets.all(8.0),
width: 48.0,
height: 48.0,
child: const CircularProgressIndicator(),
);
case ButtonState.paused:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.play_arrow),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.play,
));
case ButtonState.playing:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.pause),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.pause,
));
}
},
);
}
}
class NextSongButton extends StatelessWidget {
const NextSongButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isLastSongNotifier,
builder: (_, isLast, __) {
return IconButton(
icon: const Icon(Icons.skip_next),
iconSize: 32,
onPressed: (isLast) ? null : pageManager.next,
);
},
);
}
}
class ChangeSpeedButton extends StatefulWidget {
const ChangeSpeedButton({Key? key}) : super(key: key);
#override
State<ChangeSpeedButton> createState() => _ChangeSpeedButtonState();
}
class _ChangeSpeedButtonState extends State<ChangeSpeedButton> {
var selectedValue = 1.0;
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return DropdownButtonHideUnderline(
child: DropdownButton(
icon: const Visibility(
visible: false, child: Icon(Icons.arrow_downward)),
value: selectedValue
.toString(), //we set a value here depending on the button pressed, and call the pagemanager method assing it the value.
items: dropdownItems,
onChanged: (value) {
setState(() {
selectedValue = double.parse(value.toString());
});
pageManager.changeSpeed(selectedValue);
}),
);
}
List<DropdownMenuItem<String>> get dropdownItems {
List<DropdownMenuItem<String>> menuItems = [
const DropdownMenuItem(value: "0.75", child: Text("0.75x")),
const DropdownMenuItem(value: "1.0", child: Text("1.0x")),
const DropdownMenuItem(value: "1.25", child: Text("1.25x")),
const DropdownMenuItem(value: "1.5", child: Text("1.5x")),
const DropdownMenuItem(value: "1.75", child: Text("1.75x")),
const DropdownMenuItem(value: "2.0", child: Text("2.0x")),
];
return menuItems;
}
}
class CurrentMetadata extends StatelessWidget {
const CurrentMetadata({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<MediaItem?>(
valueListenable: pageManager.currentSongMetadataNotifier,
builder: (context, mediaItem, child) {
if (mediaItem == null) {
return SizedBox.shrink();
}
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network((mediaItem.artUri ?? '').toString()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(mediaItem.extras!['chapterName'],
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text(
mediaItem.title,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 6),
Text(
'By ${mediaItem.artist}',
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
IconButton(
icon: const Icon(Icons.favorite),
color: Colors.red,
onPressed: () {}),
],
),
)
],
);
// Text(mediaItem.album ?? ''),
// Text(mediaItem.displaySubtitle ?? ''),
},
);
}
}
PageManager.dart
import 'package:flutter/foundation.dart';
import 'package:podfic_app/models/getpodficlist_model.dart';
import 'package:podfic_app/models/series_model.dart';
import 'package:podfic_app/notifiers/chapterObject_notifier.dart';
import 'package:podfic_app/utils/api_service.dart';
import 'notifiers/play_button_notifier.dart';
import 'notifiers/progress_notifier.dart';
import 'notifiers/repeat_button_notifier.dart';
import 'services/playlist_repository.dart';
import 'package:audio_service/audio_service.dart';
import 'services/service_locator.dart';
class PageManager {
// Listeners: Updates going to the UI
final currentSongMetadataNotifier = ValueNotifier<MediaItem?>(null);
final playlistNotifier = ValueNotifier<List<String>>([]);
final progressNotifier = ProgressNotifier();
final repeatButtonNotifier = RepeatButtonNotifier();
final isFirstSongNotifier = ValueNotifier<bool>(true);
final playButtonNotifier = PlayButtonNotifier();
final isLastSongNotifier = ValueNotifier<bool>(true);
final isShuffleModeEnabledNotifier = ValueNotifier<bool>(false);
final _audioHandler = getIt<AudioHandler>();
List<Chapter>? chapterObject = getIt<ChapterObject>().chapterObject.value;
// Events: Calls coming from the UI
void init() async {
await loadPlaylist();
_listenToChangesInPlaylist();
_listenToPlaybackState();
_listenToCurrentPosition();
_listenToBufferedPosition();
_listenToTotalDuration();
_listenToChangesInSong();
// _startPlaying();
}
Future<void> loadPlaylist() async {
final mediaItems = seriesItems.chapterList?.map((chapter) {
final _podficInfo = chapter.podficInfo;
return MediaItem(
id: _podficInfo.id.toString(),
artUri: Uri.parse(_podficInfo.coverArt!),
artist: seriesItems.author,
title: seriesItems.title!,
extras: {
'url': _podficInfo.url,
'chapterName':
"Chapter ${_podficInfo.chapterNumber}: ${_podficInfo.title}"
},
);
}).toList() ??
[];
_audioHandler.addQueueItems(mediaItems);
}
// void _setInitialPlaylist() {
// final _seriesInfo = getIt<SeriesInfo>();
// final _playlist = SeriesInfo(chapterList: chapterItems);
// }
void play() => _audioHandler.play();
void pause() => _audioHandler.pause();
void seek(Duration position) => _audioHandler.seek(position);
void previous() => _audioHandler.skipToPrevious();
void next() => _audioHandler.skipToNext();
void repeat() {}
void shuffle() {}
void add() async {
//don't need this for now.
final songRepository = getIt<PlaylistRepository>();
final song = await songRepository.fetchAnotherSong();
final mediaItem = MediaItem(
id: song['id'] ?? '',
album: song['album'] ?? '',
title: song['title'] ?? '',
extras: {'url': song['url']},
);
_audioHandler.addQueueItem(mediaItem);
}
void remove() {
//don't need this for now.
final lastIndex = _audioHandler.queue.value.length - 1;
if (lastIndex < 0) return;
_audioHandler.removeQueueItemAt(lastIndex);
}
void dispose() {
_audioHandler.stop();
}
void _listenToChangesInPlaylist() {
_audioHandler.queue.listen((playlist) {
if (playlist.isEmpty) {
playlistNotifier.value = [];
currentSongMetadataNotifier.value = '' as MediaItem?;
} else {
final newList = playlist.map((item) => item.title).toList();
playlistNotifier.value = newList;
}
_updateSkipButtons();
});
}
void _listenToPlaybackState() {
_audioHandler.playbackState.listen((playbackState) {
final isPlaying = playbackState.playing;
final processingState = playbackState.processingState;
if (processingState == AudioProcessingState.loading ||
processingState == AudioProcessingState.buffering) {
playButtonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
playButtonNotifier.value = ButtonState.paused;
} else if (processingState != AudioProcessingState.completed) {
playButtonNotifier.value = ButtonState.playing;
logger.i('Reproduciendo');
} else {
_audioHandler.seek(Duration.zero);
_audioHandler.pause();
}
});
}
void _listenToCurrentPosition() {
AudioService.position.listen((position) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: position,
buffered: oldState.buffered,
total: oldState.total,
);
});
}
void _listenToBufferedPosition() {
_audioHandler.playbackState.listen((playbackState) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: playbackState.bufferedPosition,
total: oldState.total,
);
});
}
void _listenToTotalDuration() {
_audioHandler.mediaItem.listen((mediaItem) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: oldState.buffered,
total: mediaItem?.duration ?? Duration.zero,
);
});
}
void _listenToChangesInSong() {
_audioHandler.mediaItem.listen((mediaItem) {
currentSongMetadataNotifier.value = mediaItem;
_updateSkipButtons();
});
}
void _updateSkipButtons() {
final mediaItem = _audioHandler.mediaItem.value;
final playlist = _audioHandler.queue.value;
if (playlist.length < 2 || mediaItem == null) {
isFirstSongNotifier.value = true;
isLastSongNotifier.value = true;
} else {
isFirstSongNotifier.value = playlist.first == mediaItem;
isLastSongNotifier.value = playlist.last == mediaItem;
}
}
void changeSpeed(value) {
_audioHandler.setSpeed(value);
}
// void _startPlaying(){
// _audioHandler.
// }
}
// class PlaySpeedNotifier extends ValueNotifier {
// PlaySpeedNotifier() : super(initialSpeed);
// static const initialSpeed = 1.0;
// var speed = initialSpeed;
// void changeSpeed(value) {
// speed = value;
// _audio
// }
// }
Audiohandler.dart
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:podfic_app/utils/api_service.dart';
Future<AudioHandler> initAudioService() async {
return await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.mycompany.myapp.audio',
androidNotificationChannelName: 'FanPods',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
);
}
class MyAudioHandler extends BaseAudioHandler {
final _player = AudioPlayer();
final _playlist = ConcatenatingAudioSource(children: []);
MyAudioHandler() {
loadEmptyPlaylist();
_notifyAudioHandlerAboutPlaybackEvents();
_listenForDurationChanges();
_listenForCurrentSongIndexChanges();
}
Future<void> loadEmptyPlaylist() async {
try {
await _player.setAudioSource(_playlist);
} catch (e) {
logger.i('algo paso con loadEemptyplaylist');
}
}
// Future<void> startPlaying() => _player.setAudioSource(_playlist);
#override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
// manage Just Audio
final audioSource = mediaItems.map(_createAudioSource);
_playlist.addAll(audioSource.toList());
// notify system
final newQueue = queue.value..addAll(mediaItems);
queue.add(newQueue);
logger.i(newQueue);
}
UriAudioSource _createAudioSource(MediaItem mediaItem) {
return AudioSource.uri(
Uri.parse(mediaItem.extras!['url']),
tag: mediaItem,
);
}
#override
Future<void> play() => _player.play();
#override
Future<void> pause() => _player.pause();
void _notifyAudioHandlerAboutPlaybackEvents() {
_player.playbackEventStream.listen((PlaybackEvent event) {
final playing = _player.playing;
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
systemActions: const {
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
));
});
}
#override
Future<void> seek(Duration position) => _player.seek(position);
void _listenForDurationChanges() {
_player.durationStream.listen((duration) {
final index = _player.currentIndex;
final newQueue = queue.value;
if (index == null || newQueue.isEmpty) return;
final oldMediaItem = newQueue[index];
final newMediaItem = oldMediaItem.copyWith(duration: duration);
newQueue[index] = newMediaItem;
queue.add(newQueue);
mediaItem.add(newMediaItem);
});
}
#override
Future<void> skipToNext() => _player.seekToNext();
#override
Future<void> skipToPrevious() => _player.seekToPrevious();
void _listenForCurrentSongIndexChanges() {
_player.currentIndexStream.listen((index) {
final playlist = queue.value;
if (index == null || playlist.isEmpty) return;
mediaItem.add(playlist[index]);
logger.i(playlist);
});
}
#override
Future<void> addQueueItem(MediaItem mediaItem) async {
// manage Just Audio
final audioSource = _createAudioSource(mediaItem);
_playlist.add(audioSource);
// notify system
final newQueue = queue.value..add(mediaItem);
queue.add(newQueue);
}
#override
Future<void> removeQueueItemAt(int index) async {
// manage Just Audio
_playlist.removeAt(index);
// notify system
final newQueue = queue.value..removeAt(index);
queue.add(newQueue);
}
#override
Future<void> skipToQueueItem(int index) async {
if (index < 0 || index >= queue.value.length) return;
if (_player.shuffleModeEnabled) {
index = _player.shuffleIndices![index];
}
_player.seek(Duration.zero, index: index);
}
#override
Future<void> stop() async {
await _player.dispose();
return super.stop();
}
}
I tried including pageManager.play in the init section of Player and PageManager, but nothing happens. I think I need to set the audiosource as a concatenatingAudioSource, but I created a method to execute it and passed it _playlist, running it under _listenForCurrentSongIndexChanges(), but nothing happened.
Realized the play method was executing before the init method finished, so simple delay worked.
I am working on a web app where users can post stuffs and make them more accessible by associating the posts with tags. so my idea is similar to stackoverflow's way of giving tags to posts, I am creating a Textfield with which will accept only few tags(string values) which I will create from a list and users can put them in their post. But I aint getting how to implement this as textfield has only few keyboardtypes... and I what I want to achieve is if I entered a value from a that list then it should act like a chip text(tag).
or Is there any other way to do this,
your help is appreciated,
thank you
Yes, there is. you can use the flutter_tagging package on the PUB
It has supports for Web
The gif below explains what you want to achieve
You can find an implementation of a Chip Input Field type widget here:
Latest: https://gist.github.com/slightfoot/c6c0f1f1baca326a389a9aec47886ad6
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager
void main() => runApp(ChipsDemoApp());
class ChipsDemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pink,
),
home: DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Material Chips Input'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(hintText: 'normal'),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChipsInput<AppProfile>(
decoration: InputDecoration(prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
findSuggestions: _findSuggestions,
onChanged: _onChanged,
chipBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
onSelected: (_) => _onChipTapped(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
),
),
],
),
);
}
void _onChipTapped(AppProfile profile) {
print('$profile');
}
void _onChanged(List<AppProfile> data) {
print('onChanged $data');
}
Future<List<AppProfile>> _findSuggestions(String query) async {
if (query.length != 0) {
return mockResults.where((profile) {
return profile.name.contains(query) || profile.email.contains(query);
}).toList(growable: false);
} else {
return const <AppProfile>[];
}
}
}
// -------------------------------------------------
const mockResults = <AppProfile>[
AppProfile('Stock Man', 'stock#man.com', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
AppProfile('Paul', 'paul#google.com', 'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
AppProfile('Fred', 'fred#google.com',
'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
AppProfile('Bera', 'bera#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('John', 'john#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Thomas', 'thomas#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Norbert', 'norbert#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marina', 'marina#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
class AppProfile {
final String name;
final String email;
final String imageUrl;
const AppProfile(this.name, this.email, this.imageUrl);
#override
bool operator ==(Object other) =>
identical(this, other) || other is AppProfile && runtimeType == other.runtimeType && name == other.name;
#override
int get hashCode => name.hashCode;
#override
String toString() {
return 'Profile{$name}';
}
}
// -------------------------------------------------
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
Key key,
this.decoration = const InputDecoration(),
#required this.chipBuilder,
#required this.suggestionBuilder,
#required this.findSuggestions,
#required this.onChanged,
this.onChipTapped,
}) : super(key: key);
final InputDecoration decoration;
final ChipsInputSuggestions findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T> onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
#override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
int _searchId = 0;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = null;
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
#override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, TextInputConfiguration());
_connection.setEditingState(_value);
}
_connection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
}
#override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
Container(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.subhead.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(context, this, _suggestions[index]);
},
),
),
],
);
}
#override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
}
#override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
void _updateTextInputState() {
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results.where((profile) => !_chips.contains(profile)).toList(growable: false));
}
}
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
Key key,
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
}) : super(key: key);
final Duration duration;
final bool resumed;
#override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
bool _displayed = false;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}
the app is simple categories/products display , everything works fine except select a product from a category products and swip back to the products widget , the state changes and it's neither one of the states i created and just shows a loading indicator ( ProductsWrapper default return from state).
so here is the code :
ProductBloc :
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository productRepository;
ProductBloc({required this.productRepository}) : super(ProductsEmpty());
#override
Stream<Transition<ProductEvent, ProductState>> transformEvents(
Stream<ProductEvent> events,
TransitionFunction<ProductEvent, ProductState> transitionFn) {
return super.transformEvents(
events.debounceTime(const Duration(microseconds: 500)), transitionFn);
}
#override
Stream<ProductState> mapEventToState(ProductEvent event) async* {
if (event is FetchProducts) {
yield* _mapFetchProductsToState(event);
} else if (event is RefreshProducts) {
yield* _mapRefreshProductsToState(event);
} else if (event is FetchProduct) {
yield* _mapFetchProductToState(event);
} else if (event is RefreshProduct) {
yield* _mapRefreshProductToState(event);
}
}
Stream<ProductState> _mapFetchProductsToState(FetchProducts event) async* {
try {
final products =
(await productRepository.getCategoryProducts(event.categoryId));
yield ProductsLoaded(products: products.products!);
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductsToState(
RefreshProducts event) async* {
try {
final products =
await productRepository.getCategoryProducts(event.categoryId);
yield ProductsLoaded(products: products.products!);
return;
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapFetchProductToState(FetchProduct event) async* {
try {
final product =
(await productRepository.getProductDetails(event.productId));
yield ProductLoaded(product: product);
} catch (e) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductToState(RefreshProduct event) async* {
try {
final product =
await productRepository.getProductDetails(event.productId);
yield ProductLoaded(product: product);
return;
} catch (_) {
yield state;
}
}
}
states :
abstract class ProductState extends Equatable {
const ProductState();
#override
List<Object?> get props => [];
}
class ProductsEmpty extends ProductState {}
class ProductEmpty extends ProductState {}
class ProductLoading extends ProductState {}
class ProductsLoading extends ProductState {}
class ProductLoaded extends ProductState {
final Product product;
const ProductLoaded({required this.product});
ProductLoaded copyWith({required Product product}) {
return ProductLoaded(product: product);
}
#override
List<Object?> get props => [product];
#override
String toString() => 'ProductLoaded { product: ${product.name}}';
}
class ProductsLoaded extends ProductState {
final List<Product> products;
const ProductsLoaded({required this.products});
ProductsLoaded copyWith({required List<Product> products}) {
return ProductsLoaded(products: products);
}
#override
List<Object?> get props => [products];
#override
String toString() => 'ProductLoaded { products: ${products.length}}';
}
class ProductError extends ProductState {}
ProductRepository ( ProductApiService is just the api and it's working fine ) :
class ProductRepository {
final ProductApiService productApiService;
ProductRepository({ProductApiService? productApiService})
: productApiService = productApiService ?? ProductApiService();
Future<Products> getCategoryProducts(int? categoryId) async {
return productApiService.fetchCategoryProducts(categoryId);
}
Future<Product> getProductDetails(int? productId) async {
return productApiService.fetchProductDetails(productId);
}
}
ProductsWrapper :
final int? categoryId;
const ProductsWrapper({Key? key, required this.categoryId}) : super(key: key);
#override
_ProductsWrapperState createState() => _ProductsWrapperState();
}
class _ProductsWrapperState extends State<ProductsWrapper> {
final _scrollController = ScrollController();
final _scrollThreshold = 200;
Completer _productsRefreshCompleter = new Completer();
List<Product> products = [];
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
}
}
#override
void initState() {
super.initState();
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
_scrollController.addListener(_onScroll);
_productsRefreshCompleter = Completer();
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = 260;
final double itemWidth = size.width / 2;
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductsLoaded) {
products = state.products;
_productsRefreshCompleter.complete();
}
},
child: Container(
margin: EdgeInsets.all(8.0),
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductsLoading) {
print('a7a');
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductsLoaded) {
products = state.products;
if (state.products.isEmpty) {
return Center(
child: Text("No Products Found in this category"),
);
}
return Scaffold(
body: SafeArea(
child: Container(
child: GridView.builder(
itemCount: products.length,
scrollDirection: Axis.vertical,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio:
(itemWidth / itemHeight)),
itemBuilder: (context, index) => Card(
elevation: 0,
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ProductDetailScreen(
productId:
products[index]
.id)));
},
child: Container(
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
ClipRRect(
child: Image.network(
products[index]
.image!
.image
.toString(),
height: 150,
fit: BoxFit.fitWidth,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
products[index].name.toString(),
style: TextStyle(
color: Colors.black,
fontWeight:
FontWeight.bold),
),
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'\$${products[index].price.toString()}'),
),
Padding(
padding: EdgeInsets.only(
right: 8.0),
child: CircleAvatar(
backgroundColor:
Theme.of(context)
.primaryColor,
radius: 10,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
size: 20,
),
color: Colors.white,
onPressed: () {},
),
),
)
],
)
],
),
),
),
)),
),
),
);
}
return Center(
child: LoadingIndicator(strokeWidth: 5.0,),
);
}))));
}
}
ProductDetailScreen :
class ProductDetailScreen extends StatefulWidget {
final int? productId;
const ProductDetailScreen({Key? key, required this.productId})
: super(key: key);
#override
_ProductDetailScreenState createState() => _ProductDetailScreenState();
}
class _ProductDetailScreenState extends State<ProductDetailScreen> {
Completer _productRefreshCompleter = new Completer();
Product product = new Product();
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
context.read<ProductBloc>().add(FetchProduct(productId: widget.productId));
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductLoaded) {
product = state.product;
_productRefreshCompleter.complete();
_productRefreshCompleter = Completer();
}
},
child: Container(
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductLoaded) {
return Scaffold(
body: SafeArea(
child: Container(
child: Text(product.name.toString()),
),
),
);
}
return Center(
child: LoadingIndicator(
strokeWidth: 5.0,
),
);
},
),
),
),
);
}
}
any help is appreciated .
thanks for taking time reading this , have a nice day and stay safe.
The problem is that you are using one bloc to do 2 things. The products list is an entity, the single detail is another entity. And you need to use the properties of the states as a result inside blocBuilders.
Plus, you don't need any listener and completer. The bloc pattern refreshes all when state changes.
I have created a repo with a working solution.
https://github.com/eugenioamato/categoryproducts
I understand that the problem is in a lifecircle that I'm trying to set a state in Provider before the Widget is rendered but where can I do that. Only in a Container Widget? But I cannot do that unless I've a button or something.
I hope you got the issue of the problem here.
I would appreciate any hints!
my Error:
setState() or markNeedsBuild() called during build.
or
The setter 'lastPage=' was called on null.
Receiver: null
Tried calling: lastPage=true
if I set the state in here
_detectLastPage() {
int currentPage = this.currentStep == null ? 1 : this.currentStep + 1;
if (currentPage == 1 && this.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.totalSteps) {
this.lastPage = true;
_welcomeBloc.lastPage = true;
this._onLastPage();
} else {
this.lastPage = false;
_welcomeBloc.lastPage = true;
}
}
My Widget:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
class Footer extends StatelessWidget {
final int currentStep;
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
final double radius = 10.0;
final double distance = 4.0;
Container stepper;
Container nextArrow;
bool lastPage;
WelcomeBloc _welcomeBloc;
Footer({
this.activeColor,
this.inactiveColor,
this.currentStep,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
}) {
this._detectLastPage();
this._makeStepper();
this._makeNextArrow();
}
#override
Widget build(BuildContext context) {
print('footer is launching');
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
// this._welcomeBloc.lastPage = true; // I'd like to set the state here
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
RaisedButton(
child: Text('kdfljds'),
onPressed: () {
print(_welcomeBloc.lastPage);
_welcomeBloc.lastPage = true; // I can access from here BUT CANNOT access outside this container
},
)
],
),
);
}
_makeCirle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep ?? 0;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: this.radius,
width: this.radius,
margin: EdgeInsets.only(left: this.distance, right: this.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeStepper() {
List<Container> circles = List();
for (var i = 0; i < totalSteps; i++) {
circles.add(
_makeCirle(this.activeColor, this.inactiveColor, i, this.currentStep),
);
}
this.stepper = Container(
child: Row(
children: circles,
),
);
}
_makeNextArrow() {
this.nextArrow = Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () {
_welcomeBloc.controller.nextPage(
duration: this.duration ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_onLastPage() {
if (this.onFinal != null) {
this.onFinal();
}
}
_onFirstPage() {
if (this.onStart != null) {
this.onStart();
}
}
_detectLastPage() {
int currentPage = this.currentStep == null ? 1 : this.currentStep + 1;
if (currentPage == 1 && this.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.totalSteps) {
this.lastPage = true;
this._onLastPage();
} else {
this.lastPage = false;
}
}
}
BlocFile
import 'package:flutter/material.dart';
class WelcomeBloc extends ChangeNotifier {
PageController _controller = PageController();
int _currentPage;
bool _lastPage = false;
bool get lastPage => _lastPage;
set lastPage(bool value){
print(value);
_lastPage = value;
notifyListeners();
}
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value;
notifyListeners();
}
get controller => _controller;
nextPage(Duration duration, Curves curve){
controller.nextPage(duration: duration, curve: curve);
}
}
[![error screen with StateLess, since I use Provider][1]][1]
There I call like this:
_detectLastPage() {
int currentPage =
this.widget.currentStep == null ? 1 : this.widget.currentStep + 1;
if (currentPage == 1 && this.widget.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.widget.totalSteps) {
this.lastPage = true;
setState(() {
_welcomeBloc.lastPage = true;
});
this._onLastPage();
} else {
this.lastPage = false;
setState(() {
_welcomeBloc.lastPage = false;
});
}
}
And without SetState seem to be the same error...
this error if I call from inside initState from your example. Just forgot you attach it
You cannot use the setState method in a StatelessWidget. Convert it to a StatefulWidget and call the setState in the initState method.
Like this
class Footer extends StatefulWidget {
final int currentStep;
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
Footer({
this.activeColor,
this.inactiveColor,
this.currentStep,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
});
#override
_FooterState createState() => _FooterState();
}
class _FooterState extends State<Footer> {
final double radius = 10.0;
final double distance = 4.0;
Container stepper;
Container nextArrow;
bool lastPage;
WelcomeBloc _welcomeBloc;
#override
void initState(){
this._detectLastPage();
this._makeStepper();
this._makeNextArrow();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
setState((){
this._welcomeBloc.lastPage = true; // Where to use setState
});
}
#override
Widget build(BuildContext context) {
print('footer is launching');
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
RaisedButton(
child: Text('kdfljds'),
onPressed: () {
print(_welcomeBloc.lastPage);
_welcomeBloc.lastPage = true; // I can access from here BUT CANNOT access outside this container
},
)
],
),
);
}
_makeCirle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep ?? 0;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: this.radius,
width: this.radius,
margin: EdgeInsets.only(left: this.distance, right: this.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeStepper() {
List<Container> circles = List();
for (var i = 0; i < totalSteps; i++) {
circles.add(
_makeCirle(this.activeColor, this.inactiveColor, i, this.currentStep),
);
}
this.stepper = Container(
child: Row(
children: circles,
),
);
}
_makeNextArrow() {
this.nextArrow = Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () {
_welcomeBloc.controller.nextPage(
duration: this.duration ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_onLastPage() {
if (this.onFinal != null) {
this.onFinal();
}
}
_onFirstPage() {
if (this.onStart != null) {
this.onStart();
}
}
_detectLastPage() {
int currentPage = this.currentStep == null ? 1 : this.currentStep + 1;
if (currentPage == 1 && this.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.totalSteps) {
this.lastPage = true;
this._onLastPage();
} else {
this.lastPage = false;
}
}
}
If I got it right you are trying to simulate PageView navigation by some circle bellow it(Indicators).
To do so there are lots of good resources and also packages like:
This example or this package
But for your code I wrote it in 2 approaches:
First Approach
This one is your code and use provider.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(Home());
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => WelcomeBloc(),
child: Consumer<WelcomeBloc>(
builder: (BuildContext context, value, Widget child) {
PageController controller = value.controller;
print('object');
return MaterialApp(
home: Scaffold(
body: Stack(
children: <Widget>[
PageView(
controller: controller,
children: List.generate(
10, (i) => Center(child: Text('Page $i'))),
onPageChanged: (i) {
value.currentPage = i;
},
),
Footer(
activeColor: Colors.red,
duration: Duration(seconds: 1),
inactiveColor: Colors.yellow,
onFinal: () {},
onStart: () {},
totalSteps: 10,
)
],
),
),
);
},
),
);
}
}
class Footer extends StatefulWidget {
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
final double radius;
final double distance;
Footer({
this.activeColor,
this.inactiveColor,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
this.radius = 10.0,
this.distance = 4.0,
});
#override
_FooterState createState() => _FooterState();
}
class _FooterState extends State<Footer> {
bool lastPage;
WelcomeBloc _welcomeBloc;
#override
Widget build(BuildContext context) {
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
// this._welcomeBloc.lastPage = true; // I'd like to set the state here
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_makeStepper(),
_makeNextArrow(),
],
),
);
}
_makeCircle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep ?? 0;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: widget.radius,
width: widget.radius,
margin: EdgeInsets.only(left: widget.distance, right: widget.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeNextArrow() {
return Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () async {
await _welcomeBloc.nextPage(widget.duration, Curves.easeInOut);
setState(() {});
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_makeStepper() {
return Container(
child: Row(
children: List.generate(
widget.totalSteps,
(i) => _makeCircle(
this.widget.activeColor,
this.widget.inactiveColor,
i,
_welcomeBloc.currentPage,
),
),
),
);
}
_onLastPage() {
if (this.widget.onFinal != null) {
this.widget.onFinal();
}
}
_onFirstPage() {
if (this.widget.onStart != null) {
this.widget.onStart();
}
}
_detectLastPage() {
int currentPage =
_welcomeBloc.currentPage == null ? 1 : _welcomeBloc.currentPage + 1;
if (currentPage == 1 && _welcomeBloc.currentPage == null) {
this._onFirstPage();
} else if (currentPage == this.widget.totalSteps) {
this.lastPage = true;
this._onLastPage();
} else {
this.lastPage = false;
}
}
}
class WelcomeBloc extends ChangeNotifier {
final PageController _controller = PageController();
int _currentPage = 0;
bool _lastPage = false;
bool get lastPage => _lastPage;
set lastPage(bool value) {
_lastPage = value;
notifyListeners();
}
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value;
notifyListeners();
}
PageController get controller => _controller;
Future<void> nextPage(Duration duration, Curve curve) {
currentPage = controller.page.floor() + 1;
return controller.nextPage(duration: duration, curve: curve);
}
}
Second Approach
In the second one I removed provider stuff because it can be done without it by using PageController features.
import 'package:flutter/material.dart';
void main() {
runApp(Home());
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
PageController controller = PageController(initialPage: 0);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Stack(
children: <Widget>[
PageView(
controller: controller,
children: List.generate(
10,
(i) => Center(child: Text('Page $i')),
),
onPageChanged: (page) {
setState(() {});
},
),
Footer(
currentPage: controller.hasClients ? controller.page.floor() : 0,
activeColor: Colors.red,
inactiveColor: Colors.yellow,
totalSteps: 10,
onTap: () async {
await controller.nextPage(
duration: Duration(seconds: 1) ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
setState(() {});
},
)
],
),
),
);
}
}
class Footer extends StatelessWidget {
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final double radius;
final double distance;
final int currentPage;
final GestureTapCallback onTap;
Footer({
this.activeColor,
this.inactiveColor,
this.totalSteps,
this.radius = 10.0,
this.distance = 4.0,
this.currentPage,
this.onTap,
});
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_makeStepper(),
_makeNextArrow(),
],
),
);
}
_makeCircle(activeColor, inactiveColor, position) {
Color color = (position == currentPage) ? activeColor : inactiveColor;
return Container(
height: radius,
width: radius,
margin: EdgeInsets.only(left: distance, right: distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeNextArrow() {
return Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: onTap,
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_makeStepper() {
return Container(
child: Row(
children: List.generate(
totalSteps,
(i) => _makeCircle(
this.activeColor,
this.inactiveColor,
i,
),
),
),
);
}
}
So, the solution of my error is in didChangeDependencies hook.
I tried to change the state above at the very moment when the Widget was being built (that's how I got it).
So, I just needed to run it either before or after widget is built.
That's how it looks like in the code:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import 'package:flutter/scheduler.dart';
class Footer extends StatefulWidget {
final int currentStep;
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
Footer({
this.activeColor,
this.inactiveColor,
this.currentStep,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
}) {}
#override
_FooterState createState() => _FooterState();
}
class _FooterState extends State<Footer> {
final double radius = 10.0;
final double distance = 4.0;
Container stepper;
Container nextArrow;
bool lastPage;
WelcomeBloc _welcomeBloc;
#override
void didChangeDependencies() {
super.didChangeDependencies();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
this._detectLastPage();
}
#override
Widget build(BuildContext context) {
this._makeStepper();
this._makeNextArrow();
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
],
),
);
}
_makeCirle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep == null ? 0 : currentStep - 1;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: this.radius,
width: this.radius,
margin: EdgeInsets.only(left: this.distance, right: this.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeStepper() {
List<Container> circles = List();
for (var i = 0; i < widget.totalSteps; i++) {
circles.add(
_makeCirle(this.widget.activeColor, this.widget.inactiveColor, i,
this.widget.currentStep),
);
}
this.stepper = Container(
child: Row(
children: circles,
),
);
}
_makeNextArrow() {
this.nextArrow = Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () {
_welcomeBloc.controller.nextPage(
duration: this.widget.duration ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_onLastPage() {
if (this.widget.onFinal != null) {
this.widget.onFinal();
}
}
_onFirstPage() {
if (this.widget.onStart != null) {
this.widget.onStart();
}
}
_detectLastPage() {
// Here I've got inaccurate data
int currentPage =
this.widget.currentStep == null ? 1 : this.widget.currentStep;
if (currentPage == 1 && this.widget.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.widget.totalSteps) {
print('lastPage detected');
setState(() {
this.lastPage = true;
});
_welcomeBloc.lastPage = true;
this._onLastPage();
} else {
setState(() {
this.lastPage = false;
});
_welcomeBloc.lastPage = false;
}
}
}
P.S.: but I face another problem there with the data accuracy. Inside that hook I could get the class property only one step behind accurate one.
details here: didChangeDependencies hook in Flutter Widget includes not accurate data of the class
I am new to Flutter and I am working on a chat app, and whenever i choose an user i should be able to talk to him in a private way, that's what I am doing, whenever i click someone I try to move to this Chat Screen, and then I am getting this error (see title).
But when I'm pressing the back button and try again it works and shows the chat like it should, this is really confusing me, and if someone have an idea where it comes from, it would be awesome.
Here's my chat.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:social/responsive/size_config.dart';
var _firestore = Firestore.instance;
FirebaseUser loggedInUser;
String groupChatId;
class Chat extends StatelessWidget {
static const String id = 'chat_screen';
final String peerEmail;
Chat({Key key, #required this.peerEmail}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'CHAT',
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: ChatScreen(peerEmail: peerEmail));
}
}
class ChatScreen extends StatefulWidget {
final String peerEmail;
ChatScreen({this.peerEmail});
#override
_ChatScreenState createState() => _ChatScreenState(peerEmail: peerEmail);
}
class _ChatScreenState extends State<ChatScreen> {
final _auth = FirebaseAuth.instance;
final messageTextController = TextEditingController();
String peerEmail;
String messageText;
_ChatScreenState({this.peerEmail});
#override
void initState() {
super.initState();
getCurrentUser();
String email = loggedInUser.email;
getGroupId(email);
}
void getGroupId(String email) {
if (peerEmail.hashCode <= email.hashCode) {
setState(() {
groupChatId = '$peerEmail-$email';
});
} else {
setState(() {
groupChatId = '$email-$peerEmail';
});
}
}
void getCurrentUser() async {
try {
final user = await _auth.currentUser();
if (user != null) {
loggedInUser = user;
setState(() {});
}
} catch (e) {
print(e);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
MessageStream(),
Container(
decoration: BoxDecoration(color: Colors.red,borderRadius: BorderRadius.circular(10)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
controller: messageTextController,
onChanged: (value) {
//Do something with the user input.
messageText = value;
},
),
),
FlatButton(
onPressed: () {
//Implement send functionality.
messageTextController.clear();
print(messageText);
print(loggedInUser.email);
_firestore.collection('messages')
.document(groupChatId)
.collection(groupChatId).add({
'content': messageText,
'emailFrom': loggedInUser.email,
'emailTo': peerEmail,
});
},
child: Text(
'Send',
),
),
],
),
),
],
)));
}
}
class MessageStream extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _firestore
.collection('messages')
.document(groupChatId)
.collection(groupChatId)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.black),
),
);
} else {
final messages = snapshot.data.documents;
List<MessageDisplay> messageList = [];
for (var msg in messages) {
final message = msg.data['content'];
final emailTo = msg.data['emailTo'];
final emailFrom = msg.data['emailFrom'];
final messageDisplay = MessageDisplay(
message: message,
emailFrom: emailFrom,
emailTo: emailTo,
);
messageList.add(messageDisplay);
}
return Expanded(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
children: messageList != null ? messageList:CircularProgressIndicator(),
),
);
} //
},
);
}
}
class MessageDisplay extends StatelessWidget {
MessageDisplay({this.message, this.emailFrom, this.emailTo});
final String message;
final String emailFrom;
final String emailTo;
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(SizeConfig.safeBlockVertical * 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
message != null
? Text(
message,
style: TextStyle(
fontSize: SizeConfig.safeBlockVertical * 15,
color: Colors.black54,
),
)
: CircularProgressIndicator(),
emailFrom != null
? Text(
emailFrom,
)
: CircularProgressIndicator(),
],
),
);
}
}
Thanks for reading.
The most likely cause for this type of error is the new screen you are navigating to is trying to access information from the previous screen which it has no access to or has not inherited and therefore doesn't have the correct build context when trying to build causing this error.