In my app after a user signs in, I am downloading some data from the server and this data can be up to 5000 rows so it takes some time to finish. To better user experience I would like to show the user a LinearProgressIndicator about downloading status.
Things to consider:
This is not one API call, the splash screen triggers an async method and this method calls nearly 10 different API endpoints. Some of them are downloading just 1 row, some of them are downloading 100 rows and others are downloading up to 5000.
In my current implementation, splash screen calls API methods with async, so calls make in sequence. The implementation contains only a CircularProgressIndicator to show the user something is happening on the backend.
I tried to implement Stream because I think I need a Stream to follow which API call started and what is the current situation.
Here is how I try:
//SplashScreen
final viewModel = HomeViewModel();
final stream = viewModel.initializeData();
stream.listen((event) {
setState(() {
downloadingItem = event.itemName;
uploadProgress = event.received / event.total;
});
});
//HomeViewModel
Stream<ReceiveProgress> initializeData() async* {
bigService.initializeData().listen((event) async* {
yield event;
});
await veryBigService.initializeData(onReceiveProgress: (recieved, total) async* {
yield ReceiveProgress("Big Data", recieved, total);
});
await middleSizeService.initializeData(pilotID!);
await anotherBigService.sync();
await authentication.checkSubscription();
}
bigService.initializeData() typically calls three different APIs on sequence, so I think I should apply the same Stream login for that.
Stream<RecieveProgress> initializeData() async* {
bool result = await InternetConnectionChecker().hasConnection;
if (!result) return;
if (typeBox.isEmpty()) {
api.getAllTypes(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Types", count, total);
},
).then((value) {
if (value.isFailure) return;
typeBox.putMany(value.data!, mode: PutMode.insert);
syncService.updateSyncTime<ItemType>();
});
}
if (manufacturerBox.isEmpty()) {
api.getAllManufacturers(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Manufacturers", count, total);
},
).then((value) {
if (value.isFailure) return;
manufacturerBox.putMany(value.data!, mode: PutMode.insert);
syncService.updateSyncTime<Manufacturer>();
});
}
if (box.isEmpty()) {
api.getAll(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Items", count, total);
},
).then((value) {
if (value.isFailure) return;
purgeAndSaveAll(value.data!);
});
}
}
As you can see there is a class named ReceiveProgress. Here is how I defined it.
class ReceiveProgress {
final String itemName;
final int received;
final int total;
RecieveProgress(
this.itemName,
this. received,
this.total,
);
}
At the end, I want to show the results of multiple API call progress in the splash screen but I think I am stuck.
Related
I want to connect my Flutter app to bluetooth device with flutter_blue library, and return a result (anything) when the connection is ON. But I don't understand how to do.
Here my code :
Future connect(BluetoothDevice device) async {
_btDevice = device;
StreamSubscription<BluetoothDeviceState> subscription;
subscription = device.state.listen((event) async {
if (event != BluetoothDeviceState.connected) {
await device.connect();
} else {
await device.discoverServices().then((value) => _initFeatures(value));
}
})
..onDone(() {
// Cascade
print("onDone");
});
subscription.asFuture();
//subscription.cancel();
}
And when I call this function with
await newDevice.connect(bluetoothDevice).then((value) => print('OK'));
OK is written before the real connection. _initFeatures if well call when the device is connected.
I try to use asFuture from StreamSubscription with onDone, but that change nothing.
Could you help me please ?
UPDATE 12/10
I've worked on another project for few monthes, and when I come back, I can't solve the problem, so I add the full code.
The concept is a class widget calls the connect future in other class and need to receipt the end of work.
Widget
Future<void> _connectDevice() async {
try {
widget.device.connect(widget.btDevice!).then((value) => _initValue());
} catch (e) {
print(e);
}
}
_initValue() is a method to create the rest of the screen
and the Future connect()
Future connect(BluetoothDevice device) async {
_btDevice = device;
StreamSubscription<BluetoothDeviceState> subscription;
subscription = device.state.listen((event) async {
if (event != BluetoothDeviceState.connected) {
await device.connect();
} else {
await device
.discoverServices()
.then((value) => _initFeatures(value))
.then((value) => print("OK"));
}
});
await subscription.asFuture();
await subscription.cancel();
}
What I'd like is the Future finishes when print("OK") is called, in order .then((value) => _initValue()); is called.
The problem is only this end. Maybe it's not the good way to implement this kind of solution.
I am using a mongodb database with backend based on NodeJS. I am calling one api to show a list of task (custom class) in front end and calling another API to delete a task. I want to read the list of task again when a task is deleted. How do I do that?
Function to get a list of tasks
Future<List<Task>> listOfTask() async {
List<Task> result;
try {
final response = await http.get('api link');
if (response.statusCode == 200) {
List<Task> taskList = response.body.map<Task>((entry) {
return Task.fromJson(entry);
}).toList();
result = taskList;
}
} catch (e) {
print(e.toString());
}
return result;
}
Getting the list of tasks in initState
#override
void initState() {
DatabaseServices().listOfTask().then((value) {
setState(() {listOfTask = value;});
});
super.initState();
}
Function to delete task
Future<http.Response> taskDelete(task) async {
try {
final response = await http.delete('api link');
return response;
} catch (e) {
print("Error: " + e.toString());
return null;
}
}
Deleting task from frontend
onPressed: () async {
dynamic result = await DatabaseServices(token: token).taskDelete(task);
},
How do I make sure the list of task is updated once I click delete. (Note: the Function to get task list and delete works)
I have a forEachAsync inside an async* Stream and can't yield.
Stream<ProjectState> _mapProjectSelectedEventToState(ProjectSelected event) async* {
try {
yield ProjectLoading(
message: 'Fetching database',
fetchedCount: 0,
totalCount: 1,
);
await forEachAsync(fileModels, (FileEntity fileModel) async {
await downloader.download(filename: fileModel.hashName);
_totalMediaFilesFetched++;
//// ERROR - THIS DOES NOT WORK ////
yield (ProjectLoadingTick(
_totalMediaFiles,
_totalMediaFilesFetched,
));
}, maxTasks: 5);
} catch (error, stacktrace) {
yield ProjectFailure(error: error);
}
}
I've tried other means by dispatching the message and converting it to a state but it doesn't work as well. It seems like the whole app is blocked by this await forEachAsync.
I'm using the bloc pattern which reacts to the emited ProjectStates based on the current ProjectSelected event
Your attempt doesn't work because you're using yield in a callback, not in the function that's returning a Stream. That is, you're attempting the equivalent of:
Stream<ProjectState> _mapProjectSelectedEventToState(ProjectSelected event) async* {
...
await forEachAsync(fileModels, helperFunction);
...
}
Future helperFunction(FileEntity fileModel) async {
...
yield ProjectLoadingTick(...);
}
which doesn't make sense.
Since care about forEachAsync's ability to set a maximum limit to the number of outstanding asynchronous operations,
you might be better off using a StreamController that you can manually add events to:
var controller = StreamController<ProjectState>();
// Note that this is not `await`ed.
forEachAsync(fileModels, (FileEntity fileModel) async {
await downloader.download(filename: fileModel.hashName);
_totalMediaFilesFetched++;
controller.add(ProjectLoadingTick(
_totalMediaFiles,
_totalMediaFilesFetched,
));
},
maxTasks: 5);
yield* controller.stream;
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;
}
I recently learned of the fabulous way of waiting for multiple async functions to complete using Future.wait([asyncFuncOne(), asyncFunctwo()])
However, I noticed two different outcomes when running either of these blocks of code. One awaiting each function to finish, the other using Future.wait for parallel processing. What am I doing wrong?
Method 1:
await msm.initProfileData();
await msm.initActivityFeed();
await msm.getRecentlyActiveUsers();
await msm.getRecommendedUsers();
await msm.getGroups();
await msm.getFollowing();
await msm.getFollowers();
Method 2:
await Future.wait([
msm.getFollowing(),
msm.initProfileData(),
msm.initActivityFeed(),
msm.getRecentlyActiveUsers(),
msm.getRecommendedUsers(),
msm.getGroups(),
msm.getFollowers(),
]);
in Method 1, all the async functions complete before my apps home screen appears. In Method 2 the home screen appears before all the async functions complete.
Cheers and thanks in advance.
EDIT: Additional code example.
#override
void initState() {
super.initState();
googleSignIn.onCurrentUserChanged.listen((account) {
handleSignIn(account);
}, onError: (err) {
print('Error signing in: $err');
});
googleSignIn.signInSilently(suppressErrors: false).then((account) {
handleSignIn(account);
}).catchError((err) {
setState(() => _showSignIn = true);
print('Error signing in: $err');
});
}
handleSignIn(GoogleSignInAccount account) async {
if (account != null) {
await createUserInFirestore();
setState(() {
isAuth = true;
});
} else {
setState(() {
isAuth = false;
_showSignIn = true;
});
}
}
createUserInFirestore() async {
final GoogleSignInAccount user = googleSignIn.currentUser;
DocumentSnapshot doc = await usersRef.document(user.id).get();
//...
//do stuff
//...
await someFunc1(); //Method1
// await comeFunc2(); //Method2
//do more stuff
}
someFunc1() async {
msm.asyncfunc1();
msm.asyncfunc2();
}
someFunc2() async {
await Future.wait([
msm.asyncFunc1(),
msm.asyncFunc2(),
]);
}
#override
Widget build(BuildContext context) {
return isAuth ? buildAuthScreen() : buildUnAuthScreen();
}
Using Future.wait(List<Future>) will wait for all the async operations without sequence as mentioned in the docs. While using await consecutively, it'll wait for the first await async operation to finish before running the next await async operation. If you have a prerequisite output before running the next async operation, it's better to use await async in sequence instead.