Window is Full error in Flutter - firebase

When loading data from Firestore into a listview, i receive this warning
W/CursorWindow(15035): Window is full: requested allocation 483 bytes, free space 274 bytes, window size 2097152 bytes
I am suing scoped model pattern and getting user profile data in the model class. I saved all the user data into an array in the model class instead of using an streambuilder in the widget tree itself so that it is easier to page through the data, and it's frankly easier to read. However, going through the list, i receive a Window is Full warning, i understand that too much space is being allocated for this operation of storing the user profiles, however is there an alternative approach i can take to this problem?
class _ExploreScreenState extends State<ExploreScreen>
with SingleTickerProviderStateMixin {
Query _query;
AnimationController controller;
Animation<double> animation;
int limitNum = 4;
bool startAfter = false;
User _lastUser;
#override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: new ScopedModel<ExploreModel>(
model: widget.model,
child: new ScopedModelDescendant<ExploreModel> (
builder: (context, child, model) =>
model.users.length != 0 ? GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
mainAxisSpacing: 5.0,
crossAxisSpacing: 5.0
),
itemCount: model.users.length,
itemBuilder: (BuildContext context, int index) {
if(index == model.users.length-1){
model.loadUsers();
}
return BrowseTile(model.users[index]);
}
) : Text('loading...'),
)
),
),
],
)
);
}
class ExploreModel extends Model {
List<User> _users;
Query _query;
User _currentUser;
int limitNum = 20;
List<User> get users => _users;
ExploreModel(this._currentUser) {
_users = new List();
loadUsers();
}
void loadUsers() {
_query = Firestore.instance.collection('Users').where(
'gender', isEqualTo: _currentUser.prefGender)
.limit(limitNum)
.orderBy('firstName')
.orderBy('lastName');
if (_users.length > 0) _query = _query.startAfter(
[_users[_users.length - 1].firstName, _users[_users.length - 1].lastName
]);
print('Array Size: ');
print(_users.length);
_query.snapshots().listen((snapshot) {
snapshot.documents.forEach((ds) {
User user = User.fromMap(ds.data, ds.documentID);
if(user.id != _currentUser.id){
bool _added = false;
_users.forEach((userEach) {
for(var i=0; i<_users.length; i++){
if(_users[i].id == user.id){
_users.remove(_users[i]);
_users.insert(i, user);
_added = true;
notifyListeners();
}
}
});
if (!_added) {
_users.add(user);
notifyListeners();
}
}
});
});
}
}

This seems to be a known issue as mentioned in this GitHub issue ticket, but this should have been resolved as of May 2020. I suggest upgrading the Flutter SDK version that you're using and try it again.

Related

Flutter - Lazy Load When Scrolling Up (show previous data)

I'm working on a chat room where the new data will be at the bottom of a ListView and as a user scrolls up, previous chat messages will need to load (until they reach the end or original message).
By default, the user will start at the bottom of the list so they can see the new message(s).
The issue I am having is that when I reach the top of my ListView, I stay there. So when new data comes in I'm always at position 0. I'd like to lazy load or prefetch the data so that I get an infinite scroll feel and will stay at the position I stop scrolling at.
Here's some of the logic, hopefully, it will help. Please let me know if there is any other piece of the logic that would help.
void _scrollListener() {
_firstAutoscrollExecuted = true;
if (_scrollController.hasClients &&
_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_shouldAutoscroll = true;
} else {
_shouldAutoscroll = false;
}
if (_scrollController.position.pixels == 0) {
isAtTopOfTable = true;
} else {
isAtTopOfTable = false;
}
}
void _goToElement(double index) {
_scrollController.animateTo(index,
duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
_buildMessagesListView(
List<Message> messages, int numberOfChatsToDisplay, abcState state) {
final abcBloc = BlocProvider.of<abcBloc>(context);
// I've tried something like this as well to animate back to a specific position when the view refreshes but doesn't do anything
if (isAtTopOfTable) {
_goToElement(25);
}
// set our length
length = numberOfChatsToDisplay;
final isPreviousPageLoadingState =
state is PreviousPageLoadingMessagesState;
final isPreviousPageErrorState = state is PreviousPageErrorMessagesState;
final isFinishedState = state is FinishedMessagesState;
if (isPreviousPageLoadingState || isPreviousPageErrorState) {
length = length + 50;
}
return SafeArea(
bottom: false,
minimum: EdgeInsets.only(
bottom: 20.0,
),
child: Column(
children: [
Expanded(
child: StreamBuilder<Event>(
stream: itemRef
.orderByKey()
.limitToLast(numberOfChatsToDisplay)
.onValue,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.active) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
if (_scrollController.hasClients && _shouldAutoscroll) {
_scrollToBottom();
}
if (!_firstAutoscrollExecuted &&
_scrollController.hasClients) {
_scrollToBottom();
}
});
final Event? event = snapshot.data;
final Map<dynamic, dynamic> collection =
event?.snapshot.value as Map<dynamic, dynamic>;
if (collection != null) {
return NotificationListener<ScrollEndNotification>(
child: Container(
height: MediaQuery.of(context).size.height * 0.7,
child: ListView.builder(
padding: EdgeInsets.all(16.0),
controller: _scrollController,
key: PageStorageKey(50), // Tested this but doesn't work
shrinkWrap: true,
reverse: false,
physics: BouncingScrollPhysics(),
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
savedIndex = index;
if (index >= numberOfChatsToDisplay) {
return const Offstage();
}
return CustomListTile();
},
),
),
onNotification: (notification) {
if (isPreviousPageLoadingState ||
isPreviousPageErrorState ||
isFinishedState) {
return true;
}
if (_scrollController.position.pixels == 0) {
// call my BLoC logic to load in new messages
}
return true;
},
);
}
return SizedBox.shrink();
} else {
return Container();
}
},
),
),
// Custom widget for adding a new message
],
),
);
}

