Get the value of the Future<Map<String,String>> in FutureBuilder - dart

I have a Future method like below:
Future<Map<String,String>> readFavorites() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
names = prefs.getKeys();
for (var key in names) {
debugPrint("key is " + key);
debugPrint("value is " + prefs.get(key));
pairs.putIfAbsent(key, () => prefs.get(key));
}
return pairs;
}
I want to get the snapshot length plus the map's values in the futurebuilder below:
Widget build(BuildContext ctxt) {
return Container(
child: FutureBuilder(
future: readFavorites(),
builder: (context, AsyncSnapshot<Map<String,String>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
//replace this with a loading icon
child: new CircularProgressIndicator());
} else {
return ListView.builder(
itemExtent: 90,
itemCount: snapshot.data.length, <== How to get the map length?
itemBuilder: (BuildContext context, int index) {
return SingleDish(
dish_name: snapshot.data[index],
dish_picture: snapshot.data[index]., <== How to get the value from the map?
);
});
}
},
),
);
}
I tried the following but I got a null exception: snapshot.data[snapshot.data[index]]. Will appreciate any help.
UPDATE
What is interesting is that when I printed the key I got the following:
lib_cached_image_data_last_clean
Future<Map<String, String>> readFavorites() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
names = prefs.getKeys();
//This returned the correct value because I hardcoded the key
print("hardcoded key is " + prefs.getString("Cutlet"));
for (var key in names) {
//This fellow here returned lib_cached_image_data_last_clean
print("key is" + key);
pairs.putIfAbsent(key, () => prefs.get(key));
// print("key is " + pairs.length.toString());
}
return pairs;
}
So, I know for a fact that readFavorites() returns values. But am not sure why the key is not what I added in the SharedPreferences.

Take a look at this code it is auto explained and you can adapt this code to your needs.
Widget build(BuildContext ctxt) {
return Container(
child: FutureBuilder(
future: readFavorites(),
builder: (context, AsyncSnapshot<Map<String,String>> snapshot) {
switch( snapshot.connectionState){
case ConnectionState.none:
return Text("there is no connection");
case ConnectionState.active:
case ConnectionState.waiting:
return Center( child: new CircularProgressIndicator());
case ConnectionState.done:
if (snapshot.data != null){
Map<String,String> myMap = Map.from( snapshot.data ); // transform your snapshot data in map
var keysList = myMap.keys.toList(); // getting all keys of your map into a list
return ListView.builder(
itemExtent: 90,
itemCount: myMap.length, // getting map length you can use keyList.length too
itemBuilder: (BuildContext context, int index) {
return SingleDish(
dish_name: keysList[index], // key
dish_picture: myMap[ keysList[index] ], // getting your map values from current key
);
}
);
}
// here your snapshot data is null so SharedPreferences has no data...
return Text("No data was loaded from SharedPreferences");
}//end switch
},
),
);
}

Related

How to assign <List<Data>> to list variable?

How to display one by one data using this DB function?
Future<List<Data>> display() async {
//final Database db = await database;
var db = await db1;
final List<Map<String, dynamic>> maps = await db.query('syncTable');
return List.generate(maps.length, (i) {
return Data(
syn_TableName: maps[i]['syn_TableName'],
syn_ChangeSequence: maps[i]['syn_ChangeSequence'],
);
});
}
You can use the FutureBuilder to consume your display() method. Then inside the FutureBuilder you can use AsyncSnapshot.data to get your List of Dataelements.
In the next step you use can call List.map() to map your Data to widgets. In this example I use the ListTile to display:
snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
Here a minimal working example which you can try out:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder<List<Data>>(
initialData: [],
future: display(),
builder: (context, snapshot) {
return ListView(
children: snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
);
}),
),
);
}
Future<List<Data>> display() async {
return List.generate(15, (i) {
return Data(
syn_TableName: 'syn_TableName $i',
syn_ChangeSequence: 'syn_ChangeSequence $i',
);
});
}
}
class Data {
final String syn_TableName;
final String syn_ChangeSequence;
Data({this.syn_ChangeSequence, this.syn_TableName});
}

snapshot.ConnectionState always waiting

