Flutter: How to Notify main widget after inserting all data into database - sqlite

In my StatefulWidget in initState i have a method:
#override
void initState() {
super.initState();
getMyChannels();
}
getMyChannels method run a http method to get data from service and store data into database:
void getMyChannels() async {
// get data from servise and store them
_myChannel = await MyToolsProvider()
.getChannelMe("df6b88b6-f47d****");
getToolsRelToChannels(); // get data from database
setState(() {});
}
As you can see i have getToolsRelToChannels method. This method fetch data from local database. This data must be stored by await MyToolsProvider()
.getChannelMe("df6b88b6-f47d****"); method into database.
This is .getChannelMe method:
Future<ProgramsByActiveToolsModel> getChannelMe(String auth) async {
Map<String, dynamic> header = {
'Content-Type': "application/json",
"Authorization": 'Bearer $auth'
};
try {
var result = await NetworkCLient()
.getRequest(url: '$URL/api/me', header: header);
if (result != null) {
var programsByActiveToolsModel =
ProgramsByActiveToolsModel.fromJson(result);
if (programsByActiveToolsModel.responseCode == 200) {
programsByActiveToolsModel.data.forEach((item) async {
await DBProvider.db.addMyTools(item);
saveToolsbyChannelId(header, item.id);
});
return programsByActiveToolsModel;
} else
return null;
}
} catch (e) {
throw e;
}
}
In addMyTools method i store each data in one table of my database and i call saveToolsbyChannelId method for each item. This is main data that I need too.
Future<void> saveToolsbyChannelId(Map header, int channelId) async {
header["Authorization"] = 'Bearer 92122926-****';
try {
var count = await DBProvider.db.getCountToolsbyChannelId(channelId);
if (count == 0) {
var result = await NetworkCLient().getRequest(
url: '$URL/api/channel/$channelId', header: header);
if (result != null) {
var logToolsRunModel = LogTools.LogToolsRunModel.fromJson(result);
if (logToolsRunModel.responseCode == 200) {
logToolsRunModel.data.forEach((item) {
DBProvider.db.addTools(item);
});
}
}
}
} catch (e) {
throw e;
}
}
After fetching data from my service i sore these data into sqlite database .Now it's await MyToolsProvider().getChannelMe job is done!
It's time to explain getToolsRelToChannels();:
void getToolsRelToChannels() async {
_toolsRun =
await MyToolsProvider().getToolsRelatedMyChannel(_selectedChannel);
setState(() {});
}
getToolsRelatedMyChannel this method must be wait till all data in this method DBProvider.db.addTools(item) added into database and after inserting my widget must be recreated.
Future<List<ToolsByChannelIdDbModel>> getToolsRelatedMyChannel(
int channelId) async {
List<ToolsByChannelIdDbModel> list = List<ToolsByChannelIdDbModel>();
try {
var result = await DBProvider.db.getToolsById(channelId);
result.forEach((item) {
list.add(ToolsByChannelIdDbModel.fromJson(item));
});
return list;
} catch (e) {
print(e);
}
}
}
but my code is wrong because after running await MyToolsProvider().getChannelMe(***) getToolsRelToChannels method is executed and nothing is stored into database to fetching yet!!!
How could i notify my main widget after finishing database inserting???
I can not to use FutureBuilder because when run for first time, my database is empty !!!

You should await saveToolsbyChannelId in getChannelMe and await DBProvider.db.addTools(item); in saveToolsbyChannelId, otherwise you are trying to read from the database before the data has been written to it. This is assuming the rest of your code is correct, which we cannot tell for sure because there are lots of variables such as _selectedChannel that we know nothing about.

UPDATED - Check below.
What you want is to await ALL async operations. In your case
#override
void initState() async {
super.initState();
await getMyChannels();
}
and
await saveToolsbyChannelId(header, item.id);
and if DBProvider.db.addTools is asynchronous, then
await DBProvider.db.addTools(item);
UPDATE:
Since its not possible to make initState() async, you can use a callback in the future:
#override
void initState() {
super.initState();
var channelsFuture = getMyChannels();
channelsFuture.then((resp){
setState(() {});
});
}
I'd suggest that you reconsider your whole approach (from a clean architecture point of view). Take a look at the BLoC pattern. It basically boils down to this :
There's another class (called the BLoC) which handles the business logic (getting data from network and adding to the database). The widget class only handles the UI.
You kick off your asynchronous processing in the BLoC class from the widget's initState(). There's a stream listener in the widget that listens to the completion of the task by BLoC.
As soon as the BLoC completes the async task, it notifies the widget by using a stream. The stream listener in the widget knows that the data is ready and it updates the UI by calling setState();
Done !!