How can I check if value exists in Firebase using Flutter? [duplicate]

I have a collection called company.
All the companies are going to be stored like in my screenshot.
When I add another company, I want to check if the name already exists or not.
How to perform that?
Here, "Nova" and "Tradetech" are two companies.
When I try to add "Nova" with the field name: "nova" again, I want to show a notice: "Company already exists!".
I have solved this issue with the follwoing code, thanks for helping me!
IN THE FOLLOWING CODE I USED TO FIND
1)A DOCUMENT IS EXISTING OR NOT?
2)A KEY IS EXISTING OR NOT?
3)A VALUE IS EXISTING OR NOT?
SIMPLE METHOD
//////////////////////////////////////////////////////////////////////
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
String myText1 = 'temp1';
String myText2 = 'temp2';
String myText3 = 'temp3';
String myText4 = 'temp4';
String myText5 = 'temp5';
String myText6 = 'temp6';
StreamSubscription<DocumentSnapshot> subscription;
final DocumentReference documentReference =
Firestore.instance.document("company/Nova");
class Clean extends StatefulWidget {
#override
_CleanState createState() => _CleanState();
}
class _CleanState extends State<Clean> {
#override
void initState() {
super.initState();
subscription = documentReference.snapshots().listen((datasnapshot) {
//FINDING A SPECIFICDOCUMENT IS EXISTING INSIDE A COLLECTION
if (datasnapshot.exists) {
setState(() {
myText1 = "Document exist";
});
} else if (!datasnapshot.exists) {
setState(() {
myText2 = "Document not exist";
});
}
//FINDING A SPECIFIC KEY IS EXISTING INSIDE A DOCUMENT
if (datasnapshot.data.containsKey("name")) {
setState(() {
myText3 = "key exists";
});
} else if (!datasnapshot.data.containsKey("name")) {
setState(() {
myText4 = "key not exists";
});
}
//FINDING A SPECIFIC VALUE IS EXISTING INSIDE A DOCUMENT
if (datasnapshot.data.containsValue("nova")) {
setState(() {
myText5 = "value exists";
});
} else if (!datasnapshot.data.containsValue("nova")) {
setState(() {
myText6 = "value not exists";
});
}
});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
new Text(myText1),
new Text(myText2),
new Text(myText3),
new Text(myText4),
new Text(myText5),
new Text(myText6),
],
);
}
}
MY OLD COMPLEX METHOD BASED ON MY EXISTING CODE
////////////////////////////////////////////////////////
Concept
it has a search bar,when you type it will show the company name ie existing or not in
A Card and a RaisedButton. I am using lower case in Firestore in order to avoid the search error. I have forced the TextFormField output to be lower case with toLowercase(). You can change it to your own text format.
Code
//if the name is not existing it will show a raised button so u can clcik on that to
//go to a COMPANY ADDING PAGE,otherwise it will only show a **CARD** so that you
//can't go to the next page to add your company
//code:
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import './fullscreen.dart';
const blue = 0xFF3b78e7;
String filter = '';
StreamSubscription<DocumentSnapshot> subscription;
final TextEditingController _usercontroller = new TextEditingController();
class CheckAvail extends StatefulWidget {
#override
HomeState createState() => HomeState();
}
class HomeState extends State<CheckAvail> {
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
// CHILD1
new Flexible(
child: StreamBuilder(
stream: Firestore.instance
.collection('company')
.where('name', isGreaterThanOrEqualTo: filter.toLowerCase())
.limit(1)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return new Column(
children: <Widget>[
new Card(
elevation: 5.0,
child: new Image.asset('assets/progress.gif'),
)
],
);
} else {
return FirestoreListView1(documents: snapshot.data.documents);
}
},
),
),
new Card(
elevation: 0.0,
color: Colors.white,
shape: new RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60.0)),
child: Container(
padding: new EdgeInsets.only(left: 8.0),
child: new TextField(
controller: _usercontroller,
onChanged: (String z) {
setState(() {
filter = z;
});
},
decoration: const InputDecoration(
hintText: "Search...",
hintStyle: TextStyle(
fontFamily: 'roboto',
color: Colors.black38,
fontSize: 16.0,
letterSpacing: -0.500),
fillColor: Colors.white,
border: InputBorder.none,
),
),
),
),
],
),
backgroundColor: Color(blue),
);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class FirestoreListView1 extends StatelessWidget {
final List<DocumentSnapshot> documents;
FirestoreListView1({this.documents});
#override
Widget build(BuildContext context1) {
return ListView.builder(
itemCount: documents.length,
padding: new EdgeInsets.all(1.0),
itemBuilder: (BuildContext context1, int index) {
String name = documents[index].data['name'];
if (name.contains(filter.toLowerCase()) &&
name.length == filter.length) {
return new Container(
padding: new EdgeInsets.only(top: 45.0),
child: new Card(
child: new Text(
"Error:Already a Company Exists with this name\nTry another name")),
);
} else {
return (filter.length >= 1)
? new Container(
padding: new EdgeInsets.only(top: 15.0),
child: new RaisedButton(
onPressed: () => Navigator.push(
context1,
new MaterialPageRoute(
builder: (context1) => new NextPage(
value1: name,
))),
disabledColor: Colors.white,
child: new Text(
"Good!You can use this company name",
),
),
)
: new Container(padding: new EdgeInsets.only(top: 250.0),
child: new Card(child: new Text("CHECK IF YOUR COMPANY NAME \n AVAILABLE OR NOT",style: new TextStyle(fontSize: 20.0),)),
);
}
});
}
}
You can simply use a where query to only receive documents that have that name and then check whether you get documents. Here is an async example method that would perform what you want to know.
Example method
Future<bool> doesNameAlreadyExist(String name) async {
final QuerySnapshot result = await Firestore.instance
.collection('company')
.where('name', isEqualTo: name)
.limit(1)
.getDocuments();
final List<DocumentSnapshot> documents = result.documents;
return documents.length == 1;
}
As you can see, I am only receiving documents, where the name field matches the given name. I also add limit(1) to make sure that I do not unnecessarily retrieve more than 1 document (which would never happen in theory) and then I just check if the length of all documents in the company collection is equal to 1 or not. If it is equal to 1, there already is a company that has that name and otherwise not.
You could also remove the limit(1) and make the check documents.length > 1 and that would work too, but might retrieve unnecessary documents.
Example implementation
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doesNameAlreadyExist('nova'),
builder: (context, AsyncSnapshot<bool> result) {
if (!result.hasData)
return Container(); // future still needs to be finished (loading)
if (result.data) // result.data is the returned bool from doesNameAlreadyExists
return Text('A company called "Nova" already exists.');
else
return Text('No company called "Nova" exists yet.');
},
);
}
Here, I am not displaying an error message, which would be easily possible with the example method as well. However, the build method of some widget is used. This would e.g. work in a dialog, but I decided to do it to keep it simple and understandable. The FutureBuilder takes in doesNameAlreadyExist, in this case with the name "Nova" from your question and will, in the end, return a Text widget stating whether the name already exists.
Be careful
The where query is case-sensitive. This means that the check would not work if you typed e.g. "noVa" instead of "nova". As this might be important to you, you can make use of this nice method, where you would create an extra field that is insensitive, e.g. all letters are small and then you would simple query like this:
.where('name_insensitive', isEqualTo: name.toLowerCase())
final QuerySnapshot result =
await Firestore.instance.collection('users').where('nickname', isEqualTo:
nickname).getDocuments();
final List < DocumentSnapshot > documents = result.documents;
if (documents.length > 0) {
//exists
} else {
//not exists
}
Use the function:
snapshot.data!.data()!.containsKey('key_name')
to check if a field exists in your document.
PS. I just used this in my code RN and it works
I know I am late.
Posting for future users.
Try this:
DocumentReference datab = db.collection("Company").document("Nova");
datab.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
if(documentSnapshot.contains("name"))
{
Toast.makeText(YourActivity.this, "Child exixts.", Toast.LENGTH_SHORT).show();
}
else
Toast.makeText(YourActivity.this, "Doesnt exits.", Toast.LENGTH_SHORT).show();
}
});

