Flutter streamProvider not updating - flutter

When a user posts a comment. It stores the comment into the database (Mysql) but the streamProvider is not updating the listview for some reason. I'm able to access the provider and the commentData. But when I post a new comment. through add method. As I said, the listview does not display the new comment which has been sent to the database.
class CommentModel {
final int reportId;
final String text;
const CommentModel(this.reportId, this.text);
}
class CommentProvider {
Stream<List<CommentModel>> intStream(int reportId) {
return Stream.fromFuture(getComments(reportId));
}
Future<List<CommentModel>> getComments(int reportId) async {
final comments = await _fetchComments(reportId);
final List<CommentModel> messages = List<CommentModel>();
for (int i = 0; i < comments.length; i++) {
messages.add(CommentModel(reportId, comments[i]["text"])));
}
return messages;
}
Future<void> add(CommentModel data) async {
await _postComment(data.reportId, data.text);
}
}
MultiProvider(
providers: [
ChangeNotifierProvider<CommentProvider>(create: (_) => CommentProvider()),
StreamProvider<List<CommentModel>>(
create: (_) => CommentProvider().intStream(int.tryParse(reportData["id"])),
initialData: null,
),
],
child: CardCommentWidget(
reportId: int.tryParse(reportData["id"]),
),
),
final commentData = Provider.of<List<CommentModel>>(context);
ListView.builder(
key: PageStorageKey("commentsScroll"),
shrinkWrap: true,
itemCount: commentData.length,
itemBuilder: (BuildContext context, int index) {
final comment = commentData[index];
return Text(comment.text);
},
),

Looks like you simply convert the out put of the getComments method to a stream. Meaning when you call the intStream method you will get a stream which only emits the results of the getComments method once. There is nothing letting the stream know that more items are added.
I can't guess what kind of database you are using to back this up and which "streaming" capabilities it has but someone needs to let your stream know that a new item has been added. One way to solve this would be something like this:
Declare a StreamController which will act as stream and sink;
In the intStream method, initialize the StreamController with the outcome of the getComments method and return the stream of the StreamController;
After saving the comment to the database, add the comment to the StreamController.
In code this could look something like this:
class CommentProvider {
final StreamController<List<CommentModel>> _streamController;
Stream<List<CommentModel>> intStream(int reportId) {
// Initialize a new instance of the StreamController
// and emit each comment when someone starts listening
// to the stream.
if (_streamController == null) {
_streamController = StreamController<List<CommentModel>>
.broadcast(
onListen: () async => await getComments(reportId),
onErrror: (error) {
// Handle error here...
},
);
}
return _streamController.stream;
}
Future<List<CommentModel>> getComments(int reportId) async {
final comments = await _fetchComments(reportId);
final List<CommentModel> messages = List<CommentModel>();
for (int i = 0; i < comments.length; i++) {
messages.add(CommentModel(reportId, comments[i]["text"])));
}
return messages;
}
Future<void> add(CommentModel data) async {
await _postComment(data.reportId, data.text);
// Emit the updated list containing the added
// comment on the stream.
if (_streamController != null) {
final comments = await getComments(data.reportId);
_streamController?.add(comments);
}
}
}
This above code is an example and should work. You might need to tweak it a little bit as mentioned in the comments that are part of the code example. And like I mentioned some databases directly support streaming (e.g. Firebase) which directly return the result of a query as a stream and will automatically add items to the stream when they are added to the database and match the query criteria. I couldn't deduce this from your code though.
Some reading material on working with the StreamController class can be found here:
StreamController class;
Using a StreamController
EDIT:
I updated the logic in the add method to make sure the _streamController if not null.
EDIT 2:
Updated the code to return a stream emitting lists of comments, so we can better facilitate the ListView class.

Related

Signal R how properly take data from events?

