flutter_custom_cluster
A highly customizable clustering solution for Google Maps in Flutter with custom icon painters and efficient performance.
Features
- 🎯 Efficient Clustering: Optimized algorithm for smooth map interactions
- 🎨 Custom Icons: Flexible painter system for custom cluster and marker designs
- âš¡ Performance: Built-in icon caching and optimized rendering
- 🔧 Configurable: Extensive configuration options for clustering behavior
- 💪 Type-Safe: Generic API for your custom data types
- 📱 Production-Ready: Battle-tested clustering logic
Getting started
Add to your pubspec.yaml:
dependencies:
flutter_custom_cluster: ^0.1.0
Usage
1. Create Your Custom Item Class
import 'package:flutter_custom_cluster/flutter_custom_cluster.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class GymLocation extends ClusterableItem {
final String name;
final String address;
final int gymId;
GymLocation({
required String id,
required LatLng position,
required this.name,
required this.address,
required this.gymId,
}) : super(id: id, position: position);
}
2. Initialize the Cluster Manager
class MapScreen extends StatefulWidget {
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late ClusterManager<GymLocation> _clusterManager;
Set<Marker> _markers = {};
double _currentZoom = 12.0;
final List<GymLocation> _locations = [
GymLocation(
id: '1',
position: LatLng(41.2995, 69.2401),
name: 'Gym 1',
address: 'Tashkent',
gymId: 1,
),
// Add more locations...
];
@override
void initState() {
super.initState();
_clusterManager = ClusterManager<GymLocation>(
items: _locations,
config: ClusterConfig(
radius: 150,
minZoom: 0,
maxZoom: 19,
minClusterSize: 2,
),
);
_updateMarkers();
}
Future<void> _updateMarkers() async {
final markers = await _clusterManager.getMarkers(
zoom: _currentZoom,
onMarkerTap: (id, item) {
print('Tapped: ${item.name}');
// Show details, navigate, etc.
},
onClusterTap: (id, cluster) {
print('Cluster with ${cluster.clusterSize} items');
// Zoom in to cluster
},
);
setState(() {
_markers = markers;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(41.2995, 69.2401),
zoom: 12,
),
markers: _markers,
onCameraMove: (position) {
_currentZoom = position.zoom;
},
onCameraIdle: _updateMarkers,
),
);
}
}
Customization
Custom Configuration
final manager = ClusterManager<GymLocation>(
items: locations,
config: ClusterConfig(
radius: 200, // Cluster radius in pixels
minZoom: 3, // Start clustering at zoom 3
maxZoom: 18, // Stop clustering at zoom 18
minClusterSize: 3, // Minimum items to form a cluster
),
);
Custom Icon Painter
Create your own painter for complete control over cluster appearance:
class MyCustomPainter extends ClusterIconPainter {
@override
Future<Uint8List> paintCluster(ClusterItem cluster) async {
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
// Your custom painting logic here
// Draw shapes, load images, add text, etc.
final image = await recorder.endRecording().toImage(120, 120);
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return bytes!.buffer.asUint8List();
}
@override
Future<Uint8List> paintItem(ClusterableItem item) async {
// Paint individual item markers
}
}
// Use your custom painter
final manager = ClusterManager<GymLocation>(
items: locations,
painter: MyCustomPainter(),
);
Asset-Based Icons
class AssetClusterPainter extends ClusterIconPainter {
@override
Future<Uint8List> paintCluster(ClusterItem cluster) async {
// Load your asset
final data = await rootBundle.load('assets/cluster_bg.png');
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
// Draw asset and add cluster count
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
canvas.drawImage(image, Offset.zero, Paint());
// Add text showing cluster size
final textPainter = TextPainter(
text: TextSpan(
text: '${cluster.clusterSize}',
style: TextStyle(color: Colors.white, fontSize: 24),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, Offset(50, 50));
final finalImage = await recorder.endRecording().toImage(200, 200);
final pngBytes = await finalImage.toByteData(format: ui.ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
}
@override
Future<Uint8List> paintItem(ClusterableItem item) async {
final data = await rootBundle.load('assets/marker.png');
return data.buffer.asUint8List();
}
}
Custom Colors
Use the default painter with custom colors:
final manager = ClusterManager<GymLocation>(
items: locations,
painter: DefaultClusterPainter(
clusterColor: Colors.blue,
itemColor: Colors.red,
textColor: Colors.white,
borderColor: Colors.white,
clusterRadius: 50,
itemRadius: 30,
fontSize: 24,
),
);
Advanced Usage
Filter by Bounds
final markers = await manager.getMarkers(
zoom: currentZoom,
bounds: LatLngBounds(
southwest: LatLng(40.0, 68.0),
northeast: LatLng(42.0, 70.0),
),
);
Force Recalculation
final markers = await manager.getMarkers(
zoom: currentZoom,
forceRecalculate: true,
);
Get Statistics
final stats = manager.getStats();
print('Clusters: ${stats['clusters']}');
print('Individual items: ${stats['items']}');
print('Total markers: ${stats['total']}');
Cache Management
// Clear icon cache if needed
manager.clearCache();
// Force recalculation of clusters
manager.invalidate();
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
radius |
int | 150 | Cluster radius in pixels |
minZoom |
int | 0 | Minimum zoom level (0-19) |
maxZoom |
int | 19 | Maximum zoom level (0-19) |
extent |
int | 512 | Tile extent (must be power of 2) |
nodeSize |
int | 64 | KD-tree leaf node size |
minClusterSize |
int | 2 | Minimum items to form cluster |
Performance Tips
- Use appropriate radius: Larger radius = fewer clusters = better performance
- Set reasonable zoom limits: Don't cluster at very high zoom levels
- Cache icons: The package automatically caches generated icons
- Debounce updates: Use debouncing for camera movement updates
import 'package:easy_debounce/easy_debounce.dart';
onCameraIdle: () {
EasyDebounce.debounce(
'map-update',
Duration(milliseconds: 300),
() => _updateMarkers(),
);
}
Migration from Fluster
If you're coming from Fluster, the migration is straightforward:
Before (Fluster):
final fluster = Fluster<LocationMarker>(
minZoom: 0,
maxZoom: 19,
radius: 150,
points: locations,
);
final clusters = fluster.clusters(bounds, zoom);
After (flutter_custom_cluster):
final manager = ClusterManager<LocationMarker>(
items: locations,
config: ClusterConfig(
minZoom: 0,
maxZoom: 19,
radius: 150,
),
);
final markers = await manager.getMarkers(
zoom: zoom,
bounds: bounds,
);
Example
Check out the example directory for a complete working app demonstrating:
- Basic clustering setup
- Custom painters
- Asset-based icons
- Marker interactions
- Zoom controls
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Acknowledgments
- Inspired by Fluster and Supercluster
- Built for the Flutter community
Libraries
- google_maps_flexible_cluster
- A highly customizable clustering solution for Google Maps in Flutter.