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

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.

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.

Avoid ListView's unwanted refresh

As the following animation displays, when I tap one of the list items that StreamBuilder() is querying, it shows the items data on the right darker container (it's always Instance of '_JsonQueryDocumentSnapshot'). But at the same time in each tap, the whole list is refreshing itself, which is not very cost-effective I believe.
How can I avoid this unwanted refresh?
Answers with GetX state management dependency are also welcome.
class Schedule extends StatefulWidget {
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
var _chosenData;
#override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db.collection('Schedule').where('date', isGreaterThan: _yesterday).limit(10).orderBy('date').snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'], style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () {
setState(() {_chosenData = data;});
},
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: Text('$_chosenData'),
),
),
],
);
}
}
To me the easiest solution would be just make it stateless and use a Getx class.
class ScheduleController extends GetxController {
var chosenData;
void updateChosenData(var data) {
chosenData = data;
update();
}
}
And your Schedule.dart would look like this:
class Schedule extends StatelessWidget {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
#override
Widget build(BuildContext context) {
final controller = Get.put(ScheduleController());
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'],
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () => controller.updateChosenData(data), // calls method from GetX class
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: GetBuilder<ScheduleController>(
builder: (controller) => Text('${controller.chosenData}'), // only this rebuilds
),
),
),
],
);
}
}
This way the listview.builder never rebuilds, only the Text widget directly inside the GetBuilder gets rebuilt when you selected a different ListTile.
Calling setState() notifies the framework that the state of Schedule has changed, which causes a rebuild of the widget and so your StreamBuilder.
You could move your stream logic to an upper level of the widget tree. So, setState() will not trigger a rebuild of StreamBuilder.
class ParentWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('Schedule')
.where(
'date',
isGreaterThan: DateTime.now().subtract(Duration(days: 1)),
)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
return Schedule(snapshot: snapshot); // Pass snapshot to Schedule
},
);
}
}
Another approach would be using Stream.listen in initState() which is called once. This way your stream won't be subscribed for each time setState() is called.
...
late StreamSubscription<QuerySnapshot> _subscription;
#override
void initState() {
_subscription = _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots()
.listen((QuerySnapshot querySnapshot) {
setState(() {
_querySnapshot = querySnapshot;
});
});
super.didChangeDependencies();
}
#override
void dispose() {
_subscription.cancel(); // Cancel the subscription
super.dispose();
}
...

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

How can I create ExpansionPanelList from model from API on flutter?

I am trying to create on flutter a dynamic ExpansionPanelList which each ExpansionPanel is provided by my API REST service.
I have connected my API with my app and I can list all the Products:
ProductosProvider:
List<ProductoModel> _productos = new List();
final _productosStreamController = StreamController<List<ProductoModel>>.broadcast();
Function(List<ProductoModel>) get productosSink => _productosStreamController.sink.add;
Stream<List<ProductoModel>> get productosStream => _productosStreamController.stream;
List products method:
Future<ProductList> listaProductos() async {
// Call API
final resp = await http.get(_url);
final decodedData = json.decode(resp.body);
final productos = new ProductList.fromJsonList( decodedData );
_productos.addAll(productos.items);
productosSink( _productos );
return productos;
}
So, calling this listaProductos method I can list all the products from the remote database.
On my Widget page:
class _CartaPageState extends State<CartaPage> {
// Create provider
ProductosProvider productosProvider;
...
#override
Widget build(BuildContext context) {
// Initialize provider
productosProvider = new ProductosProvider();
// Listen data
final productos = productosProvider.listaProductos();
);
Here is where I have the issues, I would like to create the ExpansionPanelList using a StreamBuilder
Widget _listaProductos( Buildcontext context ) {
return StreamBuilder(
// Suscribe to stream
stream: productosProvider.productosStream,
builder: ( context , AsyncSnapshot<List<ProductoModel>> snapshot) {
if ( snapshot.hasData ) {
final productos = snapshot.data ?? [];
// Creamos la lista de items
// This method transform Producto List to Item List
productosItems = productosProvider.productosToList();
return ExpansionPanelList(
animationDuration: Duration( milliseconds: 500 ),
expansionCallback: (int index, bool isExpanded) {
setState(() {
productosItems[index].isExpanded = !isExpanded;
});
},
children: productosItems.map<ExpansionPanel>((Item item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
);
},
body: ListTile(
title: Text(item.expandedValue),
),
isExpanded: item.isExpanded,
);
}).toList()
...
}
On this way, my productosItem is refreshing all the time and the state is not updated, so the ExpansionPanel is never open/collapsed.
I don't know if I am declaring the provider on the right place, if the productosToList must be out of the provider...
Thank you in advance

Displaying Snackbar inside a SearchDelegate

I am using a SearchDelegate and want to display a Snackbar when the user tries to perform a search with an empty query. I've tried returning Scaffold widgets from both the buildSuggestions and buildResults methods and then using a Builder / GlobalKey inside the buildResults method to display a message to the user if the search query has a length of zero. However this leads to the Scaffold's state being updated during the render method which throws an exception. Has anyone dealt with a similar challenge? Seems like a common use case that you would want to display a Snackbar inside your search delegate, yet I can't seem to fathom an easy way to do it.
Figured it out
class DataSearch extends SearchDelegate<String> {
List<Drug> drugList = new List<Drug>();
DataSearch(Future<List<Drug>> listDrugName) {
this.drugListFuture = listDrugName;
}
#override
List<Widget> buildActions(BuildContext context) {
// actions for app bar
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = "";
})
];
}
#override
Widget buildLeading(BuildContext context) {
// leading icon on the left of app bar
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow, progress: transitionAnimation),
onPressed: () {
close(context, null);
});
}
#override
Widget buildResults(BuildContext context) {
// show result from selection
return null;
}
#override
Widget buildSuggestions(BuildContext context) {
return new FutureBuilder(
future: db.getDrugEntries(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData || snapshot.data.length < 1) {
return new Center(
child: new LoadingIndicator(Constants.msgLoading));
} else {
drugList = snapshot.data;
// show when user searches for something
final suggestionList = query.isEmpty
? drugList
: drugList
.where((r) =>
(r.drugId.toLowerCase())
.contains(query.toLowerCase()) ||
(r.fullDrugName.toLowerCase())
.contains(query.toLowerCase()) ||
(r.otherName.toLowerCase())
.contains(query.toLowerCase()) ||
(r.tradeName.toLowerCase())
.contains(query.toLowerCase()))
.toList();
return ListView.builder(
itemBuilder: (context, index) {
String drugName = suggestionList[index].genericName;
String drugId = suggestionList[index].drugId;
int queryIndex = drugName.indexOf(query);
if (queryIndex == -1) {
queryIndex = 0;
}
int queryIndexEnd = queryIndex + query.length;
return Container(button//...onTap:_launchExtraContent(context,drugId);
},
itemCount: suggestionList.length,
);
}
});
}
_
_launchExtraContent(BuildContext context, StringtheFileName) async {
try {
//......
} catch (e) {
_showSnackBar(context,'ERROR: Unable to retrieve file please submit a bug report');
}
}
void _showSnackBar(BuildContext context, String text) {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text(
text,
textAlign: TextAlign.center,
),
backgroundColor: Colors.red,
));
}
}