waiting for async funtion to finish - asynchronous

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();
}
));

Related

Flutter code loops infinitely even after data is loaded from firebase

I am using the following Flutter and Firebase code to scaffold out a page to a user
import 'package:fgd6ss/models/user.dart';
import 'package:fgd6ss/screens/user/usr_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:convert';
class UsertLanding extends StatefulWidget {
final Map code;
UserLanding({this.code});
User _user = User();
bool dataLoaded = false;
#override
_UserLandingState createState() => _UserLandingState();
}
class _UserLandingState extends State<UserLanding> {
#override
Widget build(BuildContext context) {
bool isValidUser = false;
dynamic userData;
Map codeData = widget.code;
try{
var document = FirebaseFirestore.instance.collection('users').where('id',isEqualTo: codeData['id']);
document.get().then((QuerySnapshot snapshot){
if (snapshot.docs.isNotEmpty) {
if (this.mounted) {
setState(() {
userData = snapshot.docs;
widget._user.name = userData[0]['name'];
widget._user.status = userData[0]['status'];
widget._user.type = userData[0]['type'];
print(widget._user.name);
});
}
}
});
}catch(e) {
print('error firebase data fetch');
}
return Scaffold(
backgroundColor: Color(0xfffefefe),
body: SafeArea(
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.all(20.0),
child: Text(
widget._user.name,
style: TextStyle(
fontSize: 22.0
),
),
)
]
)
)
);
}
}
As you can see from the above code, I have a print statement inside the query in try. When I run this code, I expect it to run once when the screen loads. But what happens is, the code keeps looping and prints out the users name again and again on the console. Is this expected? If not, what is causing this behaviour? If yes, will it cause increase in the document read quota count on Firebase end.
You have to create a separate method and call this method into your initState(). Build function is run continuously so your print statement is printed in the loop. So try with the below code. initState() method run only once when you reach on to the page
class UsertLanding extends StatefulWidget {
final Map code;
UserLanding({this.code});
User _user = User();
bool dataLoaded = false;
#override
_UserLandingState createState() => _UserLandingState();
}
class _UserLandingState extends State<UserLanding> {
bool isValidUser = false;
dynamic userData;
Map codeData;
#override
initState() {
super.initState();
codeData = widget.code;
getData();
}
getData() {
try {
var document = FirebaseFirestore.instance
.collection('users')
.where('id', isEqualTo: codeData['id']);
document.get().then((QuerySnapshot snapshot) {
if (snapshot.docs.isNotEmpty) {
if (this.mounted) {
setState(() {
userData = snapshot.docs;
widget._user.name = userData[0]['name'];
widget._user.status = userData[0]['status'];
widget._user.type = userData[0]['type'];
print(widget._user.name);
});
}
}
});
} catch (e) {
print('error firebase data fetch');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xfffefefe),
body: SafeArea(
child: Row(children: <Widget>[
Container(
padding: EdgeInsets.all(20.0),
child: Text(
widget._user.name,
style: TextStyle(fontSize: 22.0),
),
)
])));
}
}
You are calling your function in the build method. So whenever the build method will be rebuilt or refreshed, it will call that function again and again. The better approach is to call it in your InitState so it will be called only once. Here is an example that might help you.
#override
initState() {
getData();
super.initState();
}

How to refresh StreamBuilder without Navigation and without Firebase - Flutter

Currently my code works, but every 5 seconds I must make a request to the API, in order to refresh the view and see if any changes have occurred in the data.
My purpose is that it detect if there was any change in the data so that the view is updated, I am not using firebase and I am not trying to navigate to another screen either.
class _ListChatsState extends State<ListChats> {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
final messagesProvider = new ServiceMessagesAdm();
Timer _timer;
#override
void initState() {
//messagesProvider.getMessagesAdm();
//Check the server every 5 seconds
_timer = Timer.periodic(Duration(seconds: 5), (timer) => messagesProvider.getMessagesAdm());
super.initState();
}
#override
void dispose() {
//cancel the timer
if (_timer.isActive) _timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
key: scaffoldKey,
appBar: new AppBar(
title: new Text('StreamBuilder'),
actions: <Widget>[
],
),
body: FutureBuilder(
future: messagesProvider.getMessagesAdm(),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print("ESTOY EN NONE");
return image();
break;
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
break;
case ConnectionState.active:
return _buildListView(snapshot.data);
break;
case ConnectionState.done:
// TODO: Handle this case.
break;
}
return image();
},
),
);
}
Future<List<MessagesAdm>> getMessagesAdm() async {
final respuesta = await http.get(url);
List<MessagesAdm> _list;
var resBody = json.decode(respuesta.body);
print(respuesta.body);
var capsules = resBody as List;
_list = capsules.map((model) => MessagesAdm.fromJson(model)).toList();
//print(_list);
popularesSink( _list );
return _list;
}
Is there a way to detect if there was any change within the API? I am chatting.
Before I was using these lines of code .. But they did not detect changes, for me it is important to tell you that the changes were being applied from postman. In the code, could I issue any notification that triggers those streams? I leave lines of the previous code used.
class ServiceMessagesAdm {
Loads loads;
final _popularesStreamController = StreamController<List<MessagesAdm>>.broadcast();
Function(List<MessagesAdm>) get popularesSink => _popularesStreamController.sink.add;
Stream<List<MessagesAdm>> get popularesStream => _popularesStreamController.stream;
void disposeStreams() {
_popularesStreamController?.close();
}
}

