Why does my async method run twice in Flutter? - flutter

I want to load a static list data when entering indexScreen,but the list sometimes show twice the same content,sometimes not.
This is my list setting:List<ListClass> listItems=List<ListClass>();,ListClass is a simple class with on different attributes and a constructor.
I use home:IndexScreen() in main.dart to show Index page.
return MaterialApp(
home: IndexScreen(),
debugShowCheckedModeBanner: false,
onGenerateRoute: router.generator,
builder: EasyLoading.init(),
);
And before this page build,it will update listItems using:
Future<bool> initUserAndIndex() async{
if (curUserEmail==null) sharedGetData(USER_EMAIL).then((value) => curUserEmail=value.toString());
print(curUserEmail);
await UserTable().getUserInfo(curUserEmail).then((value){print("user ok");});
await CollectionTable().getIndexList().then((value){print("Collection ok");return true;});
return null;
}
buildPage:
#override
Widget build(BuildContext context) {
return FutureBuilder<Object>(
future: initUserAndIndex(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState==ConnectionState.waiting)
{
EasyLoading.show(status: 'loading...');
// avoid no return,this cause a whiteborad transition,I don't know how to solve it,too.
return Container();
}
else
{
EasyLoading.dismiss();
return SafeArea(
child: Scaffold(
// the listItems is used in Body()
body: Body(),
),
);
}
},
);
}
}
I run this app,and it prints twice user ok and Collection ok.But when I use ROUTER.NAVIGETE,it only prints once.
User Information is OK,but the list is such a great problem--the page shows twice content
I put my code at an order of relevance of this prblom,I think.Next I put my the two awaited funtion here:
User:
Future<bool> getUserInfo(String userEmail) async{
await userCollection.where({'userEmail':userEmail}).get().then((res) async {
//assign to the static variables
return true;
});
return null;
}
Collection:
Future<bool> getIndexList() async {
listItems.clear();
await listCollection.get().then((value){
var v = value.data;
for (var data in v) {
//get data and package them,add after the listItems list.
listItems.add(ListClass(header, content, userId, favorCount, wordCount));
}
return true;
});
}

You probably want to assign your future in your widget class, but not in the build method as the documentation show, otherwise, everytime your build method is triggered, it will call again your FutureBuilder.
final Future<String> _calculation = Future<String>.delayed(
const Duration(seconds: 2),
() => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: Theme.of(context).textTheme.headline2!,
textAlign: TextAlign.center,
child: FutureBuilder<String>(
future: _calculation, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// ...
}
),
),
}

Related

How can I listen to changes of multiple providers from a future builder?