Flutter App Performance Issues with Long List

I have a flutter app where user can add items to a list which are stored in firebase. User can add up to 1000 items at once. Initially this is no issue but with a growing number of list items the app gets slower and slower until when adding multiple items at once after roughly 1000 items are in the list it crashes the app due to the memory use -
thread #10, name = 'io.flutter.1.ui', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)
How can I improve the code so the performance improves. I would like to keep the setup with the Stream since it lets me dynamically filter the list on the fly. One information here as well is that WidgetA and WidgetB also both use the Stream Data to display the number of list items in the list.
Here is my code a bit simplified for ease of reading:
Main Screen Class:
Widget content(context) {
double h = MediaQuery.of(context).size.height; //screen height
double w = MediaQuery.of(context).size.width; //screen width
return StreamProvider<List<Activity>>.value(
catchError: (_, __) => null,
value: DatabaseService().activities(widget.uid),
builder: (context, snapshot) {
return SafeArea(
child: Container(
//color: Theme.of(context).backgroundColor, //SkyHookTheme.background,
child: Scaffold(
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(children: [
ListView(
controller: _scrollController,
children: <Widget>[
Column(
children: <Widget>[
WidgetA(),
WidgetB(),
ActivityList(), //List of User Activities
],
)
],
),
]),
),
),
),
);
});
}
ActivityList Class Listview Building:
ListView buildList(List<Activity> acts){
items = ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: len,
itemBuilder: (context, index) {
return ActivityTile(activity: acts[index], number: acts.length - (index));
},
);
return items;
}
Any Tips / Hints how I can improve this would be highly appreciated.
Thanks!
You have to pagination to achieve smooth perform
And just load 10 documents in one time and with
Help of scrollcontroller check you are end of the list
And then load next 10 documents that’s would be
Efficient manner .
Instead of "listview" use sliversList widget.
See the Example of sliversList and sliverscomponents here
I think #AmitSingh's suggestion is best but if you want to load data in once then you can get data in pagination but not when the user scrolls but when you got the first bunch of data.
yeah you should use pagination or lazy-loading! reading and rendering 1000 document at once is too much work for most mobile devices.
instead you should load you documents likes this
import 'package:cloud_firestore/cloud_firestore.dart';
Firestore firestore = Firestore.instance
class LongList extends StatefulWidget {
#override
_LongListState createState() => _LongListState();
}
class _LongListState extends State<LongList> {
List<DocumentSnapshot> products = []; // stores fetched products
bool isLoading = false; // track if products fetching
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _scrollController = ScrollController(); // listener for listview scrolling
getProducts() async {
if (!hasMore) {
print('No More Products');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
print(1);
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
products.addAll(querySnapshot.documents);
setState(() {
isLoading = false;
});
}
void initState(){
getProducts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.20;
if (maxScroll - currentScroll <= delta) {
getProducts();
}
});
_pageManager = PageManager();
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Pagination with Firestore'),
),
body: Column(children: [
Expanded(
child: products.length == 0
? Center(
child: Text('No Data...'),
)
: ListView.builder(
controller: _scrollController,
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5),
title: Text(products[index]['name']),
subtitle: Text(products[index] ['short_desc']),
);
},
),
),
isLoading
? Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(5),
color: Colors.yellowAccent,
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: Container()
]),
);
}
}