How can I have this Flutter Bloc to send updates?

I have this Flutter bloc that takes a Firebase stream of restaurants and depending on the position relative to the user will filter only the closest ones depending on the restaurant location. It works fine but I have to refresh with a RefreshIndicator if I want to see any changes in restaurant documents. What am I missing? Thanks in advance.
class NearestRestaurant {
final String id;
final Restaurant restaurant;
final double distance;
NearestRestaurant({this.id, this.restaurant, this.distance});
}
class NearRestaurantBloc {
final Future<List<Restaurant>> source;
final Position userCoordinates;
final _stream = StreamController<List<Restaurant>>();
NearRestaurantBloc({
this.source,
this.userCoordinates,
}) {
List<Restaurant> resList = List<Restaurant>();
source.then((rest) {
rest.forEach((res) async {
await Geolocator().distanceBetween(
userCoordinates.latitude,
userCoordinates.longitude,
res.coordinates.latitude,
res.coordinates.longitude,
).then((distance) {
if (res.active && distance < res.deliveryRadius) {
resList.add(res);
}
});
_stream.add(resList);
});
});
}
Stream<List<Restaurant>> get stream => _stream.stream;
void dispose() {
_stream.close();
}
}
class RestaurantQuery extends StatefulWidget {
#override
_RestaurantQueryState createState() => _RestaurantQueryState();
}
class _RestaurantQueryState extends State<RestaurantQuery> {
NearRestaurantBloc bloc;
#override
Widget build(BuildContext context) {
final database = Provider.of<Database>(context, listen: true);
final session = Provider.of<Session>(context);
final userCoordinates = session.position;
bloc = NearRestaurantBloc(
source: database.patronRestaurants(),
userCoordinates: userCoordinates,
);
return StreamBuilder<List<Restaurant>>(
stream: bloc.stream,
builder: (context, snapshot) {
bool stillLoading = true;
var restaurantList = List<Restaurant>();
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData && snapshot.data.length > 0) {
restaurantList = snapshot.data;
}
stillLoading = false;
}
return Scaffold(
appBar: AppBar(
title: Text(
'Restaurants near you',
style: TextStyle(color: Theme.of(context).appBarTheme.color),
),
elevation: 2.0,
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: RefreshIndicator(
onRefresh: () async {
setState(() {
});
},
child: RestaurantList(
nearbyRestaurantsList: restaurantList,
stillLoading: stillLoading,
),
),
);
},
);
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
}
In the build method under _RestaurantQueryState, you are returning the scaffold outside the builder method. Initially, restaurantList is null. Therefore, you don't produce the list. Whenever the stream updates, you get the snapshot data to update the restaurantList.
The problem occurs here. Even though the restaurantList is updated, the widget RestaurantList is not updated because it is outside the builder method. You can use the following code. Here we create a Widget that holds the RestaurantList widget. The widget gets updated whenever the stream updates.
class _RestaurantQueryState extends State<RestaurantQuery> {
NearRestaurantBloc bloc;
#override
Widget build(BuildContext context) {
final database = Provider.of<Database>(context, listen: true);
final session = Provider.of<Session>(context);
final userCoordinates = session.position;
//////////////////////////////////
//initialize RestaurantList widget
//////////////////////////////////
Widget restaurantWidget = RestaurantList(
nearbyRestaurantsList: [],
stillLoading: false,
);
bloc = NearRestaurantBloc(
source: database.patronRestaurants(),
userCoordinates: userCoordinates,
);
return StreamBuilder<List<Restaurant>>(
stream: bloc.stream,
builder: (context, snapshot) {
bool stillLoading = true;
var restaurantList = List<Restaurant>();
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData && snapshot.data.length > 0) {
restaurantList = snapshot.data;
/////////////////////////////
//update the restaurant widget
//////////////////////////////
restaurantWidget = RestaurantList(
nearbyRestaurantsList: restaurantList,
stillLoading: stillLoading,
);
}
stillLoading = false;
}
return Scaffold(
appBar: AppBar(
title: Text(
'Restaurants near you',
style: TextStyle(color: Theme.of(context).appBarTheme.color),
),
elevation: 2.0,
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
///////////////////////////
//use the restaurant Widget
///////////////////////////
body: restaurantWidget,
),
);
},
);
}
My mistake. I was listening to a future instead of a stream. Here's the updated bloc code:
class NearestRestaurant {
final String id;
final Restaurant restaurant;
final double distance;
NearestRestaurant({this.id, this.restaurant, this.distance});
}
class NearRestaurantBloc {
final Stream<List<Restaurant>> source;
final Position userCoordinates;
final _stream = StreamController<List<Restaurant>>();
NearRestaurantBloc({
this.source,
this.userCoordinates,
}) {
List<Restaurant> resList = List<Restaurant>();
source.forEach((rest) {
resList.clear();
rest.forEach((res) async {
await Geolocator().distanceBetween(
userCoordinates.latitude,
userCoordinates.longitude,
res.coordinates.latitude,
res.coordinates.longitude,
).then((distance) {
if (res.active && distance < res.deliveryRadius) {
resList.add(res);
}
});
_stream.add(resList);
});
});
}
Stream<List<Restaurant>> get stream => _stream.stream;
}