Related

Flutter Firebase async query not retrieving data inside a stream function

I am trying to query a User from firebase within another query but for some reason but I can't get the code to work
The function the wont run is await usersRef.doc(uid).get(); and can be found here:
static getUserData(String uid) async {
return await usersRef.doc(uid).get();
}
static DirectMessageListModel getDocData(QueryDocumentSnapshot qdoc, String uid) {
Userdata postUser = Userdata.fromDoc(getUserData(uid));
return DirectMessageListModel.fromDoc(qdoc, postUser);
}
static DirectMessageListModel fromDoc(QueryDocumentSnapshot doc, Userdata altUser) {
return DirectMessageListModel(
doc['chatId'],
doc['lastMsgContent'],
doc['lastMsgType'],
altUser
);
}
parent function:
Stream<List<DirectMessageListModel>> getMeassageList(){
var snaps = FirebaseFirestore.instance.collection('directMessages').where('users', arrayContains: userdata!.uid).snapshots();
List<String> usersListElement = [];
return snaps.map((event) { return event.docs.map((e) {
usersListElement = [e.get('users')[0], e.get('users')[1]];
usersListElement.remove(userdata!.uid);
return DirectMessageListModel.getDocData(e, usersListElement.first);
}).toList();
});
}
You forgot to wait for the future getUserData(uid) to complete.
Try this:
static Future<DocumentSnapshot<Object>> getUserData(String uid) async {
return await usersRef.doc(uid).get();
}
static DirectMessageListModel getDocData(
QueryDocumentSnapshot qdoc,
String uid,
) async {
Userdata postUser = Userdata.fromDoc(await getUserData(uid)); // await here
return DirectMessageListModel.fromDoc(qdoc, postUser);
}
..
// parent function.
// Also wait for the future in the parent function.
// UPDATE BELOW! Define the parent function like this:
Stream<List<Future<DirectMessageListModel>>> getMeassageList() {
var snaps = FirebaseFirestore.instance
.collection('directMessages')
.where('users', arrayContains: userdata!.uid)
.snapshots();
List<String> usersListElement = [];
return snaps.map((event) {
return event.docs.map((e) async {
usersListElement = [e.get('users')[0], e.get('users')[1]];
usersListElement.remove(userdata!.uid);
return await DirectMessageListModel.getDocData(e, usersListElement.first);
}).toList();
});
}
NB: You are fetching user data (either sender/receiver) for each message in directMessages collection. It might be better to store just sender/receiver name in directMessages collection and simply display that. Then if the user clicks on a message, you can then fetch the full sender/receiver data.

Closure: () => Map<String, dynamic> from Function 'data':