My problem is: I subscribed to events in signalR, but I don’t understand how to correctly take the data from this answer and put it in UI. The documentation shows the same method as in my code, but an empty list is returned to me in the user interface. In my case i get the data at the moment when the event comes, until i get the data from the event the list is empty and i thought to capture this data somehow, because i have to show it to the user. But the data from the event is not coming to my UI
But there is data in the console. Here they are - [{warpedBox: [604.3993, 290.7302, 1106.364, 290.7302, 1106.364, 530.2628, 604.3993, 530.2628], name: Cats, date: 2022-09-05T09:01:11.9003992+03:00, additionInfo: new animal detected, baseName: TestBase, imageGuid: 00000000-0000-0000-0000-000000000000}] . How to get data from an event?
Many thanks to Robert Sandberg
I made some changes and now my code is like that (also I added UI part, because I don't understand how to make it work)
My code is now:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
class Animals {
Alarmplayer alarmplayer = Alarmplayer();
Future<void> fetchAnimals(CallbackFunc arguments) async {
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final hubConnection = HubConnectionBuilder()
.withUrl(
'secure_link',
options: httpConnectionOptions,
)
.build();
await hubConnection.start();
hubConnection.on('Animals', (arguments);
alarmplayer.Alarm(url: 'assets/wanted.mp3', volume: 0.01);
await Future.delayed(const Duration(seconds: 2))
.then((value) => alarmplayer.StopAlarm());
});
}
return agruments
}
My UI-part:
class _TestScreenState extends State<TestScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder<void>(
///can't understand how to pass here arguments
future: fetchAnimals(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(children: [
Text(snapshot.data?[index]['name']),
]);
},
);
}),
I am sorry but I am really noob in that and can't understand how can I use it in the UI
If I understand you correctly, then you mean that you get an empty list where you do the print(detectedAnimals ); ? With this setup, your method will basically always return an empty list.
And that is because your method have already executed that print line (and the return statement) when you get the event over SignalR.
The callback you send into the hubConnection must have a way to report back to the UI, meaning it have to have a way to communicate back to the method that is calling fetchAnimals() WHEN the callback is executed.
So I'd inject the callback into fetchAnimals() as such:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
Future<void> fetchAnimals(CallbackFunc callback) async {
....
hubConnection.on('Animals', callback);
...
}

How to set multiple StateNotifierProvider (s) with dynamicaly loaded async data?

I'm completely stuck with the task below.
So, the idea is to solve these steps using Riverpod
Fetch data from db with some kind of Future async while pausing the app (display SomeLoadingPage() etc.)
Once the data has loaded:
2.1 initialize multiple global StateNotifierProviders which utilize the data in their constructors and can further be used throughout the app with methods to update their states.
2.2 then show MainScreen() and the rest of UI
So far I've tried something like this:
class UserData extends StateNotifier<AsyncValue<Map>> { // just <Map> for now, for simplicity
UserData() : super(const AsyncValue.loading()) {
init();
}
Future<void> init() async {
state = const AsyncValue.loading();
try {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
state = AsyncValue.data(response.data as Map<String, dynamic>);
} catch (e) {
state = AsyncValue.error(e);
}}}
final userDataProvider = StateNotifierProvider<UserData, AsyncValue<Map>>((ref) => UserData());
final loadingAppDataProvider = FutureProvider<bool>((ref) async {
final userData = await ref.watch(userDataProvider.future);
return userData.isNotEmpty;
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
future: ref.watch(loadingAppDataProvider.future),
builder: (ctx, AsyncSnapshot snap) {
// everything here is simplified for the sake of a question
final Widget toReturn;
if (snap.connectionState == ConnectionState.waiting) {
toReturn = const SomeLoadingPage();
} else {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
}
return toReturn;},);}}
I intentionally use FutureBuilder and not .when() because in future i may intend to use Future.wait([]) with multiple futures
This works so far, but the troubles come when I want to implement some kind of update() methods inside UserData and listen to its variables through the entire app. Something like
late Map userData = state.value ?? {};
late Map<String, dynamic> settings = userData['settings'] as Map<String, dynamic>;
void changeLang(String lang) {
print('change');
for (final key in settings.keys) {
if (key == 'lang') settings[key] = lang;
state = state.whenData((data) => {...data});
}
}
SomeLoadingPage() appears on each changeLang() method call.
In short:
I really want to have several StateNotifierProviders with the ability to modify their state from the inside and listen to it from outside. But fetch the initial state from database and make the intire app wait for this data to be fetched and these providers to be initilized.
So, I guess I figured how to solve this:
final futureExampleProvider = FutureProvider<Map>((ref) async {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
return response.data as Map;
});
final exampleProvider = StateNotifierProvider<Example, Map>((ref) {
// we get AsyncValue from FutureNotifier
final data = ref.read(futureExampleProvider);
// and wait for it to load
return data.when(
// in fact we never get loading state because of FutureBuilder in UI
loading: () => Example({'loading': 'yes'}),
error: (e, st) => Example({'error': 'yes'}),
data: (data) => Example(data),
);
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
// future: ref.watch(userDataProvider.future),
future: ref.watch(futureExampleProvider.future),
builder: (ctx, AsyncSnapshot snap) {
final Widget toReturn;
if (snap.data != null) {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
} else {
// this is the only 'Loading' UI the user see before everything get loaded
toReturn = const Text('loading');
}
return toReturn;
},
);
}
}
class Example extends StateNotifier<Map> {
Example(this.initData) : super({}) {
// here comes initial data loaded from FutureProvider
state = initData;
}
// it can be used further to refer to the initial data, kinda like cache
Map initData;
// this way we can extract any parts of initData
late Map aaa = state['bbb'] as Map
// this method can be called from UI
void ccc() {
// modify and update data
aaa = {'someKey':'someValue'};
// trigger update
state = {...state};
}
}
This works for me, at least on this level of complexity.
I'll leave question unsolved in case there are some better suggestions.