I want to display a BarChart where I use as source information two different providers.
My initial approach was to use a future builder where I show a loading icon while the data is being fetched, and then manipulate that data to suite my needs in the graph.
So I used a future builder from the widget where I display my graph and initialized it with a Future that will get the context from another reusable file.
my_wdiget.dart
...
class _MyWidgetState extends State<MyWidget> {
late Future<List<MyObject>> myFutureVariable;
Future<List<MyObject>> _getMyObjects() async {
return getMyObjects(context);
}
#override
void initState() {
super.initState();
myFutureVariable= _getMyObjects();
}
...
FutureBuilder<List<MyObject>>(
future: myFutureVariable,
builder: (BuildContext context,
AsyncSnapshot<List<MyObject>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator())),
);
default:
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'))),
);
} else if (snapshot.data == null) {
return Center(
child:
Text('You don\'t have any item yet.'))),
);
} else {
return BarChart(
_getChartData(snapshot.data!),
),
);
}
}
},
),
And this is the file where I generate the data:
my_object_utils.dart
Future<List<MyObject>> getMyObjects(BuildContext context) async {
await Future.delayed(Duration(seconds: 1)); // Simulation delayed query
var source1= Provider.of<MySource1>(context, listen: false).items;
var source2 = Provider.of<MySource2>(context, listen: false).otherItems;
List<MyObject> myObjects= [];
// Do some stuff to fill the myObjects using source1 and source2
return myObjects;
}
Problems:
This kind of works but I get the warning use_build_context_synchronously from the lines of the Provider.
I want to listen to changes, but if I set the default listen: true it will crash telling me to change that property.
So my question is, how can I have a FutureBuilder listening to changes of multiple providers?
Update using approach suggested #hydra:
If I have:
void test() {
print('a');
setState(() {});
}
Consumer2<MySource1, MySource1>(
builder: (context, sourceOne, sourceTwo, child) {
myFutureVariable = getMyObjects(sourceOne.items, sourceTwo.otherItems),
return FutureBuilder<List<MyObject>>(
future: myFutureVariable,
builder: (context, snapshot) {
...
else{
return child: ElevatedButton(
child: Text('a'),
onPressed: test,
);
}
}
),
},
),
Every time the button is pressed it will trigger the setState and and the circularProgressIndicator will appear although no changes were made in the consumers.
to solve both problems you can use Consumer2
the FutureBuilder will rebuild if either of the two provider changed
Consumer2<MySource1, MySource1>(
builder: (context, sourceOne, sourceTwo, child) {
return FutureBuilder<List<MyObject>>(
future: myFutureVariable(sourceOne.items, sourceTwo.otherItems),
builder: (context, snapshot) {
// ...
}
),
},
),
and update your function to:
Future<List<MyObject>> getMyObjects(final items, final otherItems) async {
// use your items and otherItems here.
await Future.delayed(Duration(seconds: 1)); // just for testing, right?
List<MyObject> myObjects= [];
// Do some stuff to fill the myObjects using source1 and source2
return myObjects;
}

Appbar should show number of records using futurebuilder in flutter

I have just created a demo for better understanding future builder
scaffold body showing all users from api and appear should be shown with number of users
appear's title showing 0 when loaded but does not change...what to do to rebuild it
here is my code
class _withmodelState extends State<withmodel> {
List<UserModel> userlist=[];
Future<List<UserModel>> getdata() async {
final resp =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (resp.statusCode == 200) {
print('i ma called');
List<dynamic> dlist = json.decode(resp.body);
await Future.delayed(Duration(seconds: 2));
userlist= dlist.map((e) => UserModel.fromJson(e)).toList();
return userlist;
}
return userlist;
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(title: Text("Total users="+userlist.length.toString()),),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
));
}
You can use ChangeNotifier like this, first create a class like this:
class WithmodelDecl with ChangeNotifier {
ValueNotifier<int> totalUsers = ValueNotifier<int>(0);
}
WithmodelDecl withmodeldecl = new WithmodelDecl();
then use it like this:
return SafeArea(
child: Scaffold(
appBar: PreferredSize(
child: ValueListenableBuilder<int>(
valueListenable: withmodeldecl.totalUsers,
builder: (context, value, _) {
return AppBar(
title: Text("Total users=" + value.toString()),
);
}),
preferredSize: AppBar().preferredSize),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
));
and finally change your getdata to this:
Future<List<UserModel>> getdata() async {
final resp =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (resp.statusCode == 200) {
print('i ma called');
List<dynamic> dlist = json.decode(resp.body);
await Future.delayed(Duration(seconds: 2));
userlist= dlist.map((e) => UserModel.fromJson(e)).toList();
withmodeldecl.totalUsers.value = userlist.length;
return userlist;
}
return userlist;
}
You also need to rebuild the Text widget, that you are using to show the count, when the count is available, i.e., the Future completes.
You need to wrap that Text widget with FutureBuilder like this:
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: FutureBuilder<List<UserModel>>(
future: getdata(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final List<UserModel> userlist = snapshot.data!;
return Text("Total users= ${userlist.length}");
// it's better to use String interpolation than "Total users=" + snapshot.data!.length.toString()
} else {
// return loading widget
}
},
),
),
body: MyBody(
//MyBody returning FutureBuilder for showing userlist array;
),
),
);
It is better to have the Future in a variable, and then use it like this, to avoid unwanted and repeated calling of it whenever the build() method is called:
late final Future<List<UserModel>> _userListFuture;
And initialize it in your initState(), like this:
#override
void initState() {
super.initState();
_userListFuture = Future<List<UserModel>>(getdata);
}
And use it with your FutureBuilder like this:
FutureBuilder<List<UserModel>>(
future: _userListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// return your widget showing data
} else {
// return loading widget
}
},
)

