ListView items reinitializing on List length change - flutter

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.

Related

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.

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

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

Flutter Passing Data:: Getter not found

I'm trying to make a splash screen where the user chooses a city, with each city having its own API via url_items variable to access its data to populate the ListViews in the second screen.
When I call the data in the second screen, via http.Response response = await http.get(url_items); I get an error Getter not found: url_items
How do I do the Getter properly?
class Splash extends StatefulWidget {
_SplashState createState() => _SplashState();
}
class _SplashState extends State<Splash> {
String dropdownValue = 'NY';
String city = 'NY';
String url_items = 'https://ny.com/items';
String url_stores = 'https://ny.com/stores';
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
ListTile(
title: DropdownButton<String>(
value: dropdownValue,
onChanged: (String newValue) {
setState(() {
dropdownValue = newValue;
city = newValue;
if (city == 'NY'){url_items = 'https://ny.com/items';} else {url_items = 'https://chicago.com/items';}
});
},
items: <String>['NY', 'Chicago'].map<DropdownMenuItem<String>>(
(String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}
).toList(),
),
),
RaisedButton(
child: Text('View Items'),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => Items(url_items: url_items, url_stores: url_stores, city: city)
),
);
},
),
],
),
);
}
}
class Items extends StatelessWidget {
var url_items="";
var url_stores="";
var city="";
Items({Key key, this.url_items, this.url_stores, this.city}) : super(key: key);
static Future<List<Item>> getItems() async {
http.Response response = await http.get(url_items);
String data = response.body;
List collection = json.decode(data);
Iterable<Item> _items = collection.map((_) => Item.fromJson(_));
return _items.toList();
}
Stream<List<Item>> get itemListView => Stream.fromFuture(getItems());
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: itemListView,
builder: (BuildContext context, AsyncSnapshot<List<Item>> snapshot) {
List<Item> items = snapshot.data;
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
Item item = items[index];
return ListTile(
title: Html(data: item.name),
subtitle: Html(data: item.userName),
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => ItemDetail(item.name, item.userName..),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
);
}
}
),
);
}
}
Instance variables/members cannot be accessed from a static method. so try changing
static Future<List<Item>> getItems() async {...}
to
Future<List<Item>> getItems() async {...}

How do you display detail page when the respective listview item is tapped

I am trying to pass the id of listview item to a different page with so it can populate a listview with items referenced by that id:
return SideHeaderListView(
itemCount: order.length,
itemExtend: 230.0,
headerBuilder: (BuildContext context, index) {
return new SizedBox(
child: Text(order[index]['Month'],
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 30.0),),);
},
hasSameHeader: (int a, int b) {
return order[a]['Month'] ==
order[b]['Month'];
},
itemBuilder: (BuildContext context, index) {
String value;
return new GestureDetector(
//onLongPress: _showPopupMenu(),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) =>
MyItemList(orders: order[index],)
));
},
This is the code that passes the id and inserts the id into the table:
class MyItemList extends StatelessWidget{
final Map<String, dynamic> orders;
// final Items items;
MyItemList({this.orders});
}
FlatButton(
color: Colors.lightGreen,
child: Text('ADD',style: TextStyle(color: Colors.white),),
onPressed: () async {
OrderProvider.insertItem({
'ref':MyItemList().orders,
'Quantity': _quantityController.text,
'Unit': getDropDownItem().toString(),
'TypeOfProduct': _typeController.text,
'UnitPrice':_unitpriceController.text,
'TotalPrice':getTotalPrice()
});
This is my provider class method:
return await db.rawQuery(
"select * from Items where ref = ${MyItemList(orders: id,)}",);
}
First I can suggest you to use a StateFul widget Instead of a StateLess widget in your MyItemList Class, then In the InitState you can get the data based in the respective Id, for example:
class MyItemList extends StatefulWidget {
// final Map<String, dynamic> orders;
// instead of receive a Map receive only the OrderID
final int orderId;
MyItemList({this.orderId});
#override
_MyItemListState createState() => _MyItemListState();
}
class _MyItemListState extends State<MyItemList> {
List<dynamic> itemsToSave;
#override
void initState() {
super.initState();
getItems(widget.orderId);
}
#override
Widget build(BuildContext context) {
return Container();
}
getItems(int orderId) async {
itemsToSave = await yourProvider.getItems(orderId);
print(itemsToSave);
}
}
And in your provider is better to use the whereArgs for example:
await db.rawQuery(
'SELECT * FROM Items WHERE ref = ?',
[order],
),
And if you have a map of orders, is better to iterate the map and get the data of each item separately or check the IN Operator documentation for SQLite for more explanation you can check this post.
Hope it helps.