ValueListenableBuilder is not rebuilding the screen, when hotreloading, it is working

I'm trying to build a note app, all data and other things is working perfectly, cos the data is displaying to the screen when the code file is saving, its weird , first time facing this problem
in short, the valuelistanble is not listening when the data adding from app, but when just hot reloading the data is displaying
how can i fix this,
here is the code
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback((_) async {
final value = await NoteDB.instance.getAllNotes();
});
____________________________________________
____________________________________________
//code line for aligment
Expanded(
child: ValueListenableBuilder(
valueListenable: NoteDB.instance.noteListNotifier,
builder: (context, List<NoteModel> newNotes, _) {
return GridView.count(
childAspectRatio: 3 / 4,
crossAxisCount: 2,
mainAxisSpacing: 34,
crossAxisSpacing: 30,
padding: const EdgeInsets.all(20),
//generating list for all note
children: List.generate(
newNotes.length,
(index) {
//setting the notelist to a variable called [note]
final note = newNotes[index];
if (note.id == null) {
//if the note's id is null set to sizedbox
//the note id never be null
const SizedBox();
}
return NoteItem(
id: note.id!,
//the ?? is the statement (if null)
content: note.content ?? 'No Content',
title: note.title ?? 'No Title',
);
},
),
);
},
)),
here is the NoteDB.instance.getAllNotes(); function
#override
Future<List<NoteModel>> getAllNotes() async {
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
final noteResponse = GetAllNotes.fromJson(_result.data);
noteListNotifier.value.clear();
noteListNotifier.value.addAll(noteResponse.data.reversed);
noteListNotifier.notifyListeners();
return noteResponse.data;
} else {
noteListNotifier.value.clear();
return [];
}
}
and also there is a page to create note , and when create note button pressed there is only one function calling here is function
Future<void> saveNote() async {
final title = titleController.text;
final content = contentController.text;
final _newNote = NoteModel.create(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
content: content,
);
final newNote = await NoteDB().createNote(_newNote);
if (newNote != null) {
print('Data Added to the DataBase Succesfully!');
Navigator.of(scaffoldKey.currentContext!).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => HomePage()),
(Route<dynamic> route) => false);
} else {
print('Error caught while data adding to the DataBase');
}
}
everything work fine, but while add the data the UI isn't refreshing even tho notifier is active
and if you need full code please have a look at this github link : https://github.com/Mishalhaneef/Note-app
Since this ValueNotifier has a type of List<NoteModel>, the value will not change when you add new items to the list or delete from it or clear all. The value here is a reference to the list which does not change.
You have to assign a new value to it, like:
noteListNotifier.value = List<NoteModel>[<add your current items here>];
You can manipulate your current list with List.from, removeWhere, add etc., and then re-assign the complete list.
Besides you don't need to call notifyListeners in case of a ValueNotifier, the framework handles it, see here.
Another approach would be to use a custom ChangeNotifierProvider where you can call notifyListeners when the contents of your list are changed.
Some further suggestions:
In your homescreen.dart file, instead of NoteDB.instance.noteListNotifier.value[index] you can use newNotes[index].
In data.dart, within getAllNotes, you have to set a new value for noteListNotifier in order to get the changes propagated. Currently you are just modifying items in this list and that is not considered to be a change. Try this code:
#override
Future<List<NoteModel>> getAllNotes() async {
//patching all data from local server using the url from [Post Man]
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
//if the result data is not null the rest operation will be operate
//recived data's data decoding to json map
final _resultAsJsonMap = jsonDecode(_result.data);
//and that map converting to dart class and storing to another variable
final getNoteResponse = GetAllNotes.fromJson(_resultAsJsonMap);
noteListNotifier.value = getNoteResponse.data.reversed;
//and returning the class
return getNoteResponse.data;
} else {
noteListNotifier.value = <NoteModel>[];
return [];
}
}

