I am building a Flutter app that allows users to create locations and then have those locations uploaded to a database and shown to other users on a map. I have gotten the database and displaying of locations to work properly, but I want the map, a Google Map, to dynamically reload after the user has created a location and is returned to the map screen.
Right now, I am using Firebase Realtime Database to listen for changes and trying to update the map accordingly after the user has clicked on the button to create a new location. Here is the code for that:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const LocationCreation()),
).then((valuef) {
locationsRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
if (event.snapshot.value != null) {
allMarkers = {};
final map = data as Map<dynamic, dynamic>;
map.forEach((key, value){
print(value['name']);
allLocations.add(value);
// Code to create a Marker object called marker
}
);
setState(() {
allMarkers.add(marker);
});
});
}
});
I know this code is reached, because the print statement is being called and the correct names are being printed out, including the name of the newly created location. However, the map screen is not being updated with the new locations and their markers.
The Google Map is being built in the build function as so:
GoogleMap(
scrollGesturesEnabled: true,
onMapCreated: (onCreated),
initialCameraPosition: CameraPosition(
target: _center,
zoom: 11.0,
),
markers: allMarkers,
),
And here is the onCreated function:
print('here again');
mapController = controller;
final snapshot = await locationsRef.get();
final map = snapshot.value as Map<dynamic, dynamic>;
setState(() {
allMarkers.clear();
map.forEach((key, value) {
allLocations.add(value);
//Code to make a Marker called marker
allMarkers.add(marker);
});
});
The print statement is called once upon initial building of the app, but is not called again after a new location has been created and the user is returned to the map screen.
allMarkers is not being initialized within the build function, which I found was a common problem for others.
Each time I try to make a new location, the database will store the new location and the print statement within the listen for new database event will be called, but it seems that Flutter does not detect a change to allMarkers. I have also tried allMarker.clear(), but the problem is the same. I have also tried to move the call to setState(), but nothing has worked.
Related
I'm using the flutter GoogleMap widget in my app to which I am adding markers :
Generated by mapping my bloc state.locationPoints
I've successfully implemented different appearances for those BitmapDescriptor markers through my customMarker() method
Now I'd also like to define a separate appearance for the same marker depending on wether it is active (tapped or not)
I've tried doing so using setState but it changes all of my markers appearances while I only want the current one to be changed
BlocBuilder<LocationBloc, LocationState>(
builder: (context, state) {
var markers = <Marker>{};
if (state is LocationLoaded) {
markers = state.locationPoints!.map((locationPoint) {
return Marker(
onTap: () {
},
icon: customMarker(locationPoint),
position: LatLng(
locationPoint.coordinates.latitude,
locationPoint.coordinates.longitude,
),
);
}).toSet();
}
return GoogleMap(
markers: markers,
);
},
);
You have to find the specific marker in your set of markers. If you provide your marker with an ID that contains some information from locationPoint, you could do something like this (in my case I use my place id as markerId):
final markers = state.markers
.map(
(marker) => marker.markerId.value == state.selectedPlace.id
? marker.copyWith(
iconParam: state.saveSelectedMarkerIcon,
)
: marker,
)
.toSet();
In Flutter I use
google_maps_flutter, google_directions_api and flutter_polyline_points packages to have a map with the following functionalities;
Drawing routes between current location and destination points, get distance and durations between them, driver must be notified to take left/right while driving to the destination.
I have done these all, but when current location keeps updating on the map I'm calling direction api which return lots of data like legs and steps this api call is almost every second and google will charge me a lot.
Is anybody faced to same issue, I really appreciate a help.
There is part of my codes I have done so far
void _getCurrentLocation(context) {
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black45,
pageBuilder: (BuildContext buildContext, Animation animation,
Animation secondaryAnimation) {
return Center(
child: Container(
width: MediaQuery.of(context).size.width - 10,
height: MediaQuery.of(context).size.height - 80,
padding: EdgeInsets.all(20),
color: Tingsapp.transparent,
child: CurrentLocation(),
),
);
}).then((location) {
if (location != null) {
_addMoverMarker(location, 'mover');
//updateFirebase(location);
_animateCameraToCurrentLocation(location);
}
});
}
destination point are already set.
In the above code I get user current location and add a marker as bellow
void _addMoverMarker(newLocationData, String id) async {
Uint8List imageData = await getMarker();
//LatLng latlng = LatLng(newLocationData.latitude, newLocationData.longitude);
LatLng latlng = LatLng(_moverLatitude!, _moverLongitude!);
MarkerId markerId = MarkerId(id);
Marker marker = Marker(
markerId: markerId,
position: latlng,
zIndex: 2,
icon: BitmapDescriptor.fromBytes(imageData),
infoWindow: InfoWindow(
title: "Mover location",
),
);
markers[markerId] = marker;
circle = Circle(
circleId: CircleId("circle"),
radius: 20,
zIndex: 1,
center: latlng,
strokeColor: Colors.orange.withAlpha(60),
fillColor: Colors.orange.withAlpha(300),
);
_getMoverPolyline(newLocationData);
}
and here I animate the camera
_animateCameraToCurrentLocation(newLocationData) {
if (_locationSubscription != null) {
_locationSubscription!.cancel();
}
_locationSubscription =
_locationTracker.onLocationChanged.listen((newLocationData) {
if (_mapController != null) {
_addMoverMarker(newLocationData, 'mover');
_animateCamera(_moverLatitude, _moverLongitude, 16.0);
//updateFirebase(newLocationData);
}
});
}
when I draw the polyline I call directions api here my problem starts
_getMoverPolyline(LocationData locationData) async {
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
mapKey,
PointLatLng(_originLatitude!, _originLongitude!),
PointLatLng(_moverLatitude!, _moverLongitude!),
//PointLatLng(locationData.latitude!, locationData.longitude!),
travelMode: TravelMode.driving,
);
if (result.points.isNotEmpty) {
moverPolylineCoordinates = [];
result.points.forEach((PointLatLng point) {
moverPolylineCoordinates.add(LatLng(point.latitude, point.longitude));
});
}
_addMoverPolyLine();
_getDirections(_moverLatitude, _moverLongitude, _originLatitude,
_originLongitude).then((data){
_updateData(data);
});
}
_getDirections(_moverLatitude, _moverLongitude, _originLatitude, _originLongitude) async {
Api api = Api();
var res = await api.getDirections(
_moverLatitude, _moverLongitude, _originLatitude, _originLongitude);
var jsonData = jsonDecode(res.body);
print(jsonData['routes'][0]['legs'][0]);
return jsonData['routes'][0]['legs'][0];
}
In the above code _getDirections method gets calling every second.
Isn't possible to call directions api one time?
_updateData method update data like tern right/left or Head south on my map
No I don't think that it's good practice to keep calling the API. And even in the google_directions_api documentation, they say,
Note: This service is not designed to respond in real time to user input.
And to answer your main question..
Don't call the API every time the location changes, instead call it once using the current location. And the response contains everything you need to navigate the user. Check the maneuver key inside each step of a leg.
And you should only use the location subscription to update the current location and animate the camera on the map. Use the getLocation() method to get the current location before starting the trip and use that location to generate the route. Store that route in a variable and use the data inside the route response to display whatever you want.
It´s not a good practice to constantly call directionsAPI, for the good of your pocket and to follow the recommendations of google.
If you are creating a Navigation app you first need to call the Api when the user set a destination, capture de coordinates (overview_polyline) and after that if the user goes off the road you can use maps toolkit to determine when this occurs and after that call again directionsAPI.
Objective:
To be able to show custom markers from dev only and disable google maps' default markers.
Description:
I am trying to put markers in GoogleMap from google_maps_flutter plugin but Google already has its own markers so it is getting in the way of the markers that I am trying to add. Is there any way to just show the map and add user-defined markers only? If not is it possible to minimize the number of markers shown by default map?
Just looked around and found some possible fix.
Seems like we can generate map style from this website:
Styling Wizard.
From there I toned down landmarks and then I was able to remove markers using this:
final String mapStyle =
await rootBundle.loadString('assets/map/map_style.txt');
//Set it on mapcontroller after map is created.
onMapCreated: (GoogleMapController controller) {
if (_controller.isCompleted) {
return;
}
controller.setMapStyle(mapStyle);
_controller.complete(controller);
},
// create a function to create custom marker
Future<BitmapDescriptor> createCustomMarkerBitmap() async {
Uint8List? data = await getBytesFromAsset("assets/icons/map_marker.png", 100);
return BitmapDescriptor.fromBytes(data!);
}
// then call the function to create a custom marker.
BitmapDescriptor? _marker = await createCustomMarkerBitmap();
final Marker marker = Marker(
markerId: _markerId,
position: _position,
icon: _marker, // use the marker
infoWindow: _infoWindow,
onTap: () {},
);
This question already has an answer here:
Flutter: Why setState(( ) { }) set data again and again
(1 answer)
Closed 2 years ago.
I have a flutter project that makes an http request to gather json of inventory items then render it on screen in a list view. When a user scrolls to the bottom it then loads more inventory by triggering another http call. However my initial HTTP call is being called multiple times. And I am not sure how to handle that.
The result I get is my finish print statement just triggers continuously. At first I thought I had it working because it does load the inventory to my list view. But then I noticed It just keeps loading the same data into my list view non stop. When I added that finish print statement it became clear that it is actually continuously making that http call non stop.
Id like to have it only make the call once, then make a new call again when the user scrolls to bottom.
Any tips will help thank you.
Here is my code.
Network Code
Future <List <dynamic>> fetchInventory() async{
dynamic response = await http.get('https:somelink.com',
headers: {'x-api-key': 'mykey'},);
var inventory = List<dynamic>();
//if 200 response is given then set inventory var to inventoryJson value from the http call
if (response.statusCode == 200){
dynamic inventoryJson = jsonDecode(response.body);
inventory = inventoryJson['page'];
print('finish');
}
//Inventory is returned
return inventory;
}
}
Here is my how I am using that code in my main file
class _WelcomeScreenState extends State<WelcomeScreen> {
//Create Network Helper Obj
NetworkHelper networkHelper = NetworkHelper();
//invenrtory List is set to empty
var inventory = [];
var _controller = ScrollController();
#override
void initState() {
super.initState();
// set up listener here
_controller.addListener(() {
if (_controller.position.pixels == _controller.position.maxScrollExtent) {
// Perform your task
//This method fetches the data from the fetchInventory method
print('at bottom');
networkHelper.fetchInventory().then((value) {
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory = value;
});
});
}
});
}
#override
Widget build(BuildContext context) {
//Used to format currency shown in view
var currencyFormat = NumberFormat('##,###.00', 'en_US');
//This method fetches the data from the fetchInventory method
networkHelper.fetchInventory().then((value){
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory.addAll(value);
});
});
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0.0,
title: AppBarTitle(),
),
body: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 5.0, bottom: 25.0),
child: ListView.builder(
controller: _controller,
itemCount: inventory.length,
itemBuilder: (context, index){
enter code here
enter code here
The responsibility of the build method is to construct a widget tree, and this method may be called repeatedly by the framework whenever it thinks that the screen might have changed and need to be re-rendered. As a result, its important that as little work as possible is done here. Remote calls over the network should be avoided, and implementers should try not to call setState as this will cause the UI rendering to loop.
Listening to the scroll controller as demonstrated is a good way of 'Loading more', what is missing is an initial load of the inventory in the initState() method.
So, you should move the call to networkHelper.fetchInventory() from the build method to the scrollcontroller callback (as it adds to the inventory) and move the existing call to networkHelper.fetchInventory() that initialises the inventory from the scrollcontroller callback to the start of the initState method.
I am currently working on a project with a team where I implemented a barcode scanner that is going to be a widget on a "search" page. The issue I am currently facing is that I use the barcode to generate a list of ingredients that I want to include in the search but I don't know how to return that information to the search page. I have tried two methods so far:
I tried creating a member variable of the scanner class that I would then access on the search page when I need it but because my function that returns the list is a part of the private state class that I am unsure how to access from the public class. This is the method I would prefer to solve.
I have tried using the Navigator class to push and pop the information from the seperate screens but the issue is the barcode scanner is automatically closed once a barcode is scanned so I can't pop from the stack or else it will leave the search page and go back to whatever page was previous.
Here is the code I have. The first is the function that opens the barcode scanner, scans a barcode and creates an ingredient list that I return. This 'test' list is what I'd ideally like to set as a class member for the public portion of the class not the private state class.
Future<void> scanBarcodeNormal(BuildContext context) async {
String barcodeScanRes;
List<String> test;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
barcodeScanRes = await FlutterBarcodeScanner.scanBarcode(
"#ff6666", "Cancel", true, ScanMode.BARCODE);
} on PlatformException {
barcodeScanRes = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return null;
//Call the backend with the barcode to return the Bread Crumb list
List<String> breadCrumbs = await BackendRequest.barcode("089836187635", "42e96d88b6684215c9e260273b5e56b0522de18e");
//If the backend does not return us anything this displays a popup
if(breadCrumbs == null){
showDialog(
context: context,
builder: (BuildContext context) => CustomDialog(
title: "Uh Oh",
description:
"Barcode not found in our database, please try entering the item manually",
buttonText: "Okay",
),
);
}
else{
setState(() {
_scanBarcode = breadCrumbs.toString();
});
//Check the breadcrumbs for usable ingredients
test = await getIngredients(breadCrumbs, "42e96d88b6684215c9e260273b5e56b0522de18e");
}
setState(() {
_itemName = test[0].toString();
});
//Navigator.pop(context, test);
}
Here is where I add my widget to the search page. All I do is add the widget and the build function within my scanner class handles onPressed() functionality.
decoration: InputDecoration(
labelText: "Input an Ingredient",
hintText: "Search",
prefixIcon: Icon(Icons.search),
suffixIcon: ScanButton(),
you can use provide consumer pattern in which you can put all this page into one parent theme child and than you can access variable and all other stuff.
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
other wise you can also use block patten
https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
or if you dont want to use that in that case you can make one common class and declare one static varible in that and set varible on result after scaning in completed and use when ever you want