Flutter: build list from sharedPreferences-list with immediate visible effects - flutter

I am trying to learn flutter and building a small "shopping list app". For this purpose I save the state of my shopping list to the sharedPreferences for later use. This way I was able to restore the same list after closing and opening the app again, but only after "triggering a rebuild"(?) by starting to type something in a text field, using the following code:
class _ItemChecklistState extends State<ItemChecklist> {
final List<ShoppingItem> _items = [];
final _itemNameController = TextEditingController();
final _amountController = TextEditingController()..text = '1';
final Map<int, bool> checkedMap = new Map();
bool _isComposing = false;
...
#override
Widget build(BuildContext context) {
// calling the method to "preload" my state from the shared preferences
_loadPrefs();
return Scaffold(
appBar: AppBar(
title: Text('Shopping List'),
actions: <Widget>[
IconButton(
onPressed: () => _removeCheckedItems(),
icon: Icon(Icons.remove_done)),
IconButton(
icon: const Icon(Icons.remove_circle_outline),
tooltip: 'Remove all items',
onPressed: () => _removeAllItems(),
),
],
),
body: Column(children: [
Flexible(
child: ListView.builder(
itemBuilder: (_, int index) => _items[index],
padding: EdgeInsets.all(8.0),
itemCount: _items.length,
),
),
Divider(height: 1.0),
Container(child: _buildTextComposer())
]));
}
...
// the method I use to "restore" my state
void _loadPrefs() async {
String key = 'currentItemList';
SharedPreferences prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey(key)) { return; }
_items.clear();
checkedMap.clear();
Map stateAsJson = jsonDecode(prefs.getString(key));
final itemsKey = 'items';
final checkedMapKey = 'checkedMap';
List items = stateAsJson[itemsKey];
Map checkedMapClone = stateAsJson[checkedMapKey];
for (Map item in items){
ShoppingItem newItem = ShoppingItem(
id: item['id'],
name: item['name'],
amount: item['amount'],
removeFunction: _removeItemWithId,
checkedMap: checkedMap,
saveState: _saveListToSharedPrefs,
);
_items.add(newItem);
checkedMap.putIfAbsent(newItem.id, () => checkedMapClone[newItem.id.toString()]);
}
}
...
}
Now at this point loading the state and setting the lists works fine, so _items list is updated correctly, as well as the checkedMap, but the ListView does not contain the corresponding data. How can I for example "trigger a rebuild" immediatlly or make sure that the "first" build of the ListView already contains the correct state?
Thanks :)

You have to use FutureBuilder when your UI depends on a async task
Future<List<ShoppingItem>> _getShoppingItems;
#override
void initState() {
_getShoppingItems = _loadPrefs();
super.initState();
}
#override
Widget build(BuildContext context) {
FutureBuilder<List<ShoppingItem>>(
future: _getShoppingItems,
builder: (context, snapshot) {
// Data not loaded yet
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator();
}
// Data loaded
final data = snapshot.data;
return ListView(...);
}
}
);
More info : https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Related

ListView items reinitializing on List length change

