Social Report
This example demonstrates how to create a Flutter app with an interactive map that allows users to upload and view social events, using the Maps SDK for Flutter.
How it works
The example app includes the following features:
- Display an interactive map.
- Upload a social report.
- Select and view existing social reports directly from the map.
![]() | ![]() | ![]() |
---|---|---|
Initial map view | Uploaded report | Selected social report |
UI and Map Integration
The following code creates a user interface with an interactive GemMap
and an app bar. The app bar includes two buttons: one for acquiring the user's current position and another for uploading a police report at the current location.
const projectApiToken = String.fromEnvironment('GEM_TOKEN');
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Social Report',
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;
PermissionStatus _locationPermissionStatus = PermissionStatus.denied;
bool _hasLiveDataSource = false;
// Current selected overlay item
OverlayItem? _selectedItem;
void dispose() {
GemKit.release();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text(
'Social Report',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
onPressed: _onFollowPositionButtonPressed,
icon: const Icon(
Icons.location_searching_sharp,
color: Colors.white,
),
),
if (_hasLiveDataSource)
IconButton(
onPressed: _onPrepareReportingButtonPressed,
icon: Icon(
Icons.report,
color: Colors.white,
))
],
),
body: Stack(children: [
GemMap(
key: ValueKey("GemMap"),
onMapCreated: _onMapCreated,
appAuthorization: projectApiToken,
),
if (_selectedItem != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SocialEventPanel(
overlayItem: _selectedItem!,
onClose: () {
setState(() {
_selectedItem = null;
});
},
),
),
)
]),
);
}
// The callback for when map is ready to use.
void _onMapCreated(GemMapController controller) async {
// Save controller for further usage.
_mapController = controller;
// Register callback for touch events and updated cursor position
_mapController.registerTouchCallback((point) {
_mapController.setCursorScreenPosition(point);
});
// Get selected overlay items under cursor
_mapController.registerCursorSelectionUpdatedOverlayItemsCallback((items) {
if (items.isEmpty) return;
final selectedItem = items.first;
// Update selected item
setState(() {
_selectedItem = selectedItem;
});
});
}
void _onFollowPositionButtonPressed() async {
if (kIsWeb) {
// On web platform permission are handled differently than other platforms.
// The SDK handles the request of permission for location.
final locationPermssionWeb = await PositionService.requestLocationPermission;
if (locationPermssionWeb == true) {
_locationPermissionStatus = PermissionStatus.granted;
} else {
_locationPermissionStatus = PermissionStatus.denied;
}
} else {
// For Android & iOS platforms, permission_handler package is used to ask for permissions.
_locationPermissionStatus = await Permission.locationWhenInUse.request();
}
if (_locationPermissionStatus == PermissionStatus.granted) {
// After the permission was granted, we can set the live data source (in most cases the GPS).
// The data source should be set only once, otherwise we'll get -5 error.
if (!_hasLiveDataSource) {
PositionService.instance.setLiveDataSource();
_hasLiveDataSource = true;
}
// Optionally, we can set an animation
final animation = GemAnimation(type: AnimationType.linear);
// Calling the start following position SDK method.
_mapController.startFollowingPosition(animation: animation);
setState(() {});
}
}
void _onPrepareReportingButtonPressed() async {
// Get current position quality
final improvedPos = PositionService.instance.improvedPosition;
final posQuality = improvedPos!.fixQuality;
if (posQuality == PositionQuality.invalid || posQuality == PositionQuality.inertial) {
_showSnackBar(context, message: "There is no accurate position at the moment.", duration: Duration(seconds: 3));
return;
}
// Get the reporting id (uses current position). Requires accurate position, may return GemError.notFound when in buildings/tunnels etc.
int idReport = SocialOverlay.prepareReporting();
// Get the subcategory id
SocialReportsOverlayInfo info = SocialOverlay.reportsOverlayInfo;
List<SocialReportsOverlayCategory> categs = info.getSocialReportsCategories();
SocialReportsOverlayCategory cat = categs.first;
List<SocialReportsOverlayCategory> subcats = cat.overlaySubcategories;
SocialReportsOverlayCategory subCategory = subcats.first;
// Create the other required parameters
Uint8List image =
await ImageGenerator.createReferenceImage(size: const Size(100, 100), format: ImageFileFormat.png);
ParameterList params = ParameterList.create();
// Report
GemError res =
SocialOverlay.report(idReport, subCategory.uid, "TEST MAGIC LANE", image, ImageFileFormat.png, params);
// ignore: use_build_context_synchronously
_showSnackBar(context, message: "Successfully added report: $res.", duration: Duration(seconds: 3));
}
// Show a snackbar indicating that the route calculation is in progress.
void _showSnackBar(
BuildContext context, {
required String message,
Duration duration = const Duration(hours: 1),
}) {
final snackBar = SnackBar(content: Text(message), duration: duration);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
Social Event Panel
The following code demonstrates how to retrieve details from an OverlayItem
that represents a social event.
import 'package:intl/intl.dart';
class SocialEventPanel extends StatelessWidget {
final OverlayItem overlayItem;
final VoidCallback onClose;
const SocialEventPanel({super.key, required this.overlayItem, required this.onClose});
Widget build(BuildContext context) {
final overlayImg = overlayItem.img;
return Container(
width: MediaQuery.of(context).size.width - 40,
color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: overlayImg.isValid
? Image.memory(
overlayImg.getRenderableImageBytes(size: Size(50, 50), format: ImageFileFormat.png)!,
)
: const SizedBox()),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(overlayItem.name),
Text(
"Date: ${formatTimestamp(overlayItem.previewDataJson["parameters"]["create_stamp_utc"] as String)}"),
Text("Upvotes: ${overlayItem.previewDataJson["parameters"]["score"]}"),
],
),
],
),
IconButton(onPressed: onClose, icon: Icon(Icons.close)),
],
),
],
),
);
}
String formatTimestamp(String timestampStr) {
final timestamp = int.tryParse(timestampStr);
if (timestamp == null) return "Invalid date";
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
return DateFormat('MM/dd/yyyy').format(date);
}
}
Image Generator
abstract class ImageGenerator {
static Future<Uint8List> createReferenceImage(
{required Size size,
required ImageFileFormat format,
Color bgColor = const Color.fromARGB(255, 0, 0, 255),
Color fgColor = const Color.fromARGB(255, 255, 255, 0)}) async {
final w = size.width.toInt();
final h = size.height.toInt();
final backgroundColor = image.ColorRgba8(
(bgColor.r * 255).toInt(),
(bgColor.g * 255).toInt(),
(bgColor.b * 255).toInt(),
(bgColor.a * 255).toInt());
final foregroundColor = image.ColorRgba8(
(fgColor.r * 255).toInt(),
(fgColor.g * 255).toInt(),
(fgColor.b * 255).toInt(),
(fgColor.a * 255).toInt());
var img = image.Image(width: w, height: h);
img = image.fill(img, color: backgroundColor);
img = image.drawLine(
img,
x1: 0,
y1: 0,
x2: w - 1,
y2: h - 1,
color: foregroundColor,
antialias: true,
thickness: 2,
);
img = image.drawLine(
img,
x1: w - 1,
y1: 0,
x2: 0,
y2: h - 1,
color: foregroundColor,
antialias: true,
thickness: 2,
);
switch (format) {
case ImageFileFormat.png:
return image.encodePng(img);
case ImageFileFormat.jpeg:
return image.encodeJpg(img);
case ImageFileFormat.bmp:
return image.encodeBmp(img);
default:
throw Exception('Unsupported format.');
}
}
static Future<Uint8List> createTransparentReferenceImage(
{required Size size}) async {
final w = size.width.toInt();
final h = size.height.toInt();
var img = image.Image(width: w, height: h);
img = image.fill(img, color: image.ColorRgba8(0, 0, 0, 0));
img = image.drawCircle(
img,
x: w ~/ 2,
y: h ~/ 2,
radius: min(w ~/ 2, h ~/ 2),
color: image.ColorRgb8(255, 0, 0),
antialias: true,
);
return image.encodePng(img);
}
}
Flutter Examples
Maps SDK for Flutter Examples can be downloaded or cloned with Git.