In the profile page of my app, I want to save a future list of objects from a firebase collection to a variable (myRecipes), using an async/await function. Depending on the outcome list, I want to display different widgets (using ifHasRecipes()) - if the list turns out to be null or empty, I want to display a text widget, otherwise I want to display the objects in the list using a listview builder (FavoritesHomePage class).
class Profile extends StatefulWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
List<Recipe> myRecipes;
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return myRecipes = await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes() {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
getUserRecipes(userData);
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(),
],
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),),
);
}
});
}
}
How can I make this code synchronized? I want to run the getUserRecipes() and when it's done, return different widgets depending on the outcome.
If I do a hot reload, the code "works" as I want it to, but sometimes when I navigate to this profile page via my pageview widget, the async/await function returning the variable myRecipes isn't done before the ifHasRecipes() is built, and then myRecipes is null (even though it shouldn't be)... Hope this isn't too confusing, sorry.
In this case you can use a FutureBuilder, with this one you will have different states, just like the StreamBuilder, and you can show different widgets depending on the state, until the Future is resolved and you have the data.
I've done a little bit of refactoring to your code to make it work with the FutureBuilder, also I've changed it to Stateless, in this case it will display a CircularProgressIndicator until the Future is resolved, it will also handle errors and the lack of data.
class Profile extends StatelessWidget {
const Profile({Key key}) : super(key: key);
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes(List<Recipe> myRecipes) {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes,
scrollDirection: Axis.vertical,
title: 'Your recipes',
);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
body: SafeArea(
child: FutureBuilder(
future: getUserRecipes(snapshot.data),
builder: (context, futureSnapshot) {
if (futureSnapshot.hasError)
return Text('Error: ${futureSnapshot.error}');
switch (futureSnapshot.connectionState) {
case ConnectionState.none:
return Center(child: CircularProgressIndicator());
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:{
if (futureSnapshot.hasData) {
List<Recipe> myRecipes = futureSnapshot.data;
return Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(myRecipes),
],
);
}
return Text('There\'s no available data.');
}
}
return null;
},
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),
),
);
}
},
);
}
}
If I understand the code correctly, the solution is to rebuild the widget when the future is resolved by adding setState((){}); to the getUserRecipes() method :
Future<void> getUserRecipes(UserData userData) async {
myRecipes = await DatabaseService().findUserRecipes(userData.uid);
setState((){});
}
(You don't have to return the value if you're assigning it to the state, but rather access it directly.)
By the way, you can use the ternary operator (or just regular conditions) to do conditional UI. Put this instead of ifHasRecipes(), :
(myRecipes != null && myRecipes != []) ?
FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',)
: Text('You have no favorites yet')
If you get an error with this, increase your minimum SDK version to 2.6.0 in the pubspec.yaml
Related
I'm trying to find out if there is a specific user in the bank. I intend to get this user's information and give them the option to update their information or not.
For that, I'm bringing his identifier from the previous page and trying to get his data like this:
Future<List>? _list;
.
.
#override
void initState() {
_list = widget.userServices.searchUser(widget.cpf);
super.initState();
}
This returns me a json correctly.
This is my service to get a user's data:
searchUser(cpf) async {
try {
final response = await http.get(
Uri.parse(BaseUrl.baseUrl + 'api/user/buscaProdutorPorCPF/$cpf'));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception("Error");
}
} catch (e) {
throw Exception(e.toString());
}
}
Finally, I'm trying to display the information with this example code:
#override
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _list,
builder: (context, snapshot) {
print(snapshot);
if (snapshot.hasData)
return _buildBody(snapshot.data);
else if (snapshot.hasError)
return _buildErrorPage(snapshot.error);
else
return _buildLoadingPage();
},
);
}
Widget _buildBody(List userList) => Scaffold(
appBar: AppBar(
title: Text('Employee Title = ${userList[0].nome}'),
),
body: ListView.builder(
itemCount: userList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(userList[index].email!),
);
},
),
);
Widget _buildErrorPage(error) => Material(
child: Center(
child: Text("ERROR: $error"),
),
);
Widget _buildLoadingPage() => Material(
child: Center(
child: CircularProgressIndicator(),
),
);
But I'm getting the following error:
I appreciate if anyone can help me analyze this error!
I think modifying the code like this:
return FutureBuilder(
//rest of code
);
and
Widget _buildBody(List userList) => //rest of code
will resolve the problem.
i am new to flutter and trying to display data from a http post
referencing from [1]https://flutter.dev/docs/cookbook/networking/background-parsing and [2]https://flutter.dev/docs/cookbook/networking/fetch-data
i tried to display data on a futurebuilder but it keeps displaying this from the Text('${snapshot.data}')
[Instance of 'DashBoardBanner', Instance of 'DashBoardBanner', Instance of 'DashBoardBanner']
Builder
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<List<DashBoardBanner>> futureBanner;
#override
void initState() {
super.initState();
futureBanner = getBannerDataFromServer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ListView(
children: [
Card(
child: FutureBuilder(
future: getBannerDataFromServer(),
builder: (context,snapshot){
if(snapshot.connectionState == ConnectionState.done){
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
}
return const CircularProgressIndicator();
},
),
)
],
)),
);
}
}
Class and postreq
class DashBoardBanner {
final String MsgId;
final String MsgKey;
final String MsgPic;
const DashBoardBanner(
{required this.MsgId, required this.MsgKey, required this.MsgPic});
factory DashBoardBanner.fromJson(Map<String, dynamic> json) {
return DashBoardBanner(
MsgId: json['MsgId'] as String,
MsgKey: json['MsgKey'] as String,
MsgPic: json['MsgPic'] as String,
);
}
}
Future<List<DashBoardBanner>> getBannerDataFromServer() async {
final queryParameters = {
"ApiFunc": 'Banner',
"UserKey": getDeviceKey(),
"Token": getDeviceToken(),
"SubmitContent": json.encode({"MobileNo": getMobileNo1()})
};
final response = await http.post(
Uri.http('somesite.net', '/capi.aspx', queryParameters),
);
if (response.statusCode == 200) {
Map<String, dynamic> data = jsonDecode(response.body);
final splitoff = jsonEncode(data['RespContent']);
return compute(parseBanner, splitoff);
} else {
throw Exception('Failed to load Data');
}
}
List<DashBoardBanner> parseBanner(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed
.map<DashBoardBanner>((json) => DashBoardBanner.fromJson(json))
.toList();
}
Edit : i rebuilt the file replicating reference[1] and it finally displayed the data i needed, it seems the issue stem from not having this 2nd widget which return the obj back , however how do i combine the 2nd build widget into the first without needing the whole widget as having a whole build widget to return 1 line seems pointless?
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body:Container(
child: FutureBuilder<List<DashBoardBanner>>(
future: getBannerDataFromServer(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
print(snapshot.data!.length);
return DashBoardBannersList(dashboardBanners: snapshot.data!); <--- original issue due to not having this
} else {
return CircularProgressIndicator();
}
},
),
),
);
}
}
class DashBoardBannersList extends StatelessWidget {
const DashBoardBannersList({Key? key, required this.dashboardBanners}) : super(key: key);
final List<DashBoardBanner> dashboardBanners;
#override
Widget build(BuildContext context) {
return Text(dashboardBanners[0].MsgId);
}
}
This error is caused because of the sound null safety
snapshot.data might be null for some requests so you can't access the array at a certain index cause it can be null.
If you know for sure snapshot.data exists you can use the ! operator to tell dart the variable is not null for sure like that:
snapshot.data![index];
You can also check if the data is null before accessing it like that:
if (snapshot.data != null) {
// do something with snapshot.data[index]
}
I recommed to read more about sound null safety here
Check the Firestore docs.
Inside snapshot.data, there's docs (every document of your collection).
The code is from there:
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['full_name']),
subtitle: Text(data['company']),
);
}).toList(),
);
},
);
}
The code above shows how to convert every doc (type DocumentSnapshot) to a JSON format (that can be represented with Map<String, dynamic>). To access to the doc id, you'll access with document.id, because it isn't inside the document.data() method.
You wanna retrieve a list of DashBoardBanner but you forget initialize the futurebuilder by adding a ListView.builder().
Try to use the following code idea :
FutureBuilder(
future: getBannerDataFromServer(http.Client()),
builder: (context, AsyncSnapshot snapshot) {
print(snapshot.hasData);
if (snapshot.hasError) {
return CircularProgressIndicator();
} else if (snapshot.hasData) {
return Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
var data = snapshot.data![index];
return DashBoardBannersList(dashboardBanners: data);
},),
),},
},)
i have working with test app, its just display list of employees from api call, for that i have created data model for employee and calling it. but i get nothing i know somewhere it goes wrong help me to find out the problem(actually no errors but, its does not load the data).
here is the snippets
import 'package:flutter/material.dart';
import '../models/employee.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class EmployeeListScreen extends StatefulWidget {
EmployeeListScreen({Key key}) : super(key: key);
#override
_EmployeeListScreenState createState() => _EmployeeListScreenState();
}
class _EmployeeListScreenState extends State<EmployeeListScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Employee List"),
),
body: FutureBuilder(
future: fetchEmployees(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.none) {
return Center(
child: Text("None"),
);
}
if (snapshot.connectionState == ConnectionState.active) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return Center(child: Text("No Employees"));
} else {
return Center(
child: ListView.builder(
itemCount: snapshot.data.length[![enter image description here][1]][1],
itemBuilder: (BuildContext context, int index) {
return Text(snapshot.data[index]["name"]);
},
),
);
}
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
return Container();
},
));
}
Future<List<Employee>> fetchEmployees() async {
final response = await http.get(
"http://192.168.1.199/projects/ci/employee/api/getEmployees",
headers: {"accept": "application/json"});
debugPrint("Api Finished...");
if (response.statusCode == 200) {
final result = jsonDecode(response.body);
Iterable list = result['employees'];
print(list);
return list.map((employee) => Employee.fromJson(employee)).toList();
} else {
throw Exception("Failed to Load Employees");
}
}
}
see the screen shots.
i have the result while am using traditional api call without using model and factory methods, its very confusing to me also suggest me for best sites to learn these things, even i saw the official document it not clear at all.
To help debug the issue, how about trying this simplified code below. Call your fetchEmployees() from inside loadSlowData() method.
(It's not good practice to make an async call directly in FutureBuilder future:. Instead, make the async call in initState of the StatefulWidget. Since FutureBuilder is inside the build() method, and build could be called up to 60 times a second, you can obviously see the problem. If you happen to use an animation on that part of the widget tree, which refresh at 60fps, you'll get that situation.)
import 'package:flutter/material.dart';
class FutureBuilderStatefulPage extends StatefulWidget {
#override
_FutureBuilderStatefulPageState createState() => _FutureBuilderStatefulPageState();
}
class _FutureBuilderStatefulPageState extends State<FutureBuilderStatefulPage> {
Future<String> _slowData;
#override
void initState() {
super.initState();
_slowData = loadSlowData();
}
Future<String> loadSlowData() async {
// replace with your async call ↓ ↓
return await Future.delayed(Duration(seconds: 2), () => 'The Future has arrived');
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBldr Stateful'),
),
body: FutureBuilder<String>(
future: _slowData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(child: Text(snapshot.data));
}
return Center(child: Text('Loading...'));
},
),
);
}
}
You can possibly Try snapShot.hasData instead of snapshot.data
I have a Listview.builder() inside a FutureBuilder() that displays data fetched from API. I can retrieve the data successfully. But when I call the refreshData() function, previous data gets appended in the list.. How do I properly 'refresh' the widgets inside a FutureBuilder()?
Note: I'm only using get request here, so it's impossible that the data gets duplicated in the back-end. So the problem actually lies in displaying the data.
Here is my code:
class _MyHomePageState extends State<MyHomePage> {
List<Giver> _givers = [];
Future giversList;
getData() async {
_givers.addAll(await NetworkHelper().fetchGivers());
return _givers;
}
refreshData() {
giversList = getData();
}
#override
void initState() {
super.initState();
giversList = getData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: (){
setState(() {
refreshData();
});
},
child: Text('Refresh'),
),
FutureBuilder(
future: giversList,
builder: (context, snapShot){
switch(snapShot.connectionState) {
case ConnectionState.none:
return Center(child: Text('none'));
case ConnectionState.active:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
//this is where the listview is created
case ConnectionState.done:
return ListView.builder(
shrinkWrap: true,
itemCount: _givers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapShot.data[index].name),
subtitle: Text(snapShot.data[index].address),
);
});
default:
return Center(child: Text('Default!'));
}
},
)
],
),
),
);
}
}
As #pskink mentioned in the comment above, I just replaced _givers.addAll(await NetworkHelper().fetchGivers()); with _givers = await NetworkHelper().fetchGivers();
Thanks for the help!
I am trying to update the value of totalPricewith the value that comes from the response from API. I have created a currentTotal methods that contains setState(). Then passed snapshot.data.price.totalAmountvalue to currentTotal in order to update the value of totalPrice.But, it doesnt update the value. Can you help?
double totalPrice = 0;
#override
Widget build(BuildContext context) {
currentTotal(double x) {
setState(() {
totalPrice += x;
});
}
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: FutureBuilder<SearchResult>(
future: serviceOne.postCall(),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data != null) {
return new Material(
child: CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate([
ListTile(
title: new Text(totalPrice.toString()),
)
]),
),
]
)
}
currentTotal(snapshot.data.price.totalAmount);
else if (snapshot.hasError) {
return Text("error....${snapshot.error}");
}
There are many things needs to be fixed in your build.
1 - Your widget is StatefulWidget, to use FutureBuilder inside StatefulWidget read this:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Briefly, create Future future; instance field, then assign it inside the initState and use that future for FutureBuilder.
2 - your setState not inside a method, you have probably syntax error there. Create a void method and use setState inside it.
3 - You don't need to check twice like:
if (snapshot.hasData) {
if (snapshot.data != null) {
One of them enough, after the condition check, call your method includes setState, then display it.
Edit:
Here an example template for your solution:
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
Future<int> future;
int price = 0;
#override
void initState() {
future = fetchPrice();
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder<int>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(
child: Text(price.toString()),
);
}
return Center(child: CircularProgressIndicator());
},
),
),
);
}
Future<int> fetchPrice() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
final data = json.decode(response.body);
setState(() {
price = data['userId'];
});
return data['userId'];
}
}