Can't get actual String download url from Firebase Storage and only returns Instance of 'Future<String>' even using async/await

I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?
this is the function that calls getAvatarDownloadUrl with current passed firebase user instance.
myViewModel
FutureOr<String> getAvatarUrl(User user) async {
var snapshot = await _ref
.read(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
if (snapshot != null) {
print("avatar url: $snapshot");
}
return snapshot;
}
getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.
Future<String> getAvatarDownloadUrl(String code) async {
Reference _ref =
storage.ref().child("users").child(code).child("asset.jpeg");
try {
String url = await _ref.getDownloadURL();
return url;
} on FirebaseException catch (e) {
print(e.code);
return null;
}
}
I am calling these function from HookWidget called ShowAvatar.
To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem.
However, once I want to get downloardURL from list of users (inside of ListView using index),
class ShowAvatar extends HookWidget {
// some constructors...
#override
Widget build(BuildContext context) {
// get firebase user instance
final user = useProvider(accountProvider.state).user;
// get user avatar data as Future<String>
final userLogo = useProvider(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
// get actual user data as String
final snapshot = useFuture(userLogo);
// to access above functions inside of ListView
final viewModel = useProvider(myViewModel);
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
),
),
SizedBox(height: 32),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Center(
child: Column(
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: viewModel
.getAvatarUrl(goldWinners[index].user)
.toString(), // ** this avatar data is not String but Instance of Future<String>
),
),
),
],
),
);
},
itemCount: goldWinners.length,
),
Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage.
However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.
I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.
class Avatar extends StatelessWidget {
final String avatarUrl;
final double radius;
final BoxFit fit;
Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
: super(key: key);
#override
Widget build(BuildContext context) {
print('this is avatar url : ' + avatarUrl.toString());
return avatarUrl == null
? ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.asset(
"assets/images/avatar_placeholder.png",
fit: fit,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: CachedNetworkImage(
imageUrl: avatarUrl.toString(),
placeholder: (_, url) => Skeleton(radius: radius),
errorWidget: (_, url, error) => Icon(Icons.error),
fit: fit,
));
}
}
Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:
child: FutureBuilder<String>(
future: viewModel.getAvatarUrl(goldWinners[index].user),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return snapshot.hashData
? Avatar(avatarUrl: snapshot.data)
: Text("Loading URL...")
}
)
Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,
new FutureBuilder(
future: //future you need to pass,
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, i) {
DocumentSnapshot ds = snapshot.data.docs[i];
return //the data you need to return using /*ds.data()['field value of doc']*/
});
} else if (snapshot.hasError) {
// Handle the error and stop rendering
GToast(
message:
'Error while fetching data : ${snapshot.error}',
type: true)
.toast();
return new Center(
child: new CircularProgressIndicator(),
);
} else {
// Wait for the data to fecth
return new Center(
child: new CircularProgressIndicator(),
);
}
}),
Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.
else if (snapshot.hasError) {
}
else {
}
above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.