How do I incorporate a function where return is a String into a string?

I created a function with a return value of String as follows.
Future<String?> setName() {
String? name='Jon';
return name;
}
Is there any way to incorporate this into the string?
I want to use following situations.
print('Name:${setName}');
or
appBar: AppBar(
title: Text('Name:${setName}'),
When I wrote the above, it unexpectedly output the following
Name:Closure:()=> Future<String?> from Function ...
You could either set a variable to the current state (loading, loaded) and switch the UI accordingly like
return isLoading ? LoadingView() : LoadedView()
or you just pass the future in a FutureBuilder and handle the logic inside it
return FutureBuilder<String>(
future: setName(), // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return LoadedView();
} else {
return LoadingView();
},
}
),
);
It would be better if you use FutureBuilder for future method.
Create a state future variable.
class _TestXState extends State<TestX> {
Future<String?> setName() async {
String? name = 'Jon';
await Future.delayed(Duration(seconds: 2));
return name;
}
late final nameFuture = setName();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: FutureBuilder(
future: nameFuture,
builder: (context, snapshot) {
if (snapshot.hasData) return Text("${snapshot.data}");
return Text("....");
},
),
),
);
}
}

Flutter - Instance of Future<int> returned from the function instead of the value

In the code provided the print function returns the correct number of friends that should be printed, but when I try to return it, it returns (Instance of Future ) and does not return the real value (the number)
Future<int> FriendsNumber() async {
final count = await _firestore
.collection('FriendsList')
.doc(User.userID)
.collection("FriendsList")
.where("Status", isEqualTo: 1)
.get()
.then((res) => res.size);
print('number of friends is:');
print(count);
return count;
}
return Scaffold(
body: Column(
children: [
Text(
FriendsNumber().toString();
),
],
),
);
That is how async and await works in Dart. await allows asynchronous functions to have the appearance of synchronous functions by allowing asynchronous code to be executed very similarly to synchronous code. This line in your code defers further execution of this function until the result of the firestore query is returned:
final count = await _firestore
.collection('FriendsList')
.doc(User.userID)
.collection("FriendsList")
.where("Status", isEqualTo: 1)
.get()
.then((res) => res.size);
Since dart is not blocking here it has to return some value, which in this case is a Future<int>, which bascially means that in the Future this will be resolved to an int value.
Your print statement is after the await (where the execution will pick up again when the result from firestore got returned) and thus can use value directly.
You have to use future builder for this
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder<int>(
future: FriendsNumber(),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
// while data is loading:
return Center(
child: CircularProgressIndicator(),
);
} else {
// data loaded:
final friendNumber= snapshot.data;
return Center(
child: Text('Friends are: $friendNumber'),
);
}
},
),
),
);
}
}
You can not extract the value of a Future without using a FutureBuilder widget.
If this is a data that you will need only once and it will not change you can call your Future method inside the initState() method like that :
#override
void initState() {
super.initState();
FriendsNumber().then((value) {
setState(() {
count = value; //here you can continue your filter logic, ex: value.contains etc...
});
});
}
Doing so will allow you to display the data like you show in your post.
return Scaffold(
body: Column(
children: [
Text(
count.toString();
),
],
),
);

How can I update the value of the parameter with the value that come from API response?

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'];
}
}