The await keyword in Flutter is not waiting

I am learning Dart and Flutter with a small mobile application. I have read everything I found about the await keyword but I still have problems I don't understand. Below the simplified code. I removed everything I thought it is unnessecary for understanding my problem. If something important is missing, please tell me.
My problem is the following line below the TODO (line 7): List locations = await _useCaseManager.findLocations(); In this method I query the database. I want the application to wait until the query is finished and the data are returned.
I call the method _findFirstLocation() in the build() method. But Flutter does not wait for the data. It goes on with the rest of the code, especially with the method createNextAppointmentsList(). In this method I need the data the application should wait for for the future - the attribute _selectedLocation. But because Flutter is not waiting, _selectedLocation is null.
This is the relevant part of the class.
class _AppointmentsOverviewScreenState extends State<AppointsmentsOverviewScreen> {
UseCaseManager _useCaseManager;
Location _selectedLocation;
void _findFirstLocation() async {
// TODO Hier wartet er schon wieder nicht.
List<Location> locations = await _useCaseManager.findLocations();
_selectedLocation = locations.first;
print(_selectedLocation);
}
#override
Widget build(BuildContext context) {
_useCaseManager = UseCaseManager.getInstance();
_findFirstLocation();
return Scaffold(
appBar: AppBar(
title: Text(LabelConstants.TITLE_APPOINTMENT_OVERVIEW),
),
body: Column(
children: <Widget>[
Container(child: createNextAppointmentsList(),)
],
),
);
}
Widget createNextAppointmentsList() {
return FutureBuilder<List<Appointment>>(
future: _useCaseManager.findAppointmentsForActiveGarbageCans(_selectedLocation.locationId),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(snapshot.data[index].garbageCanName),
subtitle: Text(snapshot.data[index].date),
);
},
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
);
}
}
In the method _findFirstLocation there is the following method with a database query called.
Future<List<Location>> findLocations() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(DatabaseConstants.LOCATION_TABLE_NAME);
return List.generate(maps.length, (i) {
Location location = Location(
locationId: maps[i][DatabaseConstants.COLUMN_NAME_LOCATION_ID],
street: maps[i][DatabaseConstants.COLUMN_NAME_STREET],
houseNumber: maps[i][DatabaseConstants.COLUMN_NAME_HOUSENUMBER],
zipCode: maps[i][DatabaseConstants.COLUMN_NAME_ZIP_CODE],
city: maps[i][DatabaseConstants.COLUMN_NAME_CITY],
);
return location;
});
}
Because I have had already problems with await and the cause for these problems was a foreach() with a lambda expression, I tried another type of for loop as alternative:
Future<List<Location>> findLocations() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(DatabaseConstants.LOCATION_TABLE_NAME);
final List<Location> locations = List();
for (int i = 0; i < maps.length; i++) {
var locationFromDatabase = maps[i];
Location location = Location(
locationId: maps[i][DatabaseConstants.COLUMN_NAME_LOCATION_ID],
street: maps[i][DatabaseConstants.COLUMN_NAME_STREET],
houseNumber: maps[i][DatabaseConstants.COLUMN_NAME_HOUSENUMBER],
zipCode: maps[i][DatabaseConstants.COLUMN_NAME_ZIP_CODE],
city: maps[i][DatabaseConstants.COLUMN_NAME_CITY],
);
locations.add(location);
}
return locations;
}
But in both cases, the application is not waiting for the data and I don't understand the reason.
Thank you in advance.
Christopher
You have several problems in your code:
First of all, if you want to 'await' you have to use the word await when you want the flow to await. You do it in your _findFirstLocation() function but you are not doing it when you call it, hence, you should call it like this:
await _findFirstLocation();
But even this is not correct, because you are trying to block the UI thread, which is totally prohibited (this would cause the UI to freeze having a poor user experience).
In this cases, what you need is a FutureBuilder, in which you specify what should happen while your background process is running, what should happen when it throws an error and what should happen when the result is returned.
And lastly, I suggest you to not initialize variables in the build() method, as it can be called multiple times:
_useCaseManager = UseCaseManager.getInstance();
I would move that to the body of the class if possible, and when not possible put it in the initState() method.
You do not await the method _findFirstLocation(); where you call it. That means the call will not wait for the result, it will just go to the next instruction and continue with that.
In this special case, you cannot actually await it because the build method is not async and you cannot change that. In this case, you need a FutureBuilder to show something like a spinner or wait dialog until your results are loaded.
You can find more information here:
What is a Future and how do I use it?

