How to handle stream in the background? - flutter

I'm requesting a server and server responses a Stream. The first element in the stream is the data updates the UI but other Stream events needs to be in the background. I have to return stream.first back in order to update the state.
Future<AccountResponse> cEAccount(Account request) async {
final stream = client.cEAccount(request).asBroadcastStream();
final firstRes = await stream.first;
watchBlockchainResponse(stream);
return firstRes;
}
Future watchBlockchainResponse(Stream stream) async {
await for (final res in stream) {
...
}
watchBlockchainResponse shows an overlay using overlay_support. This code is working fine. Is this a good practice? If not, how should I handle this?

You can use
stream.listen((res) {
});

Related

How make async api calls inside loop and complete loop with all data from api call. Due to async I'm losing that part of data

I'm reading json List from device memory and want to perform some operations on it's components.
When I load that list I start loop where I check each item of that list.
While in loop I add each item to new List to have updated List after loop ends so I could save it on device memory.
If some conditions are true then I use future async http call to get updated data
then theoretically I update that item of the List while staying inside loop. And thus after loop ends I must have updated Json List ready to be saved on device memory.
Problem is that While I http call inside loop, the answer delays, loop ends and new Json List is being constructed and saved on memory without the component that was supposed to be updated.
Is there any way to force wait the whole loop or something else ?
Here is the code
Future<void> readStoredData() async {
try {
final prefs = await SharedPreferences.getInstance();
_rawJsonListE = prefs.getStringList('storedData');
List<String> rawJsonListNEW = [];
bool _isNeedUpdate = false;
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
_isNeedUpdate = true;
await makeHttpCallFutureAwaitFunction(item).then((_) {
rawJsonListNEW.add(updatedItem);
});
} else {
rawJsonListNEW.add(item);
}
});
if (_isNeedUpdate) prefs.setStringList('storedData', rawJsonListNEW);
}
notifyListeners();
} catch (error) {
print('Error : ${error}');
throw error;
}
You can separate the refreshing data part to another function.
// Just need to check _rawJsonListE is empty or not
_isNeedUpdate = _rawJsonListE.isNotEmpty();
Create a new function.
Future<List<String>> checkDataAndRefresh(List<String> _rawJsonListE) async {
List<String> rawJsonListNEW = [];
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
final String newString = await makeHttpCallFutureAwaitFunction(item);
rawJsonListNEW.add(newString);
} else {
rawJsonListNEW.add(item);
}
});
return rawJsonListNEW;
}
And if _isNeedUpdate is true, do work.
if (_isNeedUpdate)
final List<String> newData = await checkDataAndRefresh(_rawJsonListE);
prefs.setStringList('storedData', newData);

Flutter check if stream is empty before close end

I'm using BehaviorSubject as a Stream controller.
In one of my functions, I want to .add more items only in case the Stream is empty of events.
#override
Future<void> fetchNextOverviewPolls() async {
if (await _pollOverviewStreamController.isEmpty) return; // My Problem
final lastDoc = await _pollOverviewStreamController.last;
final querySnapshot =
await _overviewPollsRef.startAfterDocument(lastDoc).limit(5).get();
for (final doc in querySnapshot.docs) {
_pollOverviewStreamController.add(doc);
}
}
The isEmpty property returns a value in case the Stream ends. I want to check it when the Stream is still running.
How do I do that?
BehaviorSubject supports hasValue.
In the above case, use this line instead:
if (_pollOverviewStreamController.hasValue) return;

Flutter pagination with firestore stream

How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});

How to close stream on app shutdown when created in main?

I have a custom http client that intercepts responses to provide service unavailable events via a stream:
class MyHttpClient extends http.BaseClient {
final String apiBase;
MyHttpClient({required this.apiBase});
final _controller = StreamController<ServiceUnavailableEvent>.broadcast();
Stream<ServiceUnavailableEvent> get stream => _controller.stream;
#override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final response = await request.send();
if (request.url.toString().startsWith(apiBase) &&
response.statusCode == HttpStatus.serviceUnavailable) {
_controller.sink.add(ServiceUnavailableEvent(isAvailable: false));
} else {
_controller.sink.add(ServiceUnavailableEvent(isAvailable: true));
}
return response;
}
Future<void> close() async {
await _controller.close();
}
}
The client is instanciated once in the main flutter function:
void main() {
final client = MyHttpClient(apiBase: apiBase)
runApp(MyApp(client: client));
}
Everything works fine and I added the close method on the custom http client that closes the stream. But when/how can I call this close method ?
I thought about AppLifeCycleEvent but all state seem to be the wrong place, because I only want to close the client when the app really shuts down (i.e. if the user re-opens/resumes the app a new client must be created in order to recreate the subscription).
how do you know it is still open when app shutdown. i think platform(ios/android) close it.

Is there a way to get notified when a dart stream gets its first result?

I currently have an async function that does the following:
Initializes the stream
Call stream.listen() and provide a function to listen to the stream.
await for the stream to get its first result.
The following is some pseudo code of my function:
Future<void> initStream() async {
// initialize stream
var stream = getStream();
// listen
stream.listen((result) {
// do some stuff here
});
// await until first result
await stream.first; // gives warning
}
Unfortunately it seems that calling stream.first counts as listening to the stream, and streams are not allowed to be listened by multiple...listeners?
I tried a different approach by using await Future.doWhile()
Something like the following:
bool gotFirstResult = false;
Future<void> initStream() async {
var stream = getStream();
stream.listen((result) {
// do some stuff here
gotFirstResult = true;
});
await Future.doWhile(() => !gotFirstResult);
}
This didn't work for me, and I still don't know why. Future.doWhile() was successfully called, but then the function provided to stream.listen() was never called in this case.
Is there a way to wait for the first result of a stream?
(I'm sorry if I didn't describe my question well enough. I'll definitely add other details if needed.)
Thanks in advance!
One way is converting your stream to broadcast one:
var stream = getStream().asBroadcastStream();
stream.listen((result) {
// do some stuff here
});
await stream.first;
Another way, without creating new stream, is to use Completer. It allows you to return a Future which you can complete (send value) later. Caller will be able to await this Future as usual.
Simple example:
Future<int> getValueAsync() {
var completer = Completer<int>();
Future.delayed(Duration(seconds: 1))
.then((_) {
completer.complete(42);
});
return completer.future;
}
is equivalent of
Future<int> getValueAsync() async {
await Future.delayed(Duration(seconds: 1));
return 42;
}
In your case:
Future<void> initStream() {
var stream = getStream();
var firstValueReceived = Completer<void>();
stream.listen((val) {
if (!firstValueReceived.isCompleted) {
firstValueReceived.complete();
}
// do some stuff here
});
return firstValueReceived.future;
}