-
Couldn't load subscription status.
- Fork 176
[sdk] Add MapRecorder support for Flutter SDK #1049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: public/protheeuz-map-recorder-support
Are you sure you want to change the base?
[sdk] Add MapRecorder support for Flutter SDK #1049
Conversation
Implements MapRecorder functionality to achieve feature parity with Android and iOS native SDKs. - Add Pigeon-generated platform communication layer - Implement MapRecorderController for Android and iOS platforms - Create MapRecorder wrapper class with user-facing API - Integrate into MapboxMap via recorder property - Add working example demonstrating record/replay/pause features - Mark API as @experimental matching native SDK status
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this contribution! This will be a great addition to the Flutter SDK. The implementation shows good understanding of the platform channel architecture and maintains consistency across Android and iOS.
There are a few items to address before we can merge:
- Please add tests for the core functionality (i.e. basic usage of the recorder, data handling, playback states). You can create a new file here and follow the pattern.
- Please add a short changelog entry. You can use the
mainheading for now and we'll move it if needed. - [Android] Recorder instances need to be cleaned up to avoid memory leaks
- [Android] A few minor items can be safely removed, see individual comments
- [iOS] PigeonError should be removed
I also added some recommendations for the example, but they are not critical issues.
Once you make these edits I'll approve and then you can merge the changes into this temporary branch. We'll then run that branch through our CI system to ensure all tests pass and the formatting is correct. Then we'll merge into main and release these changes with our next release!
Thanks again and let me know if you have questions about any of this.
| child: ElevatedButton.icon( | ||
| onPressed: isReplaying ? _togglePause : null, | ||
| icon: const Icon(Icons.pause), | ||
| label: const Text('Pause'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to update this text and icon after the user selects Pause so that they know to click the button again to restart the recording.
| val data = getRecorder().stopRecording() | ||
| val bytes = ByteArray(data.remaining()) | ||
| data.get(bytes) | ||
| data.rewind() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that this data.rewind() is necessary as the data has already been copied to the byte array. Is there another purpose I'm missing?
| private val mapboxMap: MapboxMap | ||
| ) : _MapRecorderMessenger { | ||
|
|
||
| private var recorder: MapboxMapRecorder? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be cleaned up to avoid memory leaks. Please add a dispose() and call it from MapboxMapController.dispose()
fun dispose() {
recorder = null
}And then in MapboxMapController.kt:
override fun dispose() {
if (mapView == null) {
return
}
lifecycleHelper?.dispose()
lifecycleHelper = null
mapView?.setViewTreeLifecycleOwner(null)
mapView = null
mapboxMap = null
methodChannel.setMethodCallHandler(null)
StyleManager.setUp(messenger, null, channelSuffix)
_CameraManager.setUp(messenger, null, channelSuffix)
Projection.setUp(messenger, null, channelSuffix)
_MapInterface.setUp(messenger, null, channelSuffix)
_AnimationManager.setUp(messenger, null, channelSuffix)
annotationController.dispose()
_LocationComponentSettingsInterface.setUp(messenger, null, channelSuffix)
LogoSettingsInterface.setUp(messenger, null, channelSuffix)
GesturesSettingsInterface.setUp(messenger, null, channelSuffix)
CompassSettingsInterface.setUp(messenger, null, channelSuffix)
ScaleBarSettingsInterface.setUp(messenger, null, channelSuffix)
AttributionSettingsInterface.setUp(messenger, null, channelSuffix)
_ViewportMessenger.setUp(messenger, null, channelSuffix)
_PerformanceStatisticsApi.setUp(messenger, null, channelSuffix)
_MapRecorderMessenger.setUp(messenger, null, channelSuffix) // ← ADD THIS
mapRecorderController.dispose() // ← ADD THIS
}| override fun startRecording(options: MapRecorderOptions) { | ||
| val nativeOptions = com.mapbox.maps.MapRecorderOptions.Builder() | ||
| .apply { | ||
| options.timeWindow?.let { timeWindow(it.toLong()) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this toLong() conversion is redundant - timeWindow is already Long? from the pigeon definition.
| avoidPlaybackPauses: false, | ||
| ); | ||
|
|
||
| setState(() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a critical issue as this is just an example, but it is worth noting that the replay() method is async. If the user navigates away during replay, the widget could be disposed, and calling setState() on a disposed widget will throw an exception.
This could be fixed by wrapping in an if (mounted) block like this:
if (mounted) {
setState(() {
isReplaying = false;
});
}The same applies to the other setState and error calls here.
| bool isRecording = false; | ||
| bool isReplaying = false; | ||
| String playbackState = 'IDLE'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to use just one approach to state management (either the boolean or the playback state). Another option would be an enum of all potential states.
| } | ||
|
|
||
| private func wrapError(_ error: Any) -> [Any?] { | ||
| if let pigeonError = error as? PigeonError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will not compile on iOS because PigeonError is not defined. However, I think it can just be removed in favor of the default error.
| @@ -0,0 +1,88 @@ | |||
| package com.mapbox.maps.mapbox_maps | |||
|
|
|||
| import com.mapbox.bindgen.DataRef | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this import can be removed
What does this pull request do?
This PR implements the MapRecorder API for Flutter SDK, exposing functionality that already exists in Android and iOS native SDKs.
API Surface:
mapboxMap.recorder.startRecording()- Begin recording map interactionsmapboxMap.recorder.stopRecording()- Stop and retrieve recorded sequencemapboxMap.recorder.replay()- Replay recorded sequence with customization optionsmapboxMap.recorder.togglePause()- Pause/resume playbackmapboxMap.recorder.getState()- Get current playback stateImplementation approach:
MapboxMaps.MapRecorder(iOS) andMapboxMapRecorder(Android)@experimentalto match native SDK statusExample usage:
Files added:
What is the motivation and context behind this change?
Issue: #1034
Context:
MapRecorder is available in both native SDKs but missing from Flutter SDK, creating a feature gap for Flutter devs.
Use cases (per native SDK documentation):
Native SDK references:
MapRecorder- marked@_spi(Experimental)MapboxMapRecorder- requires@OptIn(MapboxExperimental)Implementation notes:
pigeons/map_recorder.dart) as Mapbox internal tooling doesn't provide one for MapRecorder@experimentalwith no version compatibility guarantee for recorded file formatTesting:
Demo Videos:
Android (Pixel 9 - API 34)
tested-android.mp4
iOS (iPhone 16 Plus)
Simulator.Screen.Recording.-.iPhone.16.Plus.-.2025-10-16.at.11.16.59.mp4
Pull request checklist:
Note on tests:
Unit tests were not added as this follows the existing pattern in the SDK. The codebase currently only has
events_test.dartfor event data serialization. Other features (annotations, offline, snapshotter) also don't have unit tests. Integration testing is handled via the example app, which demonstrates all MapRecorder features with UI controls.Note on changelog:
Will add changelog entry once PR is approved, to match the appropriate version number.