what changes do i need to make to this code so that it does not show real time changes from the database..but only loads new data when refreshed.?

I need the onRefresh function here to refresh the brewList when i pull. I tried a few things but its not working and only refreshes after hot reload.
so when i pull down to refresh it should reflect any changes that were made in the firestore.
class BrewList extends StatefulWidget {
#override
_BrewListState createState() => _BrewListState();
}
class _BrewListState extends State<BrewList> {
#override
Widget build(BuildContext context) {
final brews = Provider.of<List<Brew>>(context) ?? [];
return RefreshIndicator(
onRefresh: refresh,
ListView.builder(
itemCount: brews.length,
itemBuilder: (context, index) {
return BrewTile(brew: brews[index]);
},
),
);
}
}
// get brews future
Future<List<Brew>> get brews async {
QuerySnapshot snapshot = await brewCollection.getDocuments();
return _brewListFromSnapshot(snapshot);
}
List<Brew> _brewListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc){
//print(doc.data);
return Brew(
name: doc.data['name'] ?? '',
strength: doc.data['strength'] ?? 0,
sugars: doc.data['sugars'] ?? '0'
);
}).toList();
}
try changing your stream to this and see if this helps
// get brews future
Future<List<Brew>> get brews async {
QuerySnapshot snapshot = await brewCollection.getDocuments();
return _brewListFromSnapshot(snapshot);
}
and then just call your provider when you want to update. My guess is you are also using a stream somewhere too doesn't seem necessary if you are not actively updating content.
okay I took a look at the github for this project and there are probably other changes you need to make first of now that your not using that stream it doesn't make sense to use a StreamProvider change that to a FutureProvider setup should be very similar and actually i know it was my orgional suggestion to change the provider to listen false but after looking at your project try changing back. since you do want to make periodic updates
Future refreshList() async {
setState(() {
brews = Provider.of<List<Brew>>(context) ;
});
}