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.widthandheight: Optional parameters to specify the dimensions of the marker image.offset: An optionalOffsetvalue 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
Uint8Listas input, representing the image in its raw byte format. Additionally, the function also accepts optionalwidthandheightparameters to specify the desired dimensions for the resized image. -
Image Codec Instantiation: The function uses the
instantiateImageCodecmethod from the Flutter framework to create anImageCodecinstance from the inputUint8List. AnImageCodecis responsible for decoding the compressed image data. -
Resizing the Image: The
ImageCodecdecodes the image, and then the function uses thegetNextFramemethod to retrieve the first frame (image) from the codec. ThetargetWidthandtargetHeightparameters are passed to theinstantiateImageCodecfunction 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 thetoByteDatamethod to convert the image into aByteDataobject. -
Output: Finally, the function returns the resized image as a
Uint8List(byte array), which can be used to create aBitmapDescriptorfor 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
getBytesFromUrlmethod utilizes the cache manager to get aFilerepresentation 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_flutterandflutter_cache_managerpackages to yourpubspec.yamlfile, by running:
flutter pub add google_maps_flutter
flutter pub add flutter_cache_manager
-
Run
flutter pub getto 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
CustomMarkerclass 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 theCustomMarkerclass 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.
