I am trying to implement streambuilder without Firebase, using a MongoDB database. The aim is to build a simple chat app, live streaming the messages. So far, the live streaming when I click on the send button works since I see the message displayed in the UI. I also push that message to my DB successfully.
The problem strives when I try to display the messages fetched from my datbase. They are fetched correctly, but not displayed.
final StreamController<ChatMessageModel> _chatMessagesStreamController =
StreamController<ChatMessageModel>.broadcast();
final Stream<ChatMessageModel> _chatMessagesStream =
_chatMessagesStreamController.stream;
class MessagesStream extends StatefulWidget {
var usermail;
var usermailTo;
var title;
MessagesStream(this.usermail, this.usermailTo, this.title);
#override
_MessagesStreamState createState() => _MessagesStreamState();
}
class _MessagesStreamState extends State<MessagesStream> {
final List<ChatMessageModel> _allMessagesContainedInTheStream = [];
Future<List<dynamic>>? futureMessages;
Future fetchMessagesFromBack4App(
String usermail, String usermailTo, String dogName) async {
final queryBuilder = QueryBuilder(ParseObject('Messages'))
..whereEqualTo('sender', usermail)
..whereEqualTo('receiver', usermailTo)
..whereEqualTo('dogname', dogName)
..orderByAscending('date');
final response = await queryBuilder.query();
if (response.success && response.results != null) {
for (var message in response.results!) {
//check if message was already put into stream
bool messageFoundInAllMessageLogged = false;
for (int i = 0; i < _allMessagesContainedInTheStream.length; i++) {
if (message["sender"] == _allMessagesContainedInTheStream[i].sender &&
message["receiver"] ==
_allMessagesContainedInTheStream[i].receiver &&
message["date"] == _allMessagesContainedInTheStream[i].date &&
message["dogname"] ==
_allMessagesContainedInTheStream[i].dogname &&
message["message"] ==
_allMessagesContainedInTheStream[i].message) {
messageFoundInAllMessageLogged = true;
break;
}
}
// Add message to stream if it was not logged yet
if (!messageFoundInAllMessageLogged) {
ChatMessageModel chatMessageModelRecord = ChatMessageModel(
receiver: message["receiver"],
message: message["message"],
sender: message["sender"],
dogname: message["dogname"],
date: DateTime.parse(message["date"]));
_allMessagesContainedInTheStream.add(chatMessageModelRecord);
debugPrint("putting message to stream: " + message['message']);
}
}
} else {
return [];
}
}
#override
void initState() {
fetchMessagesFromBack4App(widget.usermail, widget.usermailTo, widget.title);
_chatMessagesStream.listen((streamedMessages) {
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
});
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot) {
return Expanded(
child: ListView.builder(
// reverse: true,
padding:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index) {
if (snapshot.hasData) {
return UserChatBubble(
chatMessageModelRecord:
_allMessagesContainedInTheStream[index],
);
} else {
print(snapshot.connectionState);
return Container();
}
},
),
);
},
);
}
}
class UserChatBubble extends StatelessWidget {
final ChatMessageModel chatMessageModelRecord;
const UserChatBubble({
Key? key,
required this.chatMessageModelRecord,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 5,
),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 7 / 10,
),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
topLeft: Radius.circular(15.0),
),
color: primaryColor,
),
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 20,
),
child: ListTile(
title: Text("${chatMessageModelRecord.message}"),
subtitle: Text(chatMessageModelRecord.date.toString()),
),
),
),
],
);
}
}
The method fetchMessagesFromBack4App fetches correctly the data and add records to _allMessagesContainedInTheStream. However, when the method ends this _allMessagesContainedInTheStream list is empty (despite of inside the method is adding records). Therefore, snapshot is empty too.
Only when I press the send button then I am able to see all the messages: the fetched ones and the sent ones.
Summarizing: snapshot has no data when I navigate to my chat screen. It receives the data only when I press the send button to send a message.
_chatMessagesStream.listen() will listen to the _chatMessagesStream stream and if any event occur, everything in the block will execute.
your fetchMessagesFromBack4App() does not emit any new event to the above stream, but add value to _allMessagesContainedInTheStream which is a List
to sum up you need to change _allMessagesContainedInTheStream.add(chatMessageModelRecord); to _chatMessagesStreamController.add(chatMessageModelRecord) add new event to the stream in other for your StreamBuilder to rebuild
Related
I'm using Firebase Realtime database to add chat features to my app. In the database I have the following data:
{
events: {
"event-uuid1": {
"chat-uuid1": {
"message": "hey"
},
"chat-uuid2": {
"message": "hey again"
}
}
}
}
In my Flutter app, I have this StreamBuilder (I know this is lengthy, I'm not sure where the problem is so providing more rather than less):
class _EventChatScreenState extends ConsumerState<EventChatScreen> {
FirebaseDatabase dbInstance = FirebaseDatabase.instance;
late TextEditingController _messageFieldController;
late DatabaseReference eventDbRef;
#override
void initState() {
super.initState();
_messageFieldController = TextEditingController();
eventDbRef = dbInstance.ref("none");
}
#override
void dispose() {
_messageFieldController.dispose();
super.dispose();
}
Map<String, ChatMessage> chatMessages = {};
#override
Widget build(BuildContext context) {
final user = ref.watch(userProvider);
final event = ModalRoute.of(context)!.settings.arguments as EventRepository;
if (eventDbRef.path == "none") {
print("IT IS NONE");
eventDbRef = dbInstance.ref("/events/${event.event.eventId}/");
print(eventDbRef.path); // Print's correct value
}
return StreamBuilder(
stream: eventDbRef.onChildAdded,
builder: (context, snapshot) {
if (chatMessages == {}) {
return const Text("Loading...");
}
DatabaseEvent data;
if (snapshot.hasData) {
data = snapshot.data as DatabaseEvent;
ChatMessage newChatMessage = ChatMessage(
chatMessageId: "",
userId: "",
displayname: "",
message: "",
datetime: "",
);
for (var child in data.snapshot.children) {
switch (child.key) {
case "chatMessageId":
newChatMessage.chatMessageId = child.value.toString();
break;
case "userId":
newChatMessage.userId = child.value.toString();
break;
case "displayName":
newChatMessage.displayname = child.value.toString();
break;
case "message":
newChatMessage.message = child.value.toString();
break;
case "datetime":
final datetime = DateTime.parse(child.value.toString());
final DateFormat formatter = DateFormat('h:mm aa');
final String formatted = formatter.format(datetime);
newChatMessage.datetime = formatted;
break;
default:
}
}
if (chatMessages[data.snapshot.key] == null) {
chatMessages[data.snapshot.key!] = newChatMessage;
}
}
return ListView.builder(
itemCount: chatMessages.length,
itemBuilder: (context, index) {
String key = chatMessages.keys.elementAt(index);
if (chatMessages[key]!.userId == user.user.userId) {
return UnconstrainedBox(
alignment: Alignment.centerRight,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(left: 10),
child: Text(chatMessages[key]!.displayname),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
color: Theme.of(context).colorScheme.primary,
),
child: Text(chatMessages[key]!.message,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
Container(
padding: const EdgeInsets.only(left: 10),
child: Text(chatMessages[key]!.datetime),
),
],
),
),
);
}
},
);
},
),
The problem is that when the user goes to the chat screen one of the messages will already be present in the chat. I would expect there to be nothing since I am not setting any initial data anywhere, not using Realtime Database's persistence, and not using my own local database yet.
My understanding of StreamBuilders is that they only get new data as it comes in, not data that may already exist and is thus not sent through it (Ie. when a new chat message is sent the stream should receive it, which works, but it should not receive chat message messages already in the database). If that understanding is wrong then why am I only getting one message despite there being 2, 3, 4, etc. in the database?
Perhaps I'm understanding/using StreamBuilders, Firebase Realtime Database, or both incorrectly?
Maybe your understanding about streambuilder is wrong.
Lets say you use FutureBuilder, It'll wait till the future is over and then builds the widget accordingly but It'll not build again if something changes in your database, but for StreamBuilder, It'll basically listen (and get initial data from stream, here your db) to the stream and build whenever it changes or a new data is added to stream (here database) it will get the updated data and build the widget again.
Read here:
https://firebase.flutter.dev/docs/firestore/usage/#realtime-changes
Assign this eventDbRef.onChildAdded to a variable in initState and then use the variable as your stream parameter in streambuilder. Having the db call in Streambuilder causes it to be rerun everytime the widget tree builds.
I created a model class for Provider. Which will fit the function of getting data from SharedPreferences
Future getDataPerson() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
id = prefs.getInt('id') ?? 000;
name = prefs.getString('name') ?? "Фамилия Имя Отчество";
phone = prefs.getString('phone') ?? "3231313";
email = prefs.getString('email') ?? "";
accessToken = prefs.getString('accessToken') ?? "нет токенааа";
_login = prefs.getString('login') ?? "нет логинааа";
_password = prefs.getString('password') ?? "нет пароляяя";
notifyListeners();
}
That's how I implement my Provider
body: MultiProvider(
providers: [
ChangeNotifierProvider<ApiClient>(create: (_)=>ApiClient()),
ChangeNotifierProvider<FinalModel>(create: (_)=>FinalModel()),
],
child: FinalScreenState(),
),
In initState, I call this function.
#override
void initState() {
super.initState();
finalModel = Provider.of<FinalModel>(context,listen: false);
getDataPerson();
}
Future getDataPerson() async{
return await finalModel.getDataPerson();
}
And in the code I get these variables and paste them into Text
idController.text = finalModel.getId.toString();
return Padding(padding: EdgeInsets.only(top: 15, bottom: 10,right: 1,left: 1),
child:Center(
child: TextField(
controller: idController,
))
);
However, only the values that I wrote in the code are inserted into the text. In this line it is "3231313"
prefs.getString('phone') ?? "3231313";
I tried calling the get Data Person function in different places in the code. In the build method itself.
The data I want to insert I take from json immediately after the user logs in using the button and receives a response from the api
var statusOne =await apiClient.signIn(_loginEmail1.text, _passwordParol1.text);
Map<String, dynamic> map = jsonDecode(rawJson);
bool status = map['status'];
if (status == true) {
//Entry in SharedPreferences
setDataPerson();
//The screen on which the data is displayed
Navigator.pushReplacementNamed(context, 'final');}
method setDataPerson()
void setDataPerson() async {
final prefs = await SharedPreferences.getInstance();
var statusOne =
await apiClient.signIn(_loginEmail1.text, _passwordParol1.text);
var rawJson = AuthModel.fromJson(statusOne.data);
await prefs.setInt('id', rawJson.data!.performerId!);
await prefs.setString('name', rawJson.data!.fullName!);
await prefs.setString('phone', rawJson.data!.phone!);
await prefs.setString('accessToken', rawJson.data!.accessToken!);
}
Build method
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IdWidget(),
NameWidget(),
PhoneWidget(),
EmailWidget(),
],
);
}
PhoneWidget
class PhoneWidget extends StatelessWidget {
Widget build(BuildContext context) {
var finalModel = Provider.of<FinalModel>(context,listen: true);
return Padding(padding: EdgeInsets.only(top: 10, bottom: 10,right:
1,left: 1),
child: Consumer<FinalModel>(
builder: (context, model, widget) {
finalModel.phoneController.text = model.phone;
return Center(
child: TextField(
controller: finalModel.phoneController,
enabled: false,
decoration: const InputDecoration(
labelText: "Телефон",
contentPadding: EdgeInsets.only(left: 10, top: 10, bottom: 10),
border: OutlineInputBorder(),
),
));
})
);
}
}
However, even if you do not receive this data from the network and just write a regular string, the data will not have time to be displayed on the screen.
It's worth noting here that when I restart the screen. And that is, I update the initState and build method. Then the data is updated and immediately displayed on the screen
I am not considering inserting the listen:true parameter into provider, , because the getData Person function will be called too often.
Wrap Consumer widget to your Columnwidget so registered listeners will be called.
Widget IdWidget() {
return Consumer<YourModelClass>(
builder: (context, model, widget) => Padding(
padding: EdgeInsets.only(top: 15, bottom: 10, right: 1, left: 1),
child: Center(
child: TextField(
controller: model.id,
))));
}
Trying to create a chat application. I'm able to send and listen to the messages sent on the websocket.
The listen stream listens for any data incoming and appends it to the List which I'm using to manage the chat of the widget.
I'm using listview.builder which uses the list length and changes state of the list to show new messages.
I'm a bit confused if the websocket if listening to the same messages multiple times or the listview.builder is building the widget the amount of times length of message list is.
I'm using Django channels for websocket and print the number of messages sent is proper so nothing on server side for sure.
ERROR: -
For example
on 1st message, I see 1 message
on 2nd message, I see 2 messages being built
on 3rd message, I see 3 messages being built
... and so on.
Websocket code
class NotificationController {
static final NotificationController _singleton =
NotificationController._internal();
StreamController<String> streamController =
StreamController.broadcast(sync: true);
IOWebSocketChannel? channel;
late var channelStream = channel?.stream.asBroadcastStream();
factory NotificationController() {
return _singleton;
}
NotificationController._internal() {
initWebSocketConnection();
}
initWebSocketConnection() async {
var storedUserInfo = storage.getUserInfoStorage();
Map storedData = await storedUserInfo;
String userID = storedData['user_id'];
try {
channel = IOWebSocketChannel.connect(
Uri.parse('ws://192.168.1.109:8001/chat/$userID/'),
pingInterval: const Duration(seconds: 10),
);
} on Exception catch (e) {
return await initWebSocketConnection();
}
print("socket connection initializied");
channel?.sink.done.then((dynamic _) => _onDisconnected());
}
//int i = 0;
void sendMessage(messageObject, Function messageListener) {
try {
channelStream?.listen((data) {
//i += 1;
Map _message = json.decode(data);
messageListener(_message);
});
channel?.sink.add(json.encode(messageObject));
} on Exception catch (e) {
print(e);
}
}
void _onDisconnected() {
initWebSocketConnection();
}
}
Lisview.builder code
List<ChatMessage> messages = [];
void messageListener(Map message) {
setState(() {
messages.add(
ChatMessage(message: message['message'], toUser: message['to_user']));
});
}
ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
return Stack(
children: <Widget>[
Container(
padding: const EdgeInsets.only(
left: 14,
right: 14,
top: 10,
bottom: 10,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color:
messages[index].toUser == widget.userUid
? Colors.greenAccent
: Colors.blue,
),
alignment:
messages[index].toUser == widget.userUid
? Alignment.bottomRight
: Alignment.topLeft,
padding: const EdgeInsets.all(16),
child: Text(
messages[index].message as String,
style: const TextStyle(fontSize: 15),
),
),
),
],
);
}),
Here's the context:
In my app, users can create a question, and all questions will be displayed on a certain page. This is done with a ListView.builder whose itemBuilder property returns a QuestionTile.
The problem:
If I create a new question, the text of the new question is (usually) displayed as the text of the previous question.
Here's a picture of me adding three questions in order, "testqn123", "testqn456", "testqn789", but all are displayed as "testqn123".
Hot restarting the app will display the correct texts for each question, but hot reloading wont work.
In my _QuestionTileState class, if I change the line responsible for displaying the text of the question on the page, from
child: Text(text)
to
child: Text(widget.text)
the issue will be resolved for good. I'm not super familiar with how hot restart/reload and state works in flutter, but can someone explain all of this?
Here is the code for QuestionTile and its corresponding State class, and the line changed is the very last line with words in it:
class QuestionTile extends StatefulWidget {
final String text;
final String roomName;
final String roomID;
final String questionID; //
QuestionTile({this.questionID, this.text, this.roomName, this.roomID});
#override
_QuestionTileState createState() => _QuestionTileState(text);
}
class _QuestionTileState extends State<QuestionTile> {
final String text;
int netVotes = 0;
bool expand = false;
bool alreadyUpvoted = false;
bool alreadyDownvoted = false;
_QuestionTileState(this.text);
void toggleExpansion() {
setState(() => expand = !expand);
}
#override
Widget build(BuildContext context) {
RoomDbService dbService = RoomDbService(widget.roomName, widget.roomID);
final user = Provider.of<User>(context);
print(widget.text + " with questionID of " + widget.questionID);
return expand
? ExpandedQuestionTile(text, netVotes, toggleExpansion)
: Card(
elevation: 10,
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 7, 15, 7),
child: GestureDetector(
onTap: () => {
Navigator.pushNamed(context, "/ChatRoomPage", arguments: {
"question": widget.text,
"questionID": widget.questionID,
"roomName": widget.roomName,
"roomID": widget.roomID,
})
},
child: new Row(
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Column(
// the stack overflow functionality
children: <Widget>[
InkWell(
child: alreadyUpvoted
? Icon(Icons.arrow_drop_up,
color: Colors.blue[500])
: Icon(Icons.arrow_drop_up),
onTap: () {
dynamic result = dbService.upvoteQuestion(
user.uid, widget.questionID);
setState(() {
alreadyUpvoted = !alreadyUpvoted;
if (alreadyDownvoted) {
alreadyDownvoted = false;
}
});
},
),
StreamBuilder<DocumentSnapshot>(
stream: dbService.getQuestionVotes(widget.questionID),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
// print("Current Votes: " + "${snapshot.data.data["votes"]}");
// print("questionID: " + widget.questionID);
return Text("${snapshot.data.data["votes"]}");
}
},
),
InkWell(
child: alreadyDownvoted
? Icon(Icons.arrow_drop_down,
color: Colors.red[500])
: Icon(Icons.arrow_drop_down),
onTap: () {
dbService.downvoteQuestion(
user.uid, widget.questionID);
setState(() {
alreadyDownvoted = !alreadyDownvoted;
if (alreadyUpvoted) {
alreadyUpvoted = false;
}
});
},
),
],
),
Container(
//color: Colors.red[100],
width: 290,
child: Align(
alignment: Alignment.centerLeft,
child: Text(text)), // problem solved if changed to Text(widget.text)
),
}
}
You can wrap your UI with a Stream Builder, this will allow the UI to update every time any value changes from Firestore.
Since you are using an item builder you can wrap the widget that is placed with the item builder.
That Should update the UI
I want to stop listening to snapshot updates. The snapshot keeps listening to updates even after the screen is closed. I am using the below code to listen to the updates.
CollectionReference reference = Firestore.instance.collection('Events');
reference.snapshots().listen((querySnapshot) {
querySnapshot.documentChanges.forEach((change) {
// Do something with change
});
})
Your listener is of type StreamSubscription, so you can call some helpful methods on your listener such as cancel()
CollectionReference reference = Firestore.instance.collection('Events');
StreamSubscription<QuerySnapshot> streamSub = reference.snapshots().listen((querySnapshot) {
querySnapshot.documentChanges.forEach((change) {
// Do something with change
});
});
//somewhere
streamSub.cancel();
The listen method returns a Subscription
This class is used to cancel the listening.
You should store inside your state that object to cancel subscription on dispose.
Very late answer, but I thought I'd complete the previous answers with a code sample as it might be useful to others.
class EventsScreen extends StatefulWidget {
EventsScreen({Key key}) : super(key: key);
#override
_EventsScreenState createState() => _EventsScreenState();
}
class _EventsScreenState extends State<EventsScreen> {
StreamSubscription<QuerySnapshot> _eventsSubscription;
#override
void initState() {
// Initialise your stream subscription once
CollectionReference eventsReference = Firestore.instance.collection('Events');
_eventsSubscription = eventsReference.snapshots().listen((snapshot) => _onEventsSnapshot);
super.initState();
}
#override
Widget build(BuildContext context) {
// Build your widget here
return Container();
}
void _onEventsSnapshot(QuerySnapshot snapshot) {
// Remove the setState() call if you don't want to refresh your screen whenever you get a fresh snapshot
setState(() {
snapshot?.documentChanges?.forEach(
(docChange) => {
// If you need to do something for each document change, do it here.
},
);
// Anything you might do every time you get a fresh snapshot can be done here.
});
}
#override
void dispose() {
// Cancel your subscription when the screen is disposed
_eventsSubscription?.cancel();
super.dispose();
}
}
This approach leverages the StatefulWidget's state to handle documents changes.
A better approach would be to use a Provider or the BLoC pattern so that the subscription to your Firestore is not created and handled in the UI.
the correct way is stream.pause()
so the listener will be on pause mode
cancel() destroy the listener and his content
For me, I simply use take on the snapshot method. Basically, the same principle as rxjs with Firebase. You take one stream of data and stop listening.
return collection
.withConverter<Discount>(
fromFirestore: (snapshots, _) => Discount.fromMap(snapshots
.data()!),
toFirestore: (discount, _) => discount
.toMap(),
)
.snapshots()
.take(1); // Take 1 to stop listening to events
If you run and print out the ConnectionState, you'll see it goes to Done once it fetches all the docs in the collection.
Since this is relating to Flutter and therefore probably to widgets changing on new snapshot events, please consider using StreamBuilder to easily delegate stream management and build great reactive UIs.
Container(
alignment: FractionalOffset.center,
color: Colors.white,
child: StreamBuilder<int>(
stream: _bids,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
List<Widget> children;
if (snapshot.hasError) {
children = <Widget>[
const Icon(
Icons.error_outline,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
),
];
} else {
switch (snapshot.connectionState) {
case ConnectionState.none:
children = const <Widget>[
Icon(
Icons.info,
),
Padding(
padding: EdgeInsets.only(top: 16),
child: Text('Select a lot'),
)
];
break;
case ConnectionState.waiting:
children = const <Widget>[
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: Text('Awaiting bids...'),
)
];
break;
case ConnectionState.active:
children = <Widget>[
const Icon(
Icons.check_circle_outline,
color: Colors.green,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('\$${snapshot.data}'),
)
];
break;
case ConnectionState.done:
children = <Widget>[
const Icon(
Icons.info,
color: Colors.blue,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('\$${snapshot.data} (closed)'),
)
];
break;
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
);
},
)
Or just use snapshot.hasData and snapshot.hasError to switch between showing Circle, error, and actual data.