Building an application with Flutter and liking it so far, but an annoying problem is bugging me.
I have a "main" Stateful Widget with a list of shops in attribute, a search bar, a "child" widget which is either a map or a list, and we can switch between views by clicking on a button. There is an Inherited widget wrapped inside the main widget so we can get state of the main widget and thus the list.
I have a listener on the search bar :
void updateNameFilter(){
setState(() {
FiltersService.nameSearch=_searchTextController.text;
});}
It works perfectly and smoothly with the list widget (which has a FutureBuilder) where we filter the list.
child: TruckList(snapshot.data.where((Truck t)=>FiltersService.shouldDisplay(t)).toList(), _handleRefresh),
However, for the maps widget, we use the Google Maps plugin. At the creation of the map, we add a marker on it for every item of the list that we should display :
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
//Get the list of trucks from parent widget
List<Truck> list= await TrucksMainWidget.of(context).trucksList;
//Iterate on all trucks to add them on the map
list.where((Truck t) => FiltersService.shouldDisplay(t)).forEach((Truck t) => _addTruckMarker(t)); }
Then, everytime the value in the search bar changes, it rebuild the whole widget and it is very unefficient, with an unesthetic black screen at rebuild. Moreover, it works once in a few times : most of the time, the widget is not even rebuilding, making the results of the search wrong.
One strange thing is that when I launch the app in debug mode and I set a breakpoint on the "build" method of the map widget, it works as expected.
What I would like to do is, when the list changes in the main widget : -If we are on the list widget, rebuild -If we are on the Google maps widget : trigger a function that will erase all markers and re-add them given the new list, WITHOUT rebuilding the whole widget.
I am sure there is a way to do this, but I have been trying for a few days now and I can't find a way. Maybe someone can help me ? Thanks a lot ! :)
EDIT : Here is my code :
Inherited Widget
class _TrucksInherited extends InheritedWidget {
_TrucksInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final _TrucksMainWidgetState data;
@override
bool updateShouldNotify(_TrucksInherited oldWidget) {
return true;
}
}
class TrucksMainWidget extends StatefulWidget {
final Widget child;
TrucksMainWidget({this.child});
@override
_TrucksMainWidgetState createState() => _TrucksMainWidgetState();
static _TrucksMainWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_TrucksInherited)
as _TrucksInherited)
.data;
}
}
class _TrucksMainWidgetState extends State<TrucksMainWidget> {
final Icon _mapIcon = new Icon(
FontAwesomeIcons.map,
color: Color(0xFF666666),
);
final Icon _listIcon = new Icon(
FontAwesomeIcons.thLarge,
color: Color(0xFF666666)
);
Widget _trucksListWidget;
Widget _trucksMapWidget;
TextEditingController _searchTextController;
Icon _switchIcon;
Widget _widgetView;
Future<List<Truck>> _trucksList;
@override
void initState() {
super.initState();
_trucksList = TruckService.getAll();
_trucksListWidget = new TrucksListWidget();
_trucksMapWidget = new TrucksMapWidget();
_searchTextController = new TextEditingController();
_searchTextController.addListener(updateNameFilter);
_switchIcon = _mapIcon;
_widgetView = _trucksListWidget;
}
Future<List<Truck>> get trucksList => _trucksList;
@override
void dispose(){
_searchTextController.removeListener(updateNameFilter);
_searchTextController.dispose();
super.dispose();
}
void updateNameFilter(){
setState(() {
FiltersService.nameSearch=_searchTextController.text;
});
}
void refresh() {
setState(() {
_trucksList = TruckService.getAll();
});
}
void _onSwitchView() {
setState(() {
if (_switchIcon.icon == FontAwesomeIcons.map) {
_switchIcon = _listIcon;
_widgetView = _trucksMapWidget;
} else {
_switchIcon = _mapIcon;
_widgetView = _trucksListWidget;
}
});
}
_openFiltersDialog() async {
bool shouldUpdate = await showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return FiltersAlertBox();
},
);
if (shouldUpdate != null && shouldUpdate) {
setState(() {});
}
}
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
backgroundColor: Colors.white,
title: Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Directionality(
textDirection: Directionality.of(context),
child: TextField(
style: new TextStyle(color: Colors.black, fontSize: 16.0),
decoration: InputDecoration(
hintText: 'Recherche',
hintStyle: new TextStyle(color: Color(0xFF999999)),
fillColor: Color(0xFFF6F6F6),
filled: true,
border: InputBorder.none,
prefixIcon: Icon(Icons.search, color: Color(0xFF999999))),
autofocus: false,
controller: _searchTextController,
)),
),
actions: <Widget>[
IconButton(
icon: Icon(
FontAwesomeIcons.slidersH,
color: Color(0xFF666666),
),
onPressed: _openFiltersDialog,
),
IconButton(
icon: _switchIcon,
onPressed: _onSwitchView,
)
],
),
body: _TrucksInherited(
data: this,
child: _widgetView,
));
}
}
List Widget (OK)
class TrucksListWidget extends StatefulWidget {
TrucksListWidget();
_TrucksListWidgetState createState() => new _TrucksListWidgetState();
}
class _TrucksListWidgetState extends State<TrucksListWidget>{
Future<List<Truck>> _handleRefresh() async{
final state = TrucksMainWidget.of(context);
state.refresh();
return state.trucksList;
}
Widget build(BuildContext context){
final state = TrucksMainWidget.of(context);
return FutureBuilder<List<Truck>>(
future: state.trucksList,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasData) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: TruckList(snapshot.data.where((Truck t)=>FiltersService.shouldDisplay(t)).toList(), _handleRefresh),
);
}else if(snapshot.hasError){
return Center(
child: Text('${snapshot.error}'),
);
}
return Center(
child: CircularProgressIndicator(),
);
}
);
}
}
Map Widget (KO)
class TrucksMapWidget extends StatefulWidget {
_TrucksMapWidgetState createState() => new _TrucksMapWidgetState();
}
class _TrucksMapWidgetState extends State<TrucksMapWidget> {
Future<Position> userPosition;
GoogleMapController mapController;
Map<Marker, Truck> allMarkers = {};
StreamSubscription subscription;
void initState(){
super.initState();
initPosition();
subscription = LocationService.ctrl.stream.listen((Future<Position> p)=>userPosition=p); //Listening LocationService's stream for a change of the user position
}
void dispose(){
super.dispose();
subscription.cancel();
}
void initPosition() async{
userPosition = LocationService.getUserPosition();
}
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
//Get the list of trucks from parent widget
List<Truck> list= await TrucksMainWidget.of(context).trucksList;
//Iterate on all trucks to add them on the map
list.where((Truck t) => FiltersService.shouldDisplay(t)).forEach((Truck t) => _addTruckMarker(t));
}
void _addTruckMarker(Truck truck) async {
Marker marker = await mapController.addMarker(MarkerOptions(
icon: BitmapDescriptor.fromAsset(
truck.situation.isOpen ? 'assets/images/open_marker.png' : 'assets/images/close_marker.png',
),
infoWindowText: InfoWindowText(truck.name, truck.situation.address),
position: LatLng(truck.situation.position.lat,
truck.situation.position.long),
consumeTapEvents: true,
));
allMarkers[marker] = truck; //Linking the marker to its truck in the Map
}
Widget build(BuildContext context) {
return FutureBuilder(
future:userPosition,
builder: (context, snapshot){
if(snapshot.connectionState==ConnectionState.done){
return _getMap(snapshot.data);
}else{
return Center(
child : CircularProgressIndicator(),
);
}
},
);
}
Widget _getMap(Position pos){
return GoogleMap(
onMapCreated: _onMapCreated,
options: GoogleMapOptions(
myLocationEnabled: true,
cameraPosition: CameraPosition(
target: LatLng(pos.lat, pos.long), //Starting position at user
zoom: 8.0,
),
),
);
}
}