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 and height: Optional parameters to specify the dimensions of the marker image.
  • offset: An optional Offset 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:

  1. Input: The function takes a Uint8List as input, representing the image in its raw byte format. Additionally, the function also accepts optional width and height parameters to specify the desired dimensions for the resized image.

  2. Image Codec Instantiation: The function uses the instantiateImageCodec method from the Flutter framework to create an ImageCodec instance from the input Uint8List. An ImageCodec is responsible for decoding the compressed image data.

  3. Resizing the Image: The ImageCodec decodes the image, and then the function uses the getNextFrame method to retrieve the first frame (image) from the codec. The targetWidth and targetHeight parameters are passed to the instantiateImageCodec 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.

  4. Conversion to ByteData: After obtaining the resized image frame (FrameInfo), the function uses the toByteData method to convert the image into a ByteData object.

  5. Output: Finally, the function returns the resized image as a Uint8List (byte array), which can be used to create a BitmapDescriptor 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:

  1. 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.

  1. Fetching and Caching: The getBytesFromUrl method utilizes the cache manager to get a File 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:

  1. Add the google_maps_flutter and flutter_cache_manager packages to your pubspec.yaml file, by running:
flutter pub add google_maps_flutter
flutter pub add flutter_cache_manager
  1. Run flutter pub get to fetch the new dependencies.

  2. 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';
  1. 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.

  2. 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 the CustomMarker 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.