googlemaps / react-native-navigation-sdk

Apache License 2.0
25 stars 4 forks source link

<NavigationView /> is not initialized correctly after remount #215

Closed jmcguiresignifyhealth closed 3 months ago

jmcguiresignifyhealth commented 3 months ago

Environment details

OS: iOS 17.4 Simulator SDK Version: 0.4.0

Steps to reproduce

  1. Render <NavigationView /> and use navigationController.setDestination to add a destination
  2. Map will then show the expected route and destination
  3. Dismount and then remount the component.
  4. Map will not show current location. Moreover, setDestination does not have any affect on the map.

https://github.com/user-attachments/assets/1b7bf1f6-f23a-4875-990c-e2ceaaabbbf6

Code example

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, View } from 'react-native';

import withErrorHandling from '@hoc/withErrorHandling';
import {
  ArrivalEvent,
  Circle,
  LatLng,
  MapViewCallbacks,
  MapViewController,
  Marker,
  NavigationInitErrorCode,
  NavigationView,
  NavigationViewCallbacks,
  NavigationViewController,
  Polygon,
  Polyline,
  RouteStatus,
  RoutingOptions,
  TravelMode,
  useNavigation as useNavigationSdk,
  Waypoint,
} from '@signifyhealth/react-native-navigation-sdk';

