how can I call async function after BlocBuilder state is success? - flutter

I have tasks list from flutter_downloader but that list is not enough to show in list. I need to show other information in list view as well as download information.
I already got the download tasks in initial state but I need to wait to get another list from bloc. after DownloadedSongListLoaded, I want to call _combineList(favouriteSongs); But I only want to return the Container after _combineList(favouriteSongs) finished.
So, how can I call async function in widget in BlocBuilder or other way around.
child: BlocBuilder<FavouriteSongBloc, FavouriteSongState>(
builder: (context, state) {
if (state is FavouriteSongError) {
return SomethingWentWrongScreen(error: state.error);
} else if (state is DownloadedSongListLoaded) {
favouriteSongs = state.favouriteSongs;
await _combineList(favouriteSongs); <== here, I want to manipulate the favouriteSongs list before binding the below Container widget.
return const Container() //ListView builder will be here
}
return const CircularProgressIndicatorWidget();
},
)

Related

FLUTTER : Displaying a loading indicator while listview.builder is building

When clicking on an OPEN LIST button, the app creates a series of lists needed for future display. I found out that when the number of items in the list increases, these calculations can take a little time (2, 3 seconds). So for better UX, I would like to add something similar to a loading indicator telling the user the "lists are being prepared".
In my app, I use the package Loading Indicator : it works fine.
So I wanted to use it for this situation.
Here's what I did :
I transformed my "void" list creating functions into "Future Void".
I added the async keyword to the function plugged to my "OPEN LIST BUTTON".
But... for some reason, it never displays the loading indicator....
Here's the code (UI part) :
onMenuOuvrir: () async {
DialogBuilder(context).showLoadingIndicator(
text: 'Ouverture de la liste', color: Colors.black45);
uD.setSelectedCarnetList(
index, uD.userInfo!.carnetList![index].ref!);
await uD.getListReady();
DialogBuilder(context).hideOpenDialog();
Navigator.pushReplacementNamed(
context, EditCarnetScreen.id);
},
Here's the code (Provider / back end part) :
Future<void> getListReady() async {
await createBufferCarnetWordBank();
await createBufferCarnetList();
await createBufferGrammarList();
await createBufferLevelList();
setEditMode(false);
clearSearchList();
}
The functions "createBuffer...List" are all of type Future .
What am I doing wrong ?
I actually found out that the problem was elsewhere... not in the creation of the lists, but in the building of the "listview.builder" in the EditCarnetScreen.
So here's the question now... how can we display some kind of indicator while this task is being processed.... it seems to be "in between screens"...
Create a widget in a separate dart file :
import 'package:flutter/material.dart';
Loading(BuildContext context) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
);
});
}
Then use it anywhere in your code:
Loading(context)
Dismiss in whenever you want:
Navigator.of(context).pop()
You can use FutureBuilder widget for that, e.g.
FutureBuilder(future: Provider.of<//your provider>(context)getListReady(), builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting ? Center(child: CircularProgressIndicator()) : //your widget)
If you process the calculation in the main thread, even if the indicator shows up the UI freezes and the spinning animation would be laggy.
You may process the heavy lifting tasks by Isolates. Search for Isolate or compute method of dart.
I have used the fluttertoast: ^8.0.8 package as shown below. You can set the toast length to a longer duration if needed. FutureBuilder is not useful in the case, where your widget themselves take a long time render. FutureBuilder is useful while fetching the data required for your widgets.
#override
Widget build(BuildContext context) {
Fluttertoast.showToast(
msg: 'Loading...'
);
//Code for your complex widget here
}

Function invoke in Flutter to get attributes from Firebase

I want to define and invoke a function in Flutter to get required values from Firebase.
In the below code I have defined getCourseDetails() function and invoking it in the container by passing a parameter to get the value.
The Course_Details collection has many documents with course_id's which has attribute with name (course_name, subtitle). I use these values to build a listview cards in next steps.
I am able to get the values from the function using async await, but for some reason the values keeps on updating and never stops. It kind of goes to loop and keeps on running. I added print statements to check and it keeps on running and printing.
Please let me know what wrong I am doing here or how to define function here to avoid the issue. Thanks
class _CourseProgressListState extends State<CourseProgressList> {
String course_name, subtitle;
getCourseDetails(course_id_pass) async {
DocumentSnapshot document = await Firestore.instance.collection('Course_Details').document(course_id_pass).get();
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
}
#override
Widget build(BuildContext context) {
return Container(
child: StreamBuilder(
stream: Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots(),
builder: (context, snapshot) {
if (snapshot.data == null) return Text('no data');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (_, int index) {
var course_details = snapshot.data.documents[index];
getCourseDetails(course_details['course_id']);
Application Flow
On first build of CourseProgressList the Widget State _CourseProgressListState is loaded. This State in the build method uses a StreamBuilder to retrieve all documents from the firestore. As soon as the documents are received the StreamBuilder attempts to build the ListView using the ListViewBuilder.
In the ListViewBuilder you make the async call to getCourseDetails(course_details['course_id']); which when complete populates two attributes String course_name, subtitle;
The problem starts here
Problem
When you call
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
you trigger a Widget rebuild and so the process starts over again to rebuild the entire widget.
NB. refreshing state of a stateful widget will trigger a widget rebuild
NB. Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots() returns a stream of realtime changes implying that your List will also refresh each time there is a change to this collection
Recommendations
If you do not need to call the setState try not to call the setState.
You could let getCourseDetails(course_id_pass) return a Future/Stream with the values desired and use another FutureBuilder/StreamBuilder in your ListViewBuilder to return each ListViewItem. Your user may appreciate seeing some items instead of waiting for all the course details to be available
Abstract your request to firestore in a repository/provider or another function/class which will do the entire workload, i.e retrieving course ids then subsequently the course details and returning a Future<List<Course>> / Stream<List<Course>> for your main StreamBuilder in this widget (see reference snippet below as a guide and requires testing)
Reference Snippet
Your abstraction could look something like this but this decision is up to you. There are many software design patterns to consider or you could just start by getting it working.
//let's say we had a class Course
class Course {
String courseId;
String courseName;
String subTitle;
Course({this.courseId,this.courseName,this.subTitle})
}
Stream<List<Course>> getStudentCourses(int studentId){
return Firestore.instance
.collection('Enrolling_Courses')
.where('student_id', isEqualTo: studentId)
.snapshots()
//extract documents from snapshot
.map(snapshot => snapshot?.data ?? [])
//we will then request details for each document
.map(documents =>
/*because this is an asynchronous request for several
items which we are all interested in at the same time, we can wrap
this in
a Future.wait and retrieve the results of all as a list
*/
Future.wait(
documents.map(document =>
//making a request to firestore for each document
Firestore.instance
.collection('Course_Details')
.document(document['course_id'])
.get()
/* making a final transformation turning
each document into a Course item which we can easily pass to our
ListBuilder/Widgets
*/
.then(courseItem => Course(
courseId:document['course_id'],
courseName:
courseItem.data['course_name'],
subTitle:
courseItem.data['subtitle']
)
)
)
)
);
}
References/Resources
FutureBuilder - https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
StreamBuilder - https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Future.wait - https://api.flutter.dev/flutter/dart-async/Future/wait.html

perform a navigation inside the build method of a widget? - flutter

Fast question: how can I perform a navigation inside the build method of a widget?
I'm developing a Flutter App.
I use Bloc architecture.
I have screen with a create form. When the user presses a button, it calls a REST api. While the call is being executed I display a circular progress. When the progress ends I want the screen to be popped (using navigation).
To display the job status I use a Stream in the bloc and a StreamBuilder in the widget. So I want to do something like this:
return StreamBuilder<Job<T>>(
stream: jobstream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.jobStatus == JobStatus.progress)
// job being executed, display progress
return Center(child: CircularProgressIndicator());
else if (snapshot.data.jobStatus == JobStatus.success)
Navigator.pop(context); // Problem here!
return Center(child: CircularProgressIndicator());
} else {
return DisplayForm();
}
});
I have problems with the line: Navigator.pop(context);
Because navigation is not allowed during a build.
How can I do this navigation?
My currect dirty solution is using the following function, but its ugly:
static void deferredPop(BuildContext context) async{
Future.delayed(
Duration(milliseconds: 1),
() => Navigator.pop(context)
);
}
You can add a callback to be executed after the build method is complete with this line of code:
WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.pop(context));
'Because navigation is not allowed during a build.' that being said you should consider creating a separate function that will receive as an input something that you will take into consideration whether to pop that screen.

How to initialize and load data when moving to a screen in flutter?

I want to populate my lists by making API calls when moving to a screen in flutter. I tried calling the async function in the init state however it takes time to load the data of course, and that leads to a null error.
I tried using a future builder in my view. However, I want to make multiple API calls and I don't think that is possible with one future builder. And I would like to avoid having different future builders for each list.
Is there a neat way to do this?
Also is it advisable to load data and pass it on from the previous screen.. since I would like to avoid the loading time?
current code
FutureBuilder(
future: getAllData,
initialData: [],
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.isEmpty) return Center(child: CircularProgressIndicator(valueColor: new AlwaysStoppedAnimation<Color>(Colors.red),));
else {
return createNewTaskView(context, snapshot.data);
}
}),
init method
#override
void initState() {
this.getAllData = getSocieties();
}

onAuthStateChanged called too many times and force rebuild of top widget

I use flutter with firebase auth, and I try to use streamBuilder with the onAuthStateChanged from Firebase Auth.
If user is signedIn I return the homeScreen and in others cases the signInScreen.
Everything is working great, but when I add a print('') in the streambuilder (just before the if for example) I can see that my streamBuilder is called too many times, for example when I push a new screen, when I pop a screen ...etc.. even if there is no data (just an empty Container()) in these screens. At least onAuthStateChanged is called twice by any of my action (push, pop...) with connection.waiting state and connection.active.
Because of that my homeScreen is rebuilt each time, that mean my whole application is rebuilt each time.
The decision code :
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: userBloc.currentUser.onStateChange(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return HomeScreen();
} else {
return SignInScreen();
}
},
),
);
}
My main goal is to force streamBuilder to rebuild widget only if snapshot.hasData change, and not when onAuthStateChanged receive something not useful and doesn't need to rebuild my whole app.