So, i am working on weather application.
I am getting list of cities from device memory:
late List<String> citiesList;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: citiesList.length,
itemBuilder: (context, index) => CityCard(
name: citiesList[index],
dismissCallback: _deleteCity,
key: ValueKey(citiesList[index])
)
);
}
Future<void> _deleteCity(String name) async {
setState((){
citiesList.remove(name);
});
await CitiesListManager().deleteCity(name: name);
}
App renders list of CityCards, which are dismissibles.
Every CityCard gets temperature of its own city from api on initialization. Until it gets response from Api, it displays spinner.
class _CityCardState extends State {
late Key key;
late String cityName;
late String temperature;
late Future<double> temperatureFuture;
late Function dismissCallback;
_CityCardState({
required String cityName,
required Function dismissCallback,
required Key key
}){
this.cityName = cityName;
this.dismissCallback = dismissCallback;
this.key = key;
}
#override
void initState() {
this.temperatureFuture = _getCurrentTemperature();
super.initState();
}
Widget build(BuildContext context){
return FutureBuilder(
future: temperatureFuture,
builder: (context, AsyncSnapshot<dynamic> snapshot){
if (snapshot.hasData) {
return Dismissible(
key: key,
onDismissed: (dismissDirection) async {
await dismissCallback(cityName);
},
confirmDismiss: (dismissDirection) => _showConfirmDeletionDialog(context),
child: Text(
'$cityName',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: FontConstants.MIDDLE_SIZE
),
),
);
}
else {
return Card();
}
}
);
}
_showConfirmDeletionDialog(BuildContext dismissableContext){
return showDialog<bool>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Confirm city deletion'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
],
),
).then((value) {
return value;
});
}
Future<double> _getCurrentTemperature() async {
final double currentTemperature = await
WeatherApi().getCurrentTemperature(cityName);
setState((){
temperature = currentTemperature.toString();
});
return currentTemperature;
}
}
Whenever i dismiss one of cities, every CityCard displays spinner (which i suppose witnesses about reinitializing). Same works for when i add a city.
Looks like whenever List's length change, every item reinitializes and requesting api again.
So, im wondering if there's a way to avoid this reinitializing.
Every item reinitializing because you are calling getCurrentTemperature in the state of the CityCard.
The right logic should be to create a model like:
City {
String name;
double _temperature;
Future<double> _getCurrentTemperature() async {
...
}
}
Then populate a List of cities (better, if you need a key, think about using a Map) and pass every item to the widget CityCard, that probably could be stateless.
This way you assert that the list of cities is valorized once, with no rebuild of the widget CityCard.

Icon value not updating with provider and sqflite in flutter

I was making a simple cart app, it did well but cart count not showing when app is closed and reopened again.
I am using provider and calls fetchCartProducts() method when the app is opened. It calls fine. but cart badge widget itemcount is not changing at first time. only shows 0 at first time.
Future<void> fetchCartProducts() async {
final dataList = await DBHelper.getData('cart_food');
//convert dataList to _cartItems
final entries = dataList
.map((item) => CartModel(
item['id'],
item['price'].toDouble(),
item['productName'],
item['quantity'],
))
.map((cart) => MapEntry(cart.id, cart));
_cartItems = Map<String, CartModel>.fromEntries(entries);
print('inside fetchcart');
}
class HomeScreen extends StatefulWidget
{
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
{
Future<List<FoodItem>> _foodItems;
var _isInit = true;
#override
void initState() {
super.initState();
_foodItems = ApiService.getFoodItems();
Provider.of<CartProvider>(context, listen: false).fetchCartProducts();
setState(() {});
}
#override
void didChangeDependencies()
{
if (_isInit) {
Provider.of<CartProvider>(context).fetchCartProducts();
_isInit = false;
setState(() {});
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final cart = Provider.of<CartProvider>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: const Text('Food Cart'),
actions: [
//this is not updating when the app is closed and opened again.
Consumer<CartProvider>(
builder: (_, cartprovider, ch) => Badge(
child: ch,
value: cartprovider.itemCount.toString(),
),
child: IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) {
return CartScreen();
}),
);
},
),
),
],
),
body: FutureBuilder<List<FoodItem>>(
future: _foodItems,
builder: (conext, snapshot) => !snapshot.hasData
? const Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
FoodItem foodItem = snapshot.data[index];
return ListTile(
title: Text(foodItem.productName),
subtitle: Text(foodItem.variant),
trailing: IconButton(
onPressed: () {
cart.addToCart(
foodItem.storeid.toString(),
foodItem.productName,
1,
foodItem.price,
);
setState(() {});
},
icon: const Icon(Icons.shopping_cart),
),
);
},
),
),
);
}
}
otherwise when item added to cart, it working fine. the data loss when reopened. how to get total count when the app starts?
In order to rebuild Consumer you need to call notifyListeners() inside your CartProvider
Add notifyListeners() to your fetchCartProducts() after assigning the value to _cartItems = Map<String, CartModel>.fromEntries(entries);
Future<void> fetchCartProducts() async {
final dataList = await DBHelper.getData('cart_food');
//convert dataList to _cartItems
final entries = dataList
.map((item) => CartModel(
item['id'],
item['price'].toDouble(),
item['productName'],
item['quantity'],
))
.map((cart) => MapEntry(cart.id, cart));
_cartItems = Map<String, CartModel>.fromEntries(entries);
notifyListeners(); // <------- this line
print('inside fetchcart');
}

SearchDelegate in Flutter: The method 'where' was called on null

