Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

import 'page.dart';

class BitmapRegistryPage extends GoogleMapExampleAppPage {
const BitmapRegistryPage({Key? key})
: super(const Icon(Icons.speed), 'Bitmap registry', key: key);

@override
Widget build(BuildContext context) {
return const _BitmapRegistryBody();
}
}

// How many markers to place on the map.
const int _numberOfMarkers = 500;

class _BitmapRegistryBody extends StatefulWidget {
const _BitmapRegistryBody();

@override
State<_BitmapRegistryBody> createState() => _BitmapRegistryBodyState();
}

class _BitmapRegistryBodyState extends State<_BitmapRegistryBody> {
final Set<Marker> _markers = <Marker>{};
int? _registeredBitmapId;

@override
void initState() {
super.initState();

_registerBitmap();
}

@override
void dispose() {
_unregisterBitmap();
super.dispose();
}

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 2 / 3,
child: GoogleMap(
markers: _markers,
initialCameraPosition: const CameraPosition(
target: LatLng(0, 0),
zoom: 1.0,
),
),
),
MaterialButton(
onPressed: () async {
// Add markers to the map with a custom bitmap as the icon.
//
// To show potential performance issues:
// * large original image is used (800x600px, ~330KB)
// * bitmap is scaled down to 64x64px
// * bitmap is created once and reused for all markers. This
// doesn't help much because the bitmap is still sent to the
// platform side for each marker.
//
// Adding many markers may result in a performance hit,
// out of memory errors or even app crashes.
final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor();
_updateMarkers(bitmap);
},
child: const Text('Add $_numberOfMarkers markers, no registry'),
),
MaterialButton(
onPressed: _registeredBitmapId == null
? null
: () {
// Add markers to the map with a custom bitmap as the icon
// placed in the bitmap registry beforehand.
final RegisteredMapBitmap registeredBitmap =
RegisteredMapBitmap(id: _registeredBitmapId!);
_updateMarkers(registeredBitmap);
},
child: const Text('Add $_numberOfMarkers markers using registry'),
),
],
),
);
}

/// Register a bitmap in the bitmap registry.
Future<void> _registerBitmap() async {
if (_registeredBitmapId != null) {
return;
}

final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor();
_registeredBitmapId =
await GoogleMapBitmapRegistry.instance.register(bitmap);

// If the widget was disposed before the bitmap was registered, unregister
// the bitmap.
if (!mounted) {
_unregisterBitmap();
return;
}

setState(() {});
}

/// Unregister the bitmap from the bitmap registry.
void _unregisterBitmap() {
if (_registeredBitmapId == null) {
return;
}

GoogleMapBitmapRegistry.instance.unregister(_registeredBitmapId!);
_registeredBitmapId = null;
}

// Create a set of markers with the given bitmap and update the state with new
// markers.
void _updateMarkers(BitmapDescriptor bitmap) {
final List<Marker> newMarkers = List<Marker>.generate(
_numberOfMarkers,
(int id) {
return Marker(
markerId: MarkerId('$id'),
icon: bitmap,
position: LatLng(
Random().nextDouble() * 100 - 50,
Random().nextDouble() * 100 - 50,
),
);
},
);

setState(() {
_markers
..clear()
..addAll(newMarkers);
});
}