waiting for async funtion to finish

I want to read some txts and store their text in an array. But because I need this array for my GUI it should wait until all is done.
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path);
}
int topicNr = 3;
int finished = 0;
for (int topic = 1; topic <= topicNr; topic++) {
getFileData('assets/topic' + topic.toString() + '.txt').then(
(text) {
topics.add(text);
},
).whenComplete(() {
finished++;
});
}
while (finished < topicNr)
But when I run this code, finished won't update (I think because it is because the while loop runs on the main thread and so the async funtion can't run at the same time)
I could do this by just waiting, but this isn't really a good solution:
Future.delayed(const Duration(milliseconds: 10), () {
runApp(MaterialApp(
title: 'Navigation Basics',
home: MainMenu(),
));
});
How can I now just wait until all of those async Funtions have finished?
(sorry, I am new to Flutter)
One thing you could do is use a stateful widget and a loading modal. When the page is initialized, you set the view to be the loading modal and then call the function that gets the data and populate the data using set state. When you are done/when you are sure the final data has been loaded then you set the loading to false. See the example below:
class Page extends StatefulWidget {
page();
#override
State<StatefulWidget> createState() => new _Page();
}
class _Page extends State<Page>{
bool _loading = true; //used to show if the page is loading or not
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
_loading = false;
});
})
}
//You could also use this widget if you want the loading modal ontop your page.
Widget IsloadingWidget() {
if (_loading) {
return Stack(
children: [
new Opacity(
opacity: 0.3,
child: const ModalBarrier(
dismissible: false,
color: Colors.grey,
),
),
new Center(
child: new CircularProgressIndicator(
valueColor:
new AlwaysStoppedAnimation<Color>(Colors.green),
strokeWidth: 4.0,
),
),
],
);
} else {
return Container();
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
//If loading, return a loading widget, else return the page.
_loading ?
Container(
child: Center(
child: CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.blue))))
:Column(
children:<Widget>[
//Rest of your page.
]
)
]))
}
}
You could also set the fields of the initial data to empty values and the use set state to give them their actual values when you get the data.
so for example
string myvalue = " ";
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
//then
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
myValue = onValue.data['val'];
_loading = false;
});
})
}
Let me know if this helps.
Use the FutureBuilder to wait for the API call to complete before building the widget.
See this example: https://flutter.dev/docs/cookbook/networking/fetch-data
runApp(MaterialApp(
title: 'Navigation Basics',
home: FutureBuilder(
future: getFileData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MainMenu()
} else {
return CircularProgressIndicator();
}
));

Resources