I'm trying to do a search in my application and I'm using this SearchDelegate for that. Previously, when I used a provider, everything worked, but I had to make serious changes in the code and now this algorithm below is responsible for finding routes. I am trying to put RouteWithStops in SearchDelegate, and after that to use FutureBuilder inside Widget buildSuggestions. So the code is like this:
The algorithm for searching routes with stops with dart models:
Future<List<RouteWithStops>> getMarshrutWithStops(int ttId) async {
if (routesbyTransportType.isEmpty) {
await fetchTransportWithRoutes();
}
List<Routes> routes = routesbyTransportType[ttId].routes;
List<ScheduleVariants> variants = [];
variants.addAll(await api.fetchSchedule());
List<RouteWithStops> routesWithStops = [];
for (Routes route in routes) {
final routeWithStops = RouteWithStops();
routesWithStops.add(routeWithStops);
routeWithStops.route = route;
routeWithStops.variant =
variants.where((variant) => variant.mrId == route.mrId).first;
}
return routesWithStops;
}
Future<RouteWithStops> fetchStopsInfo(routeWithStops) async {
List<RaceCard> cards = [];
List<StopList> stops = [];
cards.addAll(await api.fetchRaceCard(routeWithStops.variant.mvId));
stops.addAll(await api.fetchStops());
print(cards);
List<StopList> currentRouteStops = [];
cards.forEach((card) {
stops.forEach((stop) {
if (card.stId == stop.stId) {
currentRouteStops.add(stop);
}
});
});
routeWithStops.stop = currentRouteStops;
return routeWithStops;
}
}
the models:
#HiveType(typeId: 0)
class RouteWithStops {
#HiveField(0)
Routes route;
#HiveField(1)
List<StopList> stop;
#HiveField(2)
List<RaceCard> cards;
#HiveField(3)
ScheduleVariants variant;
#HiveField(4)
Transport transport;
}
The SearchDelegate which I want to use to search in a list of routes, the single route I want:
class SearchBar extends SearchDelegate<RouteWithStops> {
final int ttId;
final RouteWithStops routeWithStops;
TransportService service = getIt<TransportService>();
SearchBar({this.ttId, this.routeWithStops});
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = '';
})
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
});
}
#override
Widget buildResults(BuildContext context) {
return FutureBuilder(
future: service.getMarshrutWithStops(ttId),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
List<RouteWithStops> routes = [];
List<RouteWithStops> recentRoutes = [];
final suggestion = query.isEmpty
? recentRoutes
: routes
.where((element) => element.route.mrTitle.startsWith(query))
.toList();
print(routes?.toString());
return (routes == null)
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: suggestion.length,
itemBuilder: (context, index) {
return ListTile(
title: RichText(
text: TextSpan(
text: suggestion[index]
.route
.mrTitle
.substring(0, query.length),
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold),
),
),
);
});
});
}
#override
Widget buildSuggestions(BuildContext context) {
return FutureBuilder(
future: service.getMarshrutWithStops(ttId),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
List<RouteWithStops> recentRoutes = [];
final suggestion = query.isEmpty
? recentRoutes
: routes
.where((element) => element.route.mrTitle.startsWith(query))
.toList();
print(routes?.toString());
return (routes == null)
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: suggestion.length,
itemBuilder: (context, index) {
return ListTile(
title: RichText(
text: TextSpan(
text: suggestion[index]
.route
.mrTitle
.substring(0, query.length),
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold),
),
),
);
});
});
}
}
When i am trying to serach an item in items I get the progress indicator and after I trying to type one letter my app crushes and I got this error:
The method 'where' was called on null.
Receiver: null
Tried calling: where(Closure: (RouteWithStops) => bool)
I understand that this error is pretty straightforward and it sayas that something is null inside that algorithm, but I use this algo in the whole app and everything works fine without any errors. But may be I think wrongly in using SearchDelegate. Can somebody, please, help me?
future: service.getMarshrutWithStops(ttId),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
List<RouteWithStops> recentRoutes = [];
final suggestion = query.isEmpty
? recentRoutes
: routes
.where((element) => element.route.mrTitle.startsWith(query))
.toList();
print(routes?.toString());
Your snapshot.data is null. You have a FutureBuilder and no check on the status of the future at all. It might have failed or still be processed.
Also, don't call your Future generating method in your build method. Use a variable. Your build method can be called many times and you don't want to generate a new future every time that happens.

