Skip to content

Commit 34e416f

Browse files
committed
fix: enhance viewcontroller error management with stateaware controllers
1 parent b72ef4b commit 34e416f

7 files changed

Lines changed: 235 additions & 48 deletions

File tree

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ yarn run example detox:test:ios-release
143143
```
144144

145145
Android:
146+
147+
> [!NOTE]
148+
> Create emulator named "Android_Emulator" first if you don't have one already:
146149
```bash
147150
yarn run example detox:test:android-release
148151
```

example/.detoxrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ module.exports = {
7171
emulator: {
7272
type: 'android.emulator',
7373
device: {
74-
avdName: 'Pixel_9_Pro_API_35',
74+
avdName: 'Android_Emulator',
7575
},
7676
},
7777
},

src/maps/mapView/mapView.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const MapView = (props: MapViewProps): React.JSX.Element => {
3333
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3434
const mapViewRef = useRef<any>(null);
3535
const [viewId, setViewId] = useState<number | null>(null);
36+
const mapControllerDisposeRef = useRef<(() => void) | null>(null);
3637

3738
const { onMapViewControllerCreated } = props;
3839

@@ -46,17 +47,34 @@ export const MapView = (props: MapViewProps): React.JSX.Element => {
4647
}
4748
};
4849

50+
// Create controllers when viewId changes
4951
useEffect(() => {
5052
if (!mapViewRef.current) {
5153
return;
5254
}
5355
const _viewId = findNodeHandle(mapViewRef.current) || 0;
5456
if (viewId !== _viewId) {
57+
// Dispose previous controller if viewId actually changed
58+
if (mapControllerDisposeRef.current) {
59+
mapControllerDisposeRef.current();
60+
}
5561
setViewId(_viewId);
56-
onMapViewControllerCreated(getMapViewController(_viewId));
62+
const { controller, dispose } = getMapViewController(_viewId);
63+
mapControllerDisposeRef.current = dispose;
64+
onMapViewControllerCreated(controller);
5765
}
5866
}, [onMapViewControllerCreated, viewId]);
5967

68+
// Cleanup on unmount
69+
useEffect(() => {
70+
return () => {
71+
if (mapControllerDisposeRef.current) {
72+
mapControllerDisposeRef.current();
73+
mapControllerDisposeRef.current = null;
74+
}
75+
};
76+
}, []);
77+
6078
const onMapClick = useCallback(
6179
({ nativeEvent: latlng }: { nativeEvent: LatLng }) => {
6280
props.mapViewCallbacks?.onMapClick?.(latlng);

src/maps/mapView/mapViewController.ts

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
import { NativeModules } from 'react-native';
1818
import type { Location } from '../../shared/types';
19-
import { commands, sendCommand } from '../../shared/viewManager';
19+
import {
20+
commands,
21+
createControllerContext,
22+
type ControllerResult,
23+
} from '../../shared/viewManager';
2024
import type {
2125
CameraPosition,
2226
Circle,
@@ -36,19 +40,28 @@ import type {
3640
} from './types';
3741
const { NavViewModule } = NativeModules;
3842

39-
export const getMapViewController = (viewId: number): MapViewController => {
40-
return {
43+
export const getMapViewController = (
44+
viewId: number
45+
): ControllerResult<MapViewController> => {
46+
const { sendCommand, dispose } = createControllerContext(
47+
viewId,
48+
'MapViewController'
49+
);
50+
51+
const controller: MapViewController = {
4152
setMapType: (mapType: MapType) => {
42-
sendCommand(viewId, commands.setMapType, [mapType]);
53+
sendCommand('setMapType', commands.setMapType, [mapType]);
4354
},
4455
setMapStyle: (mapStyle: string) => {
45-
sendCommand(viewId, commands.setMapStyle, [mapStyle]);
56+
sendCommand('setMapStyle', commands.setMapStyle, [mapStyle]);
4657
},
4758
setMapToolbarEnabled: (index: boolean) => {
48-
sendCommand(viewId, commands.setMapToolbarEnabled, [index]);
59+
sendCommand('setMapToolbarEnabled', commands.setMapToolbarEnabled, [
60+
index,
61+
]);
4962
},
5063
clearMapView: () => {
51-
sendCommand(viewId, commands.clearMapView, []);
64+
sendCommand('clearMapView', commands.clearMapView, []);
5265
},
5366

5467
addCircle: async (circleOptions: CircleOptions): Promise<Circle> => {
@@ -77,73 +90,95 @@ export const getMapViewController = (viewId: number): MapViewController => {
7790
},
7891

7992
removeMarker: (id: string) => {
80-
sendCommand(viewId, commands.removeMarker, [id]);
93+
sendCommand('removeMarker', commands.removeMarker, [id]);
8194
},
8295

8396
removePolyline: (id: string) => {
84-
sendCommand(viewId, commands.removePolyline, [id]);
97+
sendCommand('removePolyline', commands.removePolyline, [id]);
8598
},
8699

87100
removePolygon: (id: string) => {
88-
sendCommand(viewId, commands.removePolygon, [id]);
101+
sendCommand('removePolygon', commands.removePolygon, [id]);
89102
},
90103

91104
removeCircle: (id: string) => {
92-
sendCommand(viewId, commands.removeCircle, [id]);
105+
sendCommand('removeCircle', commands.removeCircle, [id]);
93106
},
94107

95108
setIndoorEnabled: (isOn: boolean) => {
96-
sendCommand(viewId, commands.setIndoorEnabled, [isOn]);
109+
sendCommand('setIndoorEnabled', commands.setIndoorEnabled, [isOn]);
97110
},
98111

99112
setTrafficEnabled: (isOn: boolean) => {
100-
sendCommand(viewId, commands.setTrafficEnabled, [isOn]);
113+
sendCommand('setTrafficEnabled', commands.setTrafficEnabled, [isOn]);
101114
},
102115

103116
setCompassEnabled: (isOn: boolean) => {
104-
sendCommand(viewId, commands.setCompassEnabled, [isOn]);
117+
sendCommand('setCompassEnabled', commands.setCompassEnabled, [isOn]);
105118
},
106119

107120
setMyLocationButtonEnabled: (isOn: boolean) => {
108-
sendCommand(viewId, commands.setMyLocationButtonEnabled, [isOn]);
121+
sendCommand(
122+
'setMyLocationButtonEnabled',
123+
commands.setMyLocationButtonEnabled,
124+
[isOn]
125+
);
109126
},
110127

111128
setMyLocationEnabled: (isOn: boolean) => {
112-
sendCommand(viewId, commands.setMyLocationEnabled, [isOn]);
129+
sendCommand('setMyLocationEnabled', commands.setMyLocationEnabled, [
130+
isOn,
131+
]);
113132
},
114133

115134
setRotateGesturesEnabled: (isOn: boolean) => {
116-
sendCommand(viewId, commands.setRotateGesturesEnabled, [isOn]);
135+
sendCommand(
136+
'setRotateGesturesEnabled',
137+
commands.setRotateGesturesEnabled,
138+
[isOn]
139+
);
117140
},
118141

119142
setScrollGesturesEnabled: (isOn: boolean) => {
120-
sendCommand(viewId, commands.setScrollGesturesEnabled, [isOn]);
143+
sendCommand(
144+
'setScrollGesturesEnabled',
145+
commands.setScrollGesturesEnabled,
146+
[isOn]
147+
);
121148
},
122149

123150
setScrollGesturesEnabledDuringRotateOrZoom: (isOn: boolean) => {
124-
sendCommand(viewId, commands.setScrollGesturesEnabledDuringRotateOrZoom, [
125-
isOn,
126-
]);
151+
sendCommand(
152+
'setScrollGesturesEnabledDuringRotateOrZoom',
153+
commands.setScrollGesturesEnabledDuringRotateOrZoom,
154+
[isOn]
155+
);
127156
},
128157

129158
setZoomControlsEnabled: (isOn: boolean) => {
130-
sendCommand(viewId, commands.setZoomControlsEnabled, [isOn]);
159+
sendCommand('setZoomControlsEnabled', commands.setZoomControlsEnabled, [
160+
isOn,
161+
]);
131162
},
132163

133164
setZoomLevel: (level: number) => {
134-
sendCommand(viewId, commands.setZoomLevel, [level]);
165+
sendCommand('setZoomLevel', commands.setZoomLevel, [level]);
135166
},
136167

137168
setTiltGesturesEnabled: (isOn: boolean) => {
138-
sendCommand(viewId, commands.setTiltGesturesEnabled, [isOn]);
169+
sendCommand('setTiltGesturesEnabled', commands.setTiltGesturesEnabled, [
170+
isOn,
171+
]);
139172
},
140173

141174
setZoomGesturesEnabled: (isOn: boolean) => {
142-
sendCommand(viewId, commands.setZoomGesturesEnabled, [isOn]);
175+
sendCommand('setZoomGesturesEnabled', commands.setZoomGesturesEnabled, [
176+
isOn,
177+
]);
143178
},
144179

145180
setBuildingsEnabled: (isOn: boolean) => {
146-
sendCommand(viewId, commands.setBuildingsEnabled, [isOn]);
181+
sendCommand('setBuildingsEnabled', commands.setBuildingsEnabled, [isOn]);
147182
},
148183

149184
getCameraPosition: async (): Promise<CameraPosition> => {
@@ -163,12 +198,19 @@ export const getMapViewController = (viewId: number): MapViewController => {
163198
},
164199

165200
moveCamera: (cameraPosition: CameraPosition) => {
166-
sendCommand(viewId, commands.moveCamera, [cameraPosition]);
201+
sendCommand('moveCamera', commands.moveCamera, [cameraPosition]);
167202
},
168203

169204
setPadding: (padding: Padding) => {
170205
const { top = 0, left = 0, bottom = 0, right = 0 } = padding;
171-
sendCommand(viewId, commands.setPadding, [top, left, bottom, right]);
206+
sendCommand('setPadding', commands.setPadding, [
207+
top,
208+
left,
209+
bottom,
210+
right,
211+
]);
172212
},
173213
};
214+
215+
return { controller, dispose };
174216
};

src/navigation/navigationView/navigationView.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export const NavigationView = (
3636
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3737
const mapViewRef = useRef<any>(null);
3838
const [viewId, setViewId] = useState<number | null>(null);
39+
const navControllerDisposeRef = useRef<(() => void) | null>(null);
40+
const mapControllerDisposeRef = useRef<(() => void) | null>(null);
3941

4042
const {
4143
onMapViewControllerCreated,
@@ -54,15 +56,32 @@ export const NavigationView = (
5456
}
5557
};
5658

59+
// Create controllers when viewId changes
5760
useEffect(() => {
5861
if (!mapViewRef.current) {
5962
return;
6063
}
6164
const _viewId = findNodeHandle(mapViewRef.current) || 0;
6265
if (viewId !== _viewId) {
66+
// Dispose previous controllers if viewId actually changed
67+
if (navControllerDisposeRef.current) {
68+
navControllerDisposeRef.current();
69+
}
70+
if (mapControllerDisposeRef.current) {
71+
mapControllerDisposeRef.current();
72+
}
6373
setViewId(_viewId);
64-
onNavigationViewControllerCreated(getNavigationViewController(_viewId));
65-
onMapViewControllerCreated(getMapViewController(_viewId));
74+
75+
const { controller: navController, dispose: disposeNav } =
76+
getNavigationViewController(_viewId);
77+
const { controller: mapController, dispose: disposeMap } =
78+
getMapViewController(_viewId);
79+
80+
navControllerDisposeRef.current = disposeNav;
81+
mapControllerDisposeRef.current = disposeMap;
82+
83+
onNavigationViewControllerCreated(navController);
84+
onMapViewControllerCreated(mapController);
6685
}
6786
}, [
6887
androidStylingOptions,
@@ -72,6 +91,20 @@ export const NavigationView = (
7291
viewId,
7392
]);
7493

94+
// Cleanup on unmount
95+
useEffect(() => {
96+
return () => {
97+
if (navControllerDisposeRef.current) {
98+
navControllerDisposeRef.current();
99+
navControllerDisposeRef.current = null;
100+
}
101+
if (mapControllerDisposeRef.current) {
102+
mapControllerDisposeRef.current();
103+
mapControllerDisposeRef.current = null;
104+
}
105+
};
106+
}, []);
107+
75108
const onMapClick = useCallback(
76109
({ nativeEvent: latlng }: { nativeEvent: LatLng }) => {
77110
props.mapViewCallbacks?.onMapClick?.(latlng);

0 commit comments

Comments
 (0)