In a FutureBuilder, snapshot.ConnectionState is always waiting, but the future function completed successfully. While in a different page the same block of code works and the ConnectionState transitions from waiting to done.
The future function:
Future getDoctors() async {
var res = await http.get(globals.domain + "users/docs/");
var resBody = json.decode(utf8.decode(res.bodyBytes));
print(res.statusCode);
if (res.statusCode == 200) {
if (mounted) {
setState(() {
doctors = resBody;
});
}
}
}
The future builder:
FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, snapshot) {
print(snapshot);
}
)
Actual result:
AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null)
Expected result: transiton to AsyncSnapshot<dynamic>(ConnectionState.done, null, null)
EDIT1:
while debugging noticed that the future function getDoctors() is called periodically, I guess that's why the snapshot is always waiting
Calling setState causes the widget tree to be rebuild. Therefore, when the FutureBuilder is rebuilt, the getDoctors() function is invoked again, causing an infinite loop (getDoctors()->setState()->rebuild->getDoctors()...)
The solution is to either remove setState from the getDoctors method or invoke getDoctors() once in the initState() method, store the Future and pass it to the FutureBuilder, thus ensuring that it is only done once.
Future _doctorsFuture;
initState() {
...
_doctorsFuture = getDoctors();
}
.
.
// Somewhere in build()
FutureBuilder(
future: doctorsFuture,
builder: (BuildContext context, snapshot) {
print(snapshot);
}
),
You need to return the value from the Future to finish the connection. Change your Future to something like this:
Future<dynamic> getDoctors() async {
var res = await http.get(globals.domain + "users/docs/");
if (res.statusCode == 200) {
return json.decode(utf8.decode(res.bodyBytes));
}
return null;
}
And change your FutureBuilder to this:
FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
print(snapshot);
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData && snapshot.data != null) {
return Center(...);
}
return Center(child: CircularProgressIndicator());
},
)
Please modify to match your variables and classes.
Change the return of the Future like:
`Future<String> getDoctors() async{
var res = await http.get(globals.domain + 'users/docs/');
var resBody = json.decode(utf8.decode(res.bodyBytes));
if (res.statusCode == 200) {
if (mounted) {
setState(() {
doctors = resBody;
});
}
}
}`
and the FutureBuilder:
`FutureBuilder(
future: getDoctors(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
break;
case ConnectionState.waiting:
return CircularProgressIndicator(
color: Colors.black54,
strokeWidth: 2,
);
case ConnectionState.active:
break;
case ConnectionState.done:
return Container(/*here write your return code*/);
}
return Text('Some error ocurred try getDoctor');
},
);`

Selection of Item in DropdownButton causes Flutter to throw error

I am currently trying to retrieve data (tags) from a REST API and use the data to populate a dropdown menu which I can successfully do but upon selection of the item, I get the following error which according to this would mean that the "selected value is not member of the values list":
items == null || value == null || items.where((DropdownMenuItem item) => item.value == value).length == 1': is not true.
This occurs after the dropdown menu shows my selected item. However, this is error should not be occurring as I've done the necessary logging to check that the data is indeed assigned to the list in question. Could anyone help me resolve this issue? I have isolated it to down to it originating in the setState() method in onChanged of DropdownButton but can't seem to understand why that should be causing an issue. Any help would be deeply appreciated!
My code is as follows:
class _TodosByTagsHomePageState extends State<TodosByTagsHomePage> {
Tag selectedTag;
final Logger log = new Logger('TodosByTags');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: ListView(
children: <Widget>[
FutureBuilder<List<Tag>> (
future: fetchTags(),
builder: (context, snapshot) {
if (snapshot.hasData) {
log.info("Tags are present");
_tagsList = snapshot.data;
return DropdownButton<Tag>(
value: selectedTag,
items: _tagsList.map((value) {
return new DropdownMenuItem<Tag>(
value: value,
child: Text(value.tagName),
);
}).toList(),
hint: Text("Select tag"),
onChanged: (Tag chosenTag) {
setState(() {
log.info("In set state");
selectedTag = chosenTag;
Scaffold.of(context).showSnackBar(new SnackBar(content: Text(selectedTag.tagName)));
});
},
) ;
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(width: 0.0, height: 0.0);
}),
])
);
}
// Async method to retrieve data from REST API
Future<List<Tag>> fetchTags() async {
final response =
await http.get(REST_API_URL);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
var result = compute(parseData, response.body);
return result;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
static List<Tag> parseData(String response) {
final parsed = json.decode(response);
return (parsed["data"] as List).map<Tag>((json) =>
new Tag.fromJson(json)).toList();
}
List<Tag> _tagsList = new List<Tag>();
}
// Model for Tag
class Tag {
final String tagName;
final String id;
final int v;
Tag({this.id, this.tagName, this.v});
factory Tag.fromJson(Map<String, dynamic> json) {
return new Tag(
id: json['_id'],
tagName: json['tagName'],
v: json['__v'],
);
}
}
update your code like this
I think issues that when calling setState in FutureBuilder that call fetchTags() move fetchTags() to initState() for once call
class _TodosByTagsHomePageState extends State<TodosByTagsHomePage> {
Tag selectedTag;
Future<List<Tag>> _tags;
#override
void initState() {
_tags = fetchTags();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: ListView(children: <Widget>[
FutureBuilder<List<Tag>>(
future: _tags,
builder: (context, snapshot) {
if (snapshot.hasData) {
return DropdownButton<Tag>(
value: selectedTag,
items: snapshot.data.map((value) {
print(value);
return DropdownMenuItem<Tag>(
value: value,
child: Text(value.tagName),
);
}).toList(),
hint: Text("Select tag"),
onChanged: (Tag chosenTag) {
setState(() {
selectedTag = chosenTag;
});
},
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(width: 0.0, height: 0.0);
}),
]));
}

Usage of FutureBuilder with setState

How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}
Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}
Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}