const Route: React.FC<JSX.Element> = () => {
  const [mapViewController, setMapViewController] =
    useState<MapViewController | null>(null);
  const [navigationViewController, setNavigationViewController] =
    useState<NavigationViewController | null>(null);

  const { navigationController, addListeners, removeListeners } =
    useNavigationSdk();

  const onArrival = useCallback(
    (event: ArrivalEvent) => {
      if (event.isFinalDestination) {
        navigationController.stopGuidance();
      } else {
        navigationController.continueToNextDestination();
        navigationController.startGuidance();
      }
    },
    [navigationController]
  );

  const onTrafficUpdated = useCallback(() => {}, []);

  const onNavigationReady = useCallback(() => {
  }, []);

  const onNavigationInitError = useCallback(
    (errorCode: NavigationInitErrorCode) => {},
    []
  );

  const onStartGuidance = useCallback(() => {}, []);

  const onRouteStatusOk = useCallback(() => {}, []);

  const onRouteCancelled = useCallback(() => {}, []);

  const onNoRouteFound = useCallback(() => {}, []);

  const onNetworkError = useCallback(() => {}, []);

  const onStartingGuidanceError = useCallback(() => {}, []);

  const onLocationDisabled = useCallback(() => {}, []);

  const onLocationUnknown = useCallback(() => {}, []);

  const onLocationChanged = useCallback((location: Location) => {
  }, []);

  const onRawLocationChanged = useCallback((location: Location) => {
  }, []);

  const onTurnByTurn = useCallback((turnByTurn: any) => {
  }, []);

  const onRouteChanged = useCallback(() => {}, []);

  const onRemainingTimeOrDistanceChanged = useCallback(async () => {
    if (navigationController) {
      const currentTimeAndDistance =
        await navigationController.getCurrentTimeAndDistance();
      console.log(currentTimeAndDistance);
    }
  }, [navigationController]);

  const onRouteStatusResult = useCallback(
    (routeStatus: RouteStatus) => {
      switch (routeStatus) {
        case RouteStatus.OK:
          onRouteStatusOk();
          break;
        case RouteStatus.ROUTE_CANCELED:
          onRouteCancelled();
          break;
        case RouteStatus.NO_ROUTE_FOUND:
          onNoRouteFound();
          break;
        case RouteStatus.NETWORK_ERROR:
          onNetworkError();
          break;
        case RouteStatus.LOCATION_DISABLED:
          onLocationDisabled();
          break;
        case RouteStatus.LOCATION_UNKNOWN:
          onLocationUnknown();
          break;
        default:
          onStartingGuidanceError();
      }
    },
    [
      onRouteStatusOk,
      onRouteCancelled,
      onNoRouteFound,
      onNetworkError,
      onLocationDisabled,
      onLocationUnknown,
      onStartingGuidanceError,
    ]
  );

  const navigationCallbacks: any = useMemo(
    () => ({
      onRouteChanged,
      onArrival,
      onNavigationReady,
      onNavigationInitError,
      onLocationChanged,
      onRawLocationChanged,
      onTrafficUpdated,
      onRouteStatusResult,
      onStartGuidance,
      onRemainingTimeOrDistanceChanged,
      onTurnByTurn,
    }),
    [
      onRouteChanged,
      onArrival,
      onNavigationReady,
      onNavigationInitError,
      onLocationChanged,
      onRawLocationChanged,
      onTrafficUpdated,
      onRouteStatusResult,
      onStartGuidance,
      onRemainingTimeOrDistanceChanged,
      onTurnByTurn,
    ]
  );

  useEffect(() => {
    console.log('NavigationView mounted');

    return () => {
      console.log('NavigationView dismounted');
    };
  }, []);

  useEffect(() => {
    removeListeners(navigationCallbacks);

    addListeners(navigationCallbacks);
    return () => {
      removeListeners(navigationCallbacks);
      navigationController?.clearDestinations();
      navigationController?.cleanup();
    };
  }, [navigationCallbacks, addListeners, removeListeners]);

  const onMapReady = useCallback(async () => {
    try {
      await navigationController.init();
    } catch (error) {
    }
  }, [navigationController]);

  const onRecenterButtonClick = useCallback(() => {
  }, []);

  const navigationViewCallbacks: NavigationViewCallbacks = {
    onRecenterButtonClick,
  };

  const mapViewCallbacks: MapViewCallbacks = useMemo(() => {
    return {
      onMapReady: () => {
        onMapReady();
      },
      onMarkerClick: (marker: Marker) => {
        mapViewController?.removeMarker(marker.id);
      },
      onPolygonClick: (polygon: Polygon) => {
        mapViewController?.removePolygon(polygon.id);
      },
      onCircleClick: (circle: Circle) => {
        mapViewController?.removeCircle(circle.id);
      },
      onPolylineClick: (polyline: Polyline) => {
        mapViewController?.removePolyline(polyline.id);
      },
      onMarkerInfoWindowTapped: (marker: Marker) => {
      },
      onMapClick: (latLng: LatLng) => {
      },
    };
  }, [mapViewController, onMapReady]);

  return (
    <>
      <NavigationView
        width={500}
        height={300}
        androidStylingOptions={{
          primaryDayModeThemeColor: '#34eba8',
          headerDistanceValueTextColor: '#76b5c5',
          headerInstructionsFirstRowTextSize: '20f',
        }}
        iOSStylingOptions={{
          navigationHeaderPrimaryBackgroundColor: '#34eba8',
          navigationHeaderDistanceValueTextColor: '#76b5c5',
        }}
        navigationViewCallbacks={navigationViewCallbacks}
        mapViewCallbacks={mapViewCallbacks}
        onMapViewControllerCreated={setMapViewController}
        onNavigationViewControllerCreated={setNavigationViewController}
      />
      <View
        style={{
          position: 'relative',
          top: 550,
        }}
      >
        <Button
          onPress={async () => {
            await navigationController?.clearDestinations();
            const waypoint: Waypoint = {
              title: 'Town hall',
              position: {
                lat: Number(41.13670832163134),
                lng: Number(-111.93934122631714),
              },
            };

            const routingOptions: RoutingOptions = {
              travelMode: TravelMode.DRIVING,
              avoidFerries: true,
              avoidTolls: false,
            };

            await navigationController
              .setDestination(waypoint, routingOptions)
              .then(() => {
                navigationViewController?.showRouteOverview();
              });

            console.log('set destination');
          }}
          title="Set Destination"
        />
      </View>
    </>
  );
};

export default withErrorHandling(Route, 'PROVIDER_NAVIGATION');
jokerttu commented 3 months ago

Thanks for reporting this!

jokerttu commented 3 months ago

Hi @jmcguiresignifyhealth,

You are clearing the destinations and cleaning up the navigation state when the view/route is disposed:

      navigationController?.clearDestinations();
      navigationController?.cleanup(); 

As a result, the navigation state is cleared each time the Route is disposed.

I checked the behavior with a modified example app, and found that the NavigationView is automatically attached to the active navigation session if there is one initialized as it should.

I suggest moving the navigation handling outside of the Route to maintain a global navigation state. Note that navigation can be initialized even without the NavigationView.

Closing this issue.