So im trying to stream data from firestore but when printing the data I get:
I/flutter ( 8356): Closure: () => Map<String, dynamic> from Function 'data':.
I am using this code to fetch the data:
void messagesStream() async {
Stream collectionStream = _firestore.collection('messages').snapshots();
await for (var snapshot in collectionStream) {
for (var message in snapshot.docs) {
print(message.data());
}
}
When new data is added to the messages collection I get the Closure message so it is interacting with the databse.
What I want is it to print out the contents of the new document within the collection.
Any help is appreciated.
That's not the way you're supposed to iterate the results of a Stream. If you have a Stream and you want to process its results, you're supposed to use listen() to receive the results asynchronously.
Stream collectionStream = _firestore.collection('messages').snapshots();
collectionStream.listen((QuerySnapshot querySnapshot) {
querySnapshot.documents.forEach((document) => print(document.data()));
}
See also: Firestore collection query as stream in flutter
You might also want to review the documentation to learn how to query Firestore in Flutter.
void getMessages() async {
final messages= await _firestore.collection('messages').get();
for(var message in messages.docs){
print(message.data());
}
this is working check this and call getMessages() wherever you wana call
I encountered the same issue with pretty much your exact same code (sans your Stream variable). My suggestion is to delete the Stream var altogether (I tested the code below and got it to print the data from the Firestore database) :
void messagesStream() async {
await for (var snapshot in _firestore.collection('messages').snapshots()) {
for (var message in snapshot.docs) {
print(message.data());
}
}
}
Alternatively, try addding QuerySnapShot as the data type for your Stream variable (untested):
Stream<QuerySnapshot> collectionStream = _firestore.collection('messages').snapshots();
You could also replace the entire method by creating a new Stateless Widget (MessagesStream) that returns a StreamBuilder:
class MessagesStream extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection('messages').snapshots(),
builder: (context, snapshot) {
final messages = snapshot.data.docs;
for (var message in messages) {
print(message.data());
}
...and call it wherever you see fit while you test:
class _ChatScreenState extends State<ChatScreen> { (...)
body: Column(children: <Widget> [
//Just an example.
MessageStream(),
],
),
(...)
}
***Be sure you make the _fireStore (which should be a FirebaseFirestore.instance) a global variable if you're going with the Stateless Widget route.
I received this error while trying to throw a custom error class InkError:
You meed add toMap the class.
final response = await http.post(url, body: body, headers: headers);
final json = jsonDecode(response.body);
if (response.statusCode == HttpStatus.ok) {
return json;
} else {
throw InkError(
code: 0,
message: json['message'],
statusCode: response.statusCode,
).toMap();
InkError
class InkError {
/// Error code.
final int code;
/// Error message.
final String message;
/// HTTP Status Code
final int? statusCode;
const InkError({
required this.code,
required this.message,
this.statusCode,
});
factory InkError.fromJSON(Map<String, dynamic> json) => InkError(
code: json['code'] as int,
message: json['message'] as String,
statusCode: json['statusCode'],
);
Map<String, dynamic> toMap() {
return {
'code': code,
'message': message,
'statusCode': statusCode,
};
}
#override
String toString() {
return toMap().toString();
}
}

How can I correctly yield from a stream that uses Futures/async/await?

I've encountered a weird issue where if I yield* from my provider in my flutter app, the rest of the code in the function doesn't complete.
I'm using the BLoC pattern, so my _mapEventToState function looks like this:
Stream<WizardState> _mapJoiningCongregationToState(
int identifier, int password) async* {
_subscription?.cancel();
_subscription= (_provider.doThings(
id: identifier, password: password))
.listen((progress) => {
dispatch(Event(
progressMessage: progress.progressText))
}, onError: (error){
print(error);
}, onDone: (){
print('done joiining');
});
}
Then in the provider/service... this is the first attempt.
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings(
{int id, int password}) async* {
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
yield* _progressStream.stream;
}
The yield statement returns, but only after both awaited functions have completed. This makes complete sense to me, obviously I wouldn't expect the code to complete out of order and somehow run the yield* before waiting for the 'await's to complete.
In order to "subscribe" to the progress of this service though, I need to yield the stream back up to the caller, to write updates on the UI etc. In my mind, this is as simple as moving the yield* to before the first await. Like this.
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings(
{int id, int password}) async* {
yield* _progressStream.stream;
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
}
But, then setting breakpoints on the later _progressStream.add calls show that these never get called. I'm stuck on this, any idea what it could be? I know it has something to do with how I have mixed Futures and Streams.
The yield* awaits the completion of the stream it returns.
In this case, you want to return a stream immediately, then asynchronously feed some data into that stream.
Is anything else adding events to the stream controller? If not, you should be able to just do:
#override
Stream<JoinCongregationProgress> doThings({int id, int password}) async* {
await Future.delayed(Duration(seconds:2));
yield JoinCongregationProgress(progressText: "kake1...");
await Future.delayed(Duration(seconds:2));
yield JoinCongregationProgress(progressText: "kake5!!!...");
}
No stream controller is needed.
If other functions also add to the stream controller, then you do need it. You then have to splut your stream creation into an async part which updates the stream controller, and a synchronous part which returns the stream. Maybe:
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings({int id, int password}) {
() async {
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
}(); // Spin off async background task to update stream controller.
return _progressStream.stream;
}

Stream waiting and blocking other events using Bloc in Flutter while loading Firebase snapshots

I'm writing an application using Flutter, Firebase Firestore and the bloc pattern.
When the app opens I want to load some books from the firestore database, for this purpose I have defined the following bloc code:
class EditorBloc extends Bloc<BookEvent, BooksState> {
#override
BooksState get initialState => BooksLoading();
EditorBloc();
Stream<BooksState> _loadBooks() async* {
final CollectionReference bookPostRef = Firestore.instance.collection('books');
try {
Stream<QuerySnapshot> stream = bookPostRef.snapshots();
await for (QuerySnapshot querySnapshot in stream) {
List<Book> remoteBooks = List();
for (var documentSnapshot in querySnapshot.documents) {
remoteBooks.add(Book.fromDocumentSnapshot(documentSnapshot));
}
yield BooksLoaded(remoteBooks);
// If I return here I can dispatch more events but not all the snapshots will be processed
}
} catch (_) {
yield BooksNotLoaded();
}
}
Stream<BooksState> _saveBooks(Gym gym, Sector sector, BookHistory booksHistory) async* {
// Some code
}
Stream<BooksState> _addBooks(Gym gym, Sector sector, BookHistory booksHistory) async* {
// Some code
}
#override
Stream<BooksState> mapEventToState(BooksState currentState, BookEvent event) async* {
if (event is LoadBooks) {
yield* _loadBooks(event.gym, event.sector);
} else if(event is SaveBooks) {
yield BooksLoading();
yield* _saveBooks(event.gym, event.sector, event.booksHistory);
} else if (event is AddBook) {
yield* _addBooks(gym, sector, booksHistory)
}
}
}
When the application start I use the following code load the collection of books:
myBloc.dispatch(LoadBooks());
And I get notified when the books are loaded, but if I dispatch an other event like this one:
myBloc.dispatch(AddBook(newBook));
The event is never processed because the EditorBloc is still waiting for other snapshots in the method _loadBooks.
What shall I do to be able to dispatch other events?
Thanks!
One way you can approach this is by listening to the Stream inside Future. Once that Future finishes, you can then start the other events.
Future<bool> whenFinishLoading(Stream<BooksState> source) async {
await for (value in source) {
// Define a condition expecting a value from Stream
if (value) {
return value;
}
}
return false;
}
You can then use Future whenFinishLoading(Stream<BookState>) waiting for it to finish to start the next Stream event.

Flutter Return Length of Documents from Firebase

Im trying to return the length of a list of documents with this function:
Future totalLikes(postID) async {
var respectsQuery = Firestore.instance
.collection('respects')
.where('postID', isEqualTo: postID);
respectsQuery.getDocuments().then((data) {
var totalEquals = data.documents.length;
return totalEquals;
});
}
I'm initialize this in the void init state (with another function call:
void initState() {
totalLikes(postID).then((result) {
setState(() {
_totalRespects = result;
});
});
}
However, when this runs, it initially returns a null value since it doesn't have time to to fully complete. I have tried to out an "await" before the Firestore call within the Future function but get the compile error of "Await only futures."
Can anyone help me understand how I can wait for this function to fully return a non-null value before setting the state of "_totalRespsects"?
Thanks!
I think you're looking for this:
Future totalLikes(postID) async {
var respectsQuery = Firestore.instance
.collection('respects')
.where('postID', isEqualTo: postID);
var querySnapshot = await respectsQuery.getDocuments();
var totalEquals = querySnapshot.documents.length;
return totalEquals;
}
Note that this loads all documents, just to determine the number of documents, which is incredibly wasteful (especially as you get more documents). Consider keeping a document where you maintain the count as a field, so that you only have to read a single document to get the count. See aggregation queries and distributed counters in the Firestore documentation.
Perfect code for your problem:
int? total;
getLength() async {
var getDocuments = await DatabaseHelper.registerUserCollection
.where("register", isEqualTo: "yes")
.get();
setState(() {
total = getDocuments.docs.length;
});
}
#override
void initState() {
super.initState();
getLength();
if (kDebugMode) {
print(total);
}
}

Resources