Flutter Programmatically trigger FutureBuilder

Let's say I have something like this:
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh,
...
)
}
)
In the _handleRefresh method, I want to programmatically trigger the re-run of the FutureBuilder.
Is there such a thing?
The use case:
When a user pulls down the refreshIndicator, then the _handleRefresh simply makes the FutureBuilder rerun itself.
Edit:
Full code snippet end to end, without the refreshing part. I've switched to using the StreamBuilder, how will the refreshIndicator part fit in all of it?
class DealList extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _DealList();
}
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
// prevents refreshing of tab when switch to
// Why? https://stackoverflow.com/q/51224420/1757321
bool get wantKeepAlive => true;
final RestDatasource api = new RestDatasource();
String token;
StreamController _dealsController;
#override
void initState() {
super.initState();
_dealsController = new StreamController();
_loadingDeals();
}
_loadingDeals() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
this.token = prefs.getString('token');
final res =
this.api.checkInterests(this.token).then((interestResponse) async {
_dealsController.add(interestResponse);
return interestResponse;
});
return res;
}
_handleRefresh(data) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
await this.api.checkInterests(token).then((interestResponse) {
_dealsController.add(interestResponse);
});
return null;
}
#override
Widget build(BuildContext context) {
super.build(context); // <-- this is with the wantKeepAlive thing
return StreamBuilder(
stream: _dealsController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
...
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No deals');
}
if (snapshot.hasData) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DealsDetailPage(
dealDetail: deal,
),
),
);
},
title: Text(deal['name']),
subtitle: Text(deal['expires']),
);
},
),
}
},
);
}
}
Why not using a StreamBuilder and a Stream instead of a FutureBuilder?
Something like that...
class _YourWidgetState extends State<YourWidget> {
StreamController<String> _refreshController;
...
initState() {
super...
_refreshController = new StreamController<String>();
_loadingDeals();
}
_loadingDeals() {
_refreshController.add("");
}
_handleRefresh(data) {
if (x) _refreshController.add("");
}
...
build(context) {
...
return StreamBuilder(
stream: _refreshController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh(snapshot.data),
...
)
}
);
}
}
I created a Gist with the Flutter main example using the StreamBuilder, check it out
Using StreamBuilder is a solution, however, to trigger the FutureBuilder programmatically, just call setState, it'll rebuild the Widget.
return RefreshIndicator(
onRefresh: () {
setState(() {});
},
...
)
I prefer FutureBuilder over StreamBuilder since I am using Firestore for my project and you get billed by reads so my solution was this
_future??= getMyFuture();
shouldReload(){
setState(()=>_future = null)
}
FutureBuilder(
future: _future,
builder: (context, snapshot){
return Container();
},
)
and any user activity that needs you to get new data simply call shouldReload()

Resources