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
13 changes: 10 additions & 3 deletions lib/src/map/controller/map_controller_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
implements MapController {
final _mapEventStreamController = StreamController<MapEvent>.broadcast();

late MapInteractiveViewerState _interactiveViewerState;
/// The viewer of the map this controller is attached to, or `null` while it
/// does not exist yet.
///
/// The viewer is created during layout, so a `FlutterMap` that has been
/// built but not laid out - for example one on a route that another route
/// covers from the first frame - has none, even though `onMapReady` has
/// already fired.
MapInteractiveViewerState? _interactiveViewerState;

Animation<LatLng>? _moveAnimation;
Animation<double>? _zoomAnimation;
Expand Down Expand Up @@ -336,7 +343,7 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>

if (value.options != null &&
value.options!.interactionOptions != newOptions.interactionOptions) {
_interactiveViewerState.updateGestures(
_interactiveViewerState?.updateGestures(
value.options!.interactionOptions,
newOptions.interactionOptions,
);
Expand Down Expand Up @@ -702,7 +709,7 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>

void _emitMapEvent(MapEvent event) {
if (event.source == MapEventSource.mapController && event is MapEventMove) {
_interactiveViewerState.interruptAnimatedMovement(event);
_interactiveViewerState?.interruptAnimatedMovement(event);
}

options.onMapEvent?.call(event);
Expand Down
113 changes: 113 additions & 0 deletions test/map/unlaid_out_map_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';

/// Builds [map] on a route that an opaque route covers from the very first
/// frame.
///
/// The `Overlay` builds the covered route but never lays it out, so the
/// `LayoutBuilder` inside `FlutterMap` never runs and the map's
/// `MapInteractiveViewer` is never created - while `onMapReady`, scheduled
/// from `FlutterMap`'s `initState`, still fires after the first frame.
///
/// This happens whenever an app is launched straight into a deeper screen,
/// for example from a deep link or a notification, with the map underneath.
Widget _coveredMap(Widget map) => MaterialApp(
// Without `onGenerateRoute`, `MaterialApp` builds no `Navigator` and
// ignores `onGenerateInitialRoutes`.
onGenerateRoute: (_) => null,
onGenerateInitialRoutes: (_) => [
MaterialPageRoute<void>(builder: (_) => Scaffold(body: map)),
MaterialPageRoute<void>(
builder: (_) => const Scaffold(body: Text('cover')),
),
],
);

Future<void> _uncover(WidgetTester tester) async {
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
}

void main() {
testWidgets(
'moving the camera from onMapReady does not throw when the map has not '
'been laid out, and the move is kept',
(tester) async {
final controller = MapController();
addTearDown(controller.dispose);
const destination = LatLng(52, 9);
var ready = false;

await tester.pumpWidget(
_coveredMap(
FlutterMap(
mapController: controller,
options: MapOptions(
onMapReady: () {
ready = true;
controller.move(destination, 15);
},
),
children: const [],
),
),
);
await tester.pump();

expect(ready, isTrue);
expect(tester.takeException(), isNull);

await _uncover(tester);

expect(tester.takeException(), isNull);
expect(controller.camera.center, destination);
expect(controller.camera.zoom, 15);
},
);

testWidgets(
'changing the interaction options before the map has been laid out does '
'not throw, and the new options apply once it is',
(tester) async {
final controller = MapController();
addTearDown(controller.dispose);
final interactive = ValueNotifier(false);
addTearDown(interactive.dispose);

await tester.pumpWidget(
_coveredMap(
ValueListenableBuilder(
valueListenable: interactive,
builder: (_, isInteractive, __) => FlutterMap(
mapController: controller,
options: MapOptions(
initialCenter: const LatLng(0, 0),
interactionOptions: InteractionOptions(
flags: isInteractive
? InteractiveFlag.all
: InteractiveFlag.none,
),
),
children: const [],
),
),
),
);
await tester.pump();

interactive.value = true;
await tester.pump();

expect(tester.takeException(), isNull);

await _uncover(tester);
await tester.drag(find.byType(FlutterMap), const Offset(-200, 0));
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
expect(controller.camera.center, isNot(const LatLng(0, 0)));
},
);
}