/// Load a bitmap from an asset and create a scaled [BytesMapBitmap] from it.
Future<BytesMapBitmap> _getAssetBitmapDescriptor() async {
final ByteData byteData = await rootBundle.load('assets/checkers.png');
final Uint8List bytes = byteData.buffer.asUint8List();
return BitmapDescriptor.bytes(
bytes,
width: 64,
height: 64,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

import 'animate_camera.dart';
import 'bitmap_registry.dart';
import 'clustering.dart';
import 'heatmap.dart';
import 'lite_mode.dart';
Expand Down Expand Up @@ -47,6 +48,7 @@ final List<GoogleMapExampleAppPage> _allPages = <GoogleMapExampleAppPage>[
const ClusteringPage(),
const MapIdPage(),
const HeatmapPage(),
const BitmapRegistryPage(),
];

/// MapsDemo is the Main Application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ flutter:
uses-material-design: true
assets:
- assets/

# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
{google_maps_flutter_android: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_web}}
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf

part 'src/controller.dart';
part 'src/google_map.dart';
part 'src/bitmap_registry.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
part of '../google_maps_flutter.dart';

/// A bitmap registry.
///
/// Bitmaps can be created before they are used in markers and then registered
/// with the registry. This allows for more efficient rendering of markers
/// on the map. For example, if multiple markers use the same bitmap, bitmap
/// can be registered once and then reused. This eliminates the need to
/// transfer the bitmap data multiple times to the platform side.
///
/// Using bitmap registry is optional.
///
/// Example:
/// ```dart
/// // Register a bitmap
/// final registeredBitmap = await GoogleMapBitmapRegistry.instance.register(
/// Bitmap.fromAsset('assets/image.png'),
/// );
///
/// // Use the registered bitmap as marker icon
/// Marker(
/// markerId: MarkerId('markerId'),
/// icon: registeredBitmap,
/// position: LatLng(0, 0)
/// ),
/// )
/// ```
class GoogleMapBitmapRegistry {
GoogleMapBitmapRegistry._();

/// The singleton instance of [GoogleMapBitmapRegistry].
static final GoogleMapBitmapRegistry instance = GoogleMapBitmapRegistry._();

// The number of registered images. Also used as a unique identifier for each
// registered image.
int _imageCount = 0;

/// Registers a [bitmap] with the registry.
///
/// Returns a unique identifier for the registered bitmap.
Future<int> register(MapBitmap bitmap) async {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could return RegisteredMapBitmap object directly

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was doing initially but then there was a comment to my other PR about not mixing platform and user-facing packages. I think it will be the case here too because RegisteredMapBitmap is part of platform interface while this comment is about user-facing plugin.

I see these options:

  • Use int (the way it's done now)
  • Add GoogleMapsRegisteredMapBitmap or something like that to the user-facing plugin
  • Use RegisteredMapBitmap and see what maintainers say

Copy link

@jokerttu jokerttu Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, BitmapDescriptor classes are used already as icon objects etc.
I think the case was more about the internal configuration umbrella objects?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIR it was about using platform interface's MarkerType in google_maps_flutter, exposing it for end users. All other objects are exposed but I was asked not to do that because they're changing how federated plugins are doing things.

I'd rather keep it like it is now and wait for review or ask a question when main PR is created. What do you think?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is quite easy to change later if they want to

_imageCount++;
final int id = _imageCount;
await GoogleMapsFlutterPlatform.instance.registerBitmap(id, bitmap);
return id;
}

/// Unregisters a bitmap with the given [id].
Future<void> unregister(int id) {
Copy link

@jokerttu jokerttu Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this take RegisteredMapBitmap as a parameter.

return GoogleMapsFlutterPlatform.instance.unregisterBitmap(id);
}

/// Unregister all previously registered bitmaps and clear the cache.
Future<void> clear() {
return GoogleMapsFlutterPlatform.instance.clearBitmapCache();
}
}
5 changes: 5 additions & 0 deletions packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ topics:
# The example deliberately includes limited-use secrets.
false_secrets:
- /example/web/index.html

# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
{google_maps_flutter_android: {path: ../../../packages/google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../../packages/google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../../packages/google_maps_flutter/google_maps_flutter_web}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

import 'fake_google_maps_flutter_platform.dart';

void main() {
late FakeGoogleMapsFlutterPlatform platform;

setUp(() {
platform = FakeGoogleMapsFlutterPlatform();
GoogleMapsFlutterPlatform.instance = platform;
});

test('Adding bitmap to registry', () async {
final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20));
final int id = await GoogleMapBitmapRegistry.instance.register(bitmap);
expect(id, 1);
expect(
platform.bitmapRegistryRecorder.bitmaps,
<String>['REGISTER $id $bitmap'],
);
});

test('Removing bitmap from registry', () async {
final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20));
final int id = await GoogleMapBitmapRegistry.instance.register(bitmap);
await GoogleMapBitmapRegistry.instance.unregister(id);
expect(
platform.bitmapRegistryRecorder.bitmaps,
<String>['REGISTER $id $bitmap', 'UNREGISTER $id'],
);
});

test('Clearing bitmap registry', () async {
final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20));
final int id1 = await GoogleMapBitmapRegistry.instance.register(bitmap);
final int id2 = await GoogleMapBitmapRegistry.instance.register(bitmap);
expect(
platform.bitmapRegistryRecorder.bitmaps,
<String>['REGISTER $id1 $bitmap', 'REGISTER $id2 $bitmap'],
);

await GoogleMapBitmapRegistry.instance.clear();
expect(
platform.bitmapRegistryRecorder.bitmaps,
<String>['REGISTER $id1 $bitmap', 'REGISTER $id2 $bitmap', 'CLEAR CACHE'],
);
});

test('Bitmap ID is incremental', () async {
final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20));
final int id1 = await GoogleMapBitmapRegistry.instance.register(bitmap);
final int id2 = await GoogleMapBitmapRegistry.instance.register(bitmap);
final int id3 = await GoogleMapBitmapRegistry.instance.register(bitmap);
final int id4 = await GoogleMapBitmapRegistry.instance.register(bitmap);
expect(id2, id1 + 1);
expect(id3, id1 + 2);
expect(id4, id1 + 3);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {
Map<int, PlatformMapStateRecorder> mapInstances =
<int, PlatformMapStateRecorder>{};

/// A recorder for the bitmap registry calls.
PlatformBitmapRegistryRecorder bitmapRegistryRecorder =
PlatformBitmapRegistryRecorder();

PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!;

/// Whether to add a small delay to async calls to simulate more realistic
Expand Down Expand Up @@ -264,6 +268,21 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {
return mapEventStreamController.stream.whereType<ClusterTapEvent>();
}

@override
Future<void> registerBitmap(int id, MapBitmap bitmap) async {
bitmapRegistryRecorder.bitmaps.add('REGISTER $id $bitmap');
}

@override
Future<void> unregisterBitmap(int id) async {
bitmapRegistryRecorder.bitmaps.add('UNREGISTER $id');
}

@override
Future<void> clearBitmapCache() async {
bitmapRegistryRecorder.bitmaps.add('CLEAR CACHE');
}

@override
void dispose({required int mapId}) {
disposed = true;
Expand Down Expand Up @@ -331,3 +350,11 @@ class PlatformMapStateRecorder {
final List<ClusterManagerUpdates> clusterManagerUpdates =
<ClusterManagerUpdates>[];
}

/// A fake implementation of native bitmap registry, which stores all the
/// updates for inspection in tests.
class PlatformBitmapRegistryRecorder {
PlatformBitmapRegistryRecorder();

final List<String> bitmaps = <String>[];
}
Loading