Cannot display data downloaded from Firestore using Listview.Builder and ListTile

I want to display a list from downloading the data from firestore. The download is successful (the full list can be printed) but somehow it cannot be displayed. Simply nothing is shown when I use the ListView.builder and ListTile. Pls help what is the problem of my code. Great thanks.
class DownloadDataScreen extends StatefulWidget {
#override
List<DocumentSnapshot> carparkList = []; //List for storing carparks
_DownloadDataScreen createState() => _DownloadDataScreen();
}
class _DownloadDataScreen extends State<DownloadDataScreen> {
void initState() {
super.initState();
readFromFirebase();
}
void readFromFirebase() async {
await FirebaseFirestore.instance
.collection('carpark')
.get()
.then((QuerySnapshot snapshot) {
snapshot.docs.forEach((DocumentSnapshot cp) {
widget.carparkList.add(cp);
//to prove data are successfully downloaded
print('printing cp');
print(cp.data());
print(cp.get('name'));
});
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(
'Car Park',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 9,
child: Container(
child: ListView.builder(
itemCount: widget.carparkList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.carparkList[index].get('name')),
subtitle: Text(
widget.carparkList[index].get('district')),
onTap: () {
},
);
},
),
),
),
],
),
),
);
}
}
create the list in state add it to the top line of the initState method List carparkList = [];
class DownloadDataScreen extends StatefulWidget {
_DownloadDataScreen createState() => _DownloadDataScreen();
}
class _DownloadDataScreen extends State<DownloadDataScreen> {
List<DocumentSnapshot> carparkList = []; //List for storing carparks
void initState() {
super.initState();
readFromFirebase();
}
void readFromFirebase() async {
await FirebaseFirestore.instance
.collection('carpark')
.get()
.then((QuerySnapshot snapshot) {
snapshot.docs.forEach((DocumentSnapshot cp) {
widget.carparkList.add(cp);
//to prove data are successfully downloaded
print('printing cp');
print(cp.data());
print(cp.get('name'));
});
});
}

Using search delegate for a listview generated from Future

Here is my listview generated from a Future from a json file.
class _ChorusPage extends State<ChorusPage> {
static Future<List<Chorus>> getList() async {
var data = await rootBundle.loadString('assets/chorusJson.json');
var jsonMap = json.decode(data); // cast<Map<String, dynamic>>();
List<Chorus> choruses = [];
for (var c in jsonMap) {
Chorus chorus = Chorus.fromJson(c);
choruses.add(chorus);
}
// var = User.fromJson(parsedJson);
// choruses = jsonMap.map<Chorus>((json) => Chorus.fromJson(json)).toList();
print(choruses.length);
return choruses;
}
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('Chorus'), actions: <Widget>[
IconButton(icon: Icon(Icons.search), onPressed: () {})
]),
body: Container(
child: FutureBuilder(
future: getList(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Container(
child: Center(child: CircularProgressIndicator()));
} else {
return ListView.builder(
itemCount: snapshot.data.length, // + 1,
itemBuilder: (BuildContext context, int index) {
return _listItem(index, snapshot);
});
}
})),
);
}
I am trying to implement a search function using the search delegate. The tutorial I am watching searches a List (https://www.youtube.com/watch?v=FPcl1tu0gDs&t=444s). What I have here is a Future. I am wondering how do you convert a future into a List. Or is there any other workaround.
class DataSearch extends SearchDelegate<String> {
Future<List<Chorus>> chorusList = _ChorusPage.getList();
// ????????????????????? How do I convert.
#override
List<Widget> buildActions(BuildContext context) {
// actions for app bar
return [IconButton(icon: Icon(Icons.clear), onPressed: () {})];
}
#override
Widget buildLeading(BuildContext context) {
// leading icon on the left of the app bar
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {});
}
#override
Widget buildResults(BuildContext context) {
// show ssome result based on the selection
throw UnimplementedError();
}
#override
Widget buildSuggestions(BuildContext context) {
/*
final suggestionList = query.isEmpty ? recentChorus : chorus;
return ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(suggestList[chorus]),
),
itemCount: suggestionList.length,
);
// show when someone searches for
*/
}
}
In my opinion you should set your chorusList and call somewhere your getList method with the .then method store the value inside your chorusList.
List<Chorus> chorusList;
_ChorusPage.getList().then((value) => chorusList);