Retrieve realtime data from firebase

I have a flutter project set up to receive moisture sensor data from firebase. I am uploading data to firebase and trying to retrieve it on my app but it only retrieves data after some time. Moreover, the value is null till then. And also, when changed there is no change in data on the app. How do I get the data so that it shows data from the moment I start the app and changes when data on the firebase updates? Here is my code.
double moistureData;
#override
bool _isLoading = false;
void initState() {
super.initState();
databaseReference.child('Data').once().then((DataSnapshot snapshot) {
int moisture = snapshot.value['Moisture'];
moistureData = moisture.toDouble();
print(moisture);
setState(() {
if (moistureData != null) {
_isLoading = true;
}
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Moisture'),
),
body: StreamBuilder(
stream: databaseReference.child('Data').child('Moisture').onValue,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
} else {
return Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text('$moistureData'),
RaisedButton(
child: Text('Refresh'),
onPressed: () {
setState(() {
databaseReference
.child('Data')
.once()
.then((DataSnapshot snapshot) {
int moisture = snapshot.value['Moisture'];
moistureData = moisture.toDouble();
});
});
},
)
],
),
),
);
}
}));
of course, it takes some time to load data from firebase, create one bool variable and set it false.
double moistureData;
bool _isLoading=false;
#override
void initState() {
super.initState();
databaseReference.child('Data').once().then((DataSnapshot snapshot) {
final int moisture = snapshot.value['Moisture'];
moistureData=moisture.toDouble();
print(moisture);
setState((){
if(moistureData!=null)
_isLoading=true;
});
});
}
And inside your widget pass, circular progress indicator or an empty container when _isLoading is false. when it's true called your widget inside else.
To listen for both the current value and updates, you need to observe one of the on... stream properties of the query object. The closest to your current code is onValue stream.
An example from the FlutterFire example app:
_counterRef.onValue.listen((Event event) {
setState(() {
_error = null;
_counter = event.snapshot.value ?? 0;
});
}
So you see that this also uses the setState(...) that #satish's answer explains.

How to display a loading spinner while getting the current location in Flutter?

I'm working on a Flutter app which fetches the user's current location, but while the location is being fetched, it shows this ugly red error screen (which disappears once the location is fetched).
Instead of this, I'd like to display a loading spinner or splash screen. I've narrowed the problem down to this method that is called during initState():
void _setCurrentLocation() {
Geolocator().getCurrentPosition().then((currLoc) {
setState(() {
currentLocation = currLoc;
});
});
}
The entire source file can also be found here on GitHub.
Thanks in advance!
Use FutureBuilder Widget
call your _setCurrentLocation method inside initState method and assign it to one variable like getLoc.
Future<Position> getLoc;
#override
void initState() {
// TODO: implement initState
getLoc = _setCurrentLocation();
super.initState();
}
Change your method with return statement.
Future<Position> _setCurrentLocation() async {
var Location = await Geolocator().getCurrentPosition();
return Location;
}
Put all your design code inside futurebuilder widget
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getLoc,
builder: (context, data) {
if (data.hasData) {
return Text(data.data.toString());
} else {
return Center(child: CircularProgressIndicator());
}
});
}
The simplest way is to use conditional rendering. currentLocation will be null until _setCurrentLocation set it to a value.
class LocationDisplayPage extends StatefulWidget {
#override
_LocationDisplayPageState createState() => _LocationDisplayPageState();
}
class _LocationDisplayPageState extends State<LocationDisplayPage> {
Position? _currentLocation;
void _setCurrentLocation() {
Geolocator().getCurrentPosition().then((currLoc) {
setState(() {
_currentLocation = currLoc;
});
});
}
#override
void initState() {
super.initState();
_setCurrentLocation();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _currentLocation == null
? CircularProgressIndicator()
: WidgetToRenderLocation(),
),
);
}
}
usa il widget Visibility()
bool _isLoading = false;
void _setCurrentLocation() {
_isLoading = true;
Geolocator().getCurrentPosition().then((currLoc) {
setState(() {
_isLoading = false;
currentLocation = currLoc;
});
});
}
return Scaffold(
key: scaffoldKey,
body: Container(
child: Visibility(
visible: _isLoading,
child: Stack(
children: <Widget>[
GoogleMap( ...
replacement: Container(
child: CircularProgressIndicator(),
),

Resources