How to Create Custom Image Markers from a URL in Flutter Google Maps (with Caching)
Building a CustomMarker Helper
To create custom image markers in Flutter Google Maps, we'll utilize the CustomMarker
class. This class contains a method called buildMarkerFromUrl
, which will abstract away all the logic.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class CustomMarker {
static Future<Marker?> buildMarkerFromUrl({
required String id,
required String url,
required LatLng position,
int? width,
int? height,
Offset offset = const Offset(0.5, 0.5),
VoidCallback? onTap,
}) async {
var icon = await getIconFromUrl(
url,
height: height,
width: width,
);
if (icon == null) return null;
return Marker(
markerId: MarkerId(id),
position: position,
icon: icon,
anchor: offset,
onTap: onTap,
);
}
static Future<BitmapDescriptor?> getIconFromUrl(String url, {
int? width,
int? height,
}) async {
Uint8List? bytes = await getBytesFromUrl(
url,
height: height,
width: width
);
if (bytes == null) return null;
return BitmapDescriptor.fromBytes(bytes);
}
static Future<Uint8List?> getBytesFromUrl(String url, {
int? width,
int? height,
}) async {
// Modify this as needed, but you need caching unless you're displaying just a few markers.
var cache = CacheManager(Config(
"markers",
stalePeriod: const Duration(days: 7),
));
File file = await cache.getSingleFile(url);
Uint8List bytes = await file.readAsBytes();
return resizeImageFromBytes(bytes, width: width, height: height);
}
static Future<Uint8List?> resizeImageFromBytes(Uint8List bytes, {
int? width,
int? height,
}) async {
Codec codec = await instantiateImageCodec(
bytes,
targetWidth: width,
targetHeight: height
);
FrameInfo fi = await codec.getNextFrame();
ByteData? data = await fi.image.toByteData(format: ImageByteFormat.png);
return data?.buffer.asUint8List();
}
}
Explanation of the Code
buildMarkerFromUrl
Function
The CustomMarker
class contains a static method called buildMarkerFromUrl
, which is responsible for creating a custom marker from a given URL. Let's go through the important parts of this method:
id
: A unique identifier for the marker.url
: The URL of the image that will be used as the marker icon.position
: The geographical position (latitude and longitude) where the marker will be placed on the map.width
andheight
: Optional parameters to specify the dimensions of the marker image.offset
: An optionalOffset
value that defines the anchor point of the marker, determining the point on the image that corresponds to the marker's position on the map.onTap
: An optional callback that will be triggered when the marker is tapped by the user.
Inside the method, we call another method called getIconFromUrl
to retrieve the marker icon as a BitmapDescriptor
from the provided URL. If the icon is successfully fetched, we create a new Marker
instance with the specified properties, including the icon, position, anchor, and onTap callback. If an error occurs during this process, we capture and log the error using Sentry, a tool for error monitoring and reporting.
resizeImageFromBytes
Function
The resizeImageFromBytes
function is a helper method within the CustomMarker
class responsible for resizing an image represented as a Uint8List
(byte array).
Let's break down the steps performed by the resizeImageFromBytes
function:
-
Input: The function takes a
Uint8List
as input, representing the image in its raw byte format. Additionally, the function also accepts optionalwidth
andheight
parameters to specify the desired dimensions for the resized image. -
Image Codec Instantiation: The function uses the
instantiateImageCodec
method from the Flutter framework to create anImageCodec
instance from the inputUint8List
. AnImageCodec
is responsible for decoding the compressed image data. -
Resizing the Image: The
ImageCodec
decodes the image, and then the function uses thegetNextFrame
method to retrieve the first frame (image) from the codec. ThetargetWidth
andtargetHeight
parameters are passed to theinstantiateImageCodec
function to specify the desired dimensions for the resized image. If only one size (with or height) is provided, it will resize that side, while maintaining the aspect ratio of the other. -
Conversion to ByteData: After obtaining the resized image frame (
FrameInfo
), the function uses thetoByteData
method to convert the image into aByteData
object. -
Output: Finally, the function returns the resized image as a
Uint8List
(byte array), which can be used to create aBitmapDescriptor
for the custom marker.
Caching using flutter_cache_manager
The flutter_cache_manager
package is used to cache files fetched from URLs, reducing the need to download the same images repeatedly and improving the performance of the application since we don't have to re-download the same map marker image for every instance of a Marker.
Here's how caching is implemented using flutter_cache_manager
in the CustomMarker
class:
- Cache Manager Configuration: A cache manager is initialized with a configuration that specifies the cache's name (in this case, "markers") and the
stalePeriod
(the duration for which the cached images remain valid before they are considered stale and may be replaced with newer versions).
var cache = CacheManager(Config(
"markers",
stalePeriod: const Duration(days: 7),
));
In this example, the cached images are considered valid for seven days before they become stale and are subject to replacement.
- Fetching and Caching: The
getBytesFromUrl
method utilizes the cache manager to get aFile
representation of the image from the cache. If the image is not found in the cache or is stale, it is fetched from the provided URL and then stored in the cache for future use.
File file = await cache.getSingleFile(url);
The cache.getSingleFile(url)
method fetches the image from the given URL and stores it in the cache. Subsequent requests for the same image will be served from the cache until the cache duration expires.
By combining the flutter_cache_manager
package with the resizeImageFromBytes
function, the CustomMarker
class ensures that the images fetched from URLs are efficiently cached and resized before being used as custom markers on the Google Maps widget. This approach helps improve the performance and user experience of the map functionality in the Flutter app.
Implementing Custom Image Markers
Now that we've written the CustomMarker
class, let's move on to using it in our Widget.
To get started, you'll need to create a Flutter project and add the necessary dependencies for Google Maps and caching. Assuming you already have a basic Flutter project set up, follow these steps:
- Add the
google_maps_flutter
andflutter_cache_manager
packages to yourpubspec.yaml
file, by running:
flutter pub add google_maps_flutter
flutter pub add flutter_cache_manager
-
Run
flutter pub get
to fetch the new dependencies. -
Import the required packages in your Dart file:
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
-
Set up your Google Maps widget with the desired initial camera position and other configurations. For this guide, we'll focus on adding custom image markers.
-
Now, let's utilize the
CustomMarker
class to create custom markers from URLs. Assume you have a list of locations, and each location has an associated image URL. Here's an example of how you can use theCustomMarker
class to add these custom markers:
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class MapWithCustomMarkers extends StatefulWidget {
_MapWithCustomMarkers
State createState() => _MapWithCustomMarkersState();
}
class _MapWithCustomMarkersState extends State<MapWithCustomMarkers> {
final Set<Marker> _markers = {};
void initState() {
super.initState();
_loadMarkers();
}
Future<void> _loadMarkers() async {
// Replace 'locations' with your list of locations containing image URLs
List<Location> locations = [
Location(id: '1', name: 'Location 1', imageUrl: 'https://example.com/image1.jpg', latitude: 37.7749, longitude: -122.4194),
Location(id: '2', name: 'Location 2', imageUrl: 'https://example.com/image2.jpg', latitude: 37.6841, longitude: -122.4109),
// Add more locations as needed
];
List<Marker> customMarkers = await Future.wait(
locations.map(
(location) async => await CustomMarker.buildMarkerFromUrl(
id: location.id,
url: location.imageUrl,
position: LatLng(location.latitude, location.longitude),
width: 100,
onTap: () {
// Handle marker tap event here
},
),
),
);
setState(() {
_markers.addAll(customMarkers.where((marker) => marker != null));
});
}
Widget build(BuildContext context) {
return GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(37.7749, -122.4194),
zoom: 12,
),
markers: _markers,
);
}
}
class Location {
final String id;
final String name;
final String imageUrl;
final double latitude;
final double longitude;
Location({
required this.id,
required this.name,
required this.imageUrl,
required this.latitude,
required this.longitude,
});
}
With this implementation, you can now display custom image markers on your Google Maps widget in Flutter, with the added benefits of caching to optimize performance.