Skip to content

[Bug]: Crash on android when open multiple times #3895

@FrancMihani

Description

@FrancMihani

Mapbox Implementation

Mapbox

Mapbox Version

10.16.2

React Native Version

0.73.6

Platform

Android

@rnmapbox/maps version

10.1.36

Standalone component to reproduce

import React, { forwardRef } from 'react'
import useTheme from 'hooks/useTheme'
import { setAccessToken, MapView, VectorSource, FillExtrusionLayer } from '@rnmapbox/maps'
import { MAPBOX_API_TOKEN } from 'constants/env'

type Props = typeof MapView.defaultProps

setAccessToken(MAPBOX_API_TOKEN)

const Mapbox = forwardRef<MapView, Props>((props, ref) => {
  const { isDarkMode, colors } = useTheme()
  return (
    <MapView
      ref={ref}
      style={[{ flex: 1 }, props.style]}
      {...props}
      styleURL={isDarkMode ? 'mapbox://styles/mapbox/dark-v11' : undefined}>
      <VectorSource id="composite">
        <FillExtrusionLayer
          id="3d-buildings"
          sourceLayerID="building"
          style={{
            fillExtrusionColor: colors.surface,
            fillExtrusionHeight: ['get', 'height'],
            fillExtrusionOpacity: 1,
          }}
          minZoomLevel={0}
          maxZoomLevel={200}
        />
      </VectorSource>
      {props.children}
    </MapView>
  )
})

export default Mapbox
import React, { useEffect, useMemo, useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTripCoordinates } from 'services/gps/api/trips/coordinates'
import useTheme from 'hooks/useTheme'
import moment from 'moment'
import { StyleSheet, View } from 'react-native'
import { Text } from 'react-native-paper'
import Mapbox from 'components/Mapbox'
import { Camera, LineLayer, MarkerView, ShapeSource, SkyLayer } from '@rnmapbox/maps'
import FA5Icon from 'react-native-vector-icons/FontAwesome5'
import IconButton from 'components/Buttons/IconButton'
import PlayerControl from 'components/PlayerControl'
import { Padding } from 'styles'

import { SignedInGpsScreenPropsFor } from 'navigation/types'
import { MapState } from '@rnmapbox/maps/src/components/MapView'

const toRadians = (deg: number) => (deg * Math.PI) / 180
const toDegrees = (rad: number) => (rad * 180) / Math.PI

const GpsReplayTripScreen = ({ navigation, route }: SignedInGpsScreenPropsFor<'GpsReplayTripScreen'>) => {
  const { id } = route.params
  const { colors } = useTheme()
  const [hasFollowingStarted, setHasFollowingStarted] = useState(false)
  const [isPlaying, setIsPlaying] = useState(false)
  const [progress, setProgress] = useState(0)
  const [mapState, setMapState] = useState<MapState>({
    gestures: { isGestureActive: false },
    properties: { bounds: { ne: [], sw: [] }, center: [], heading: 0, pitch: 0, zoom: 0 },
    timestamp: 0,
  })
  const { top } = useSafeAreaInsets()
  const { data, isLoading } = useTripCoordinates({ trip_id: id })
  const coordinates = useMemo(() => data?.map(item => [+item.lng, +item.lat]) || [], [data])
  const [drivingSpeed, setDrivingSpeed] = useState(0)
  const [timestamp, setTimestamp] = useState('')

  const timeRange = useMemo<number>(() => {
    if (!data || data?.length < 2) return 0
    const start = data?.[0]?.timestamp
    const end = data?.[data?.length - 1]?.timestamp

    return moment(end).diff(start, 'seconds') || 0
  }, [data])

  const position = useMemo(() => {
    const total = coordinates.length
    if (total === 0) return { coordinate: [0, 0], heading: 0 }

    const index = Math.floor(progress * (total - 1))
    const nextIndex = Math.min(index + 1, total - 1)
    const t = (progress * (total - 1)) % 1

    const currentData = data[index]
    if (currentData) {
      const speed = Number(currentData.average_speed)
      if (speed !== drivingSpeed) setDrivingSpeed(speed)

      if (currentData.timestamp !== timestamp) setTimestamp(currentData.timestamp)
    }

    const [lon1, lat1] = coordinates[index]
    const [lon2, lat2] = coordinates[nextIndex]

    // Interpolated coordinate
    const lon = lon1 + (lon2 - lon1) * t
    const lat = lat1 + (lat2 - lat1) * t

    // Heading calculation
    const dLng = toRadians(lon2 - lon1)
    const lat1Rad = toRadians(lat1)
    const lat2Rad = toRadians(lat2)

    const y = Math.sin(dLng) * Math.cos(lat2Rad)
    const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng)

    const absoluteHeading = (toDegrees(Math.atan2(y, x)) + 360) % 360

    const heading = absoluteHeading - 45 - mapState.properties.heading
    return { coordinate: [lon, lat], heading, absoluteHeading }
  }, [coordinates, data, drivingSpeed, mapState.properties.heading, progress, timestamp])

  useEffect(() => {
    const timeout = setTimeout(() => {
      setHasFollowingStarted(true)
    }, 2000)

    return () => clearTimeout(timeout)
  }, [])

  const markerSize = useMemo(() => mapState.properties.zoom * 3, [mapState.properties.zoom])
  const lineWidth = useMemo(() => mapState.properties.zoom * 0.7, [mapState.properties.zoom])

  return (
    <View style={styles.page}>
      <Mapbox style={styles.map} onCameraChanged={setMapState}>
        <Camera
          heading={position.absoluteHeading}
          zoomLevel={!hasFollowingStarted ? 16 : undefined}
          centerCoordinate={position.coordinate}
          animationDuration={isPlaying ? 300 : 1000}
          animationMode={hasFollowingStarted ? 'easeTo' : 'flyTo'}
          pitch={60}
        />
        <SkyLayer id="sky" style={{ skyType: 'atmosphere' }} />

        <ShapeSource
          id="lineSource"
          shape={{
            type: 'Feature',
            properties: {},
            geometry: {
              type: 'LineString',
              coordinates,
            },
          }}>
          <LineLayer
            id="lineLayer"
            style={{
              lineColor: colors.primary,
              lineWidth,
              lineCap: 'round',
              lineJoin: 'round',
            }}
          />
        </ShapeSource>
        <MarkerView id="moving-object" coordinate={position.coordinate}>
          <FA5Icon
            name="location-arrow"
            color={colors.blue}
            size={markerSize}
            style={{ transform: [{ rotate: `${position.heading}deg` }] }}
          />
        </MarkerView>
      </Mapbox>

      <IconButton
        style={{ top: top + 10, position: 'absolute', left: Padding }}
        containerColor={colors.onPrimary}
        icon="arrow-left"
        onPress={() => navigation.goBack()}
      />

      <PlayerControl
        timeRange={timeRange}
        Top={<Text style={{ textAlign: 'center' }}>{`${moment(timestamp).format('HH:mm')} ${drivingSpeed} km/h`}</Text>}
        disabled={isLoading}
        isPlaying={isPlaying}
        setIsPlaying={setIsPlaying}
        progress={progress}
        setProgress={setProgress}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  page: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  map: { flex: 1, width: '100%' },
})

export default GpsReplayTripScreen

Mapbox.txt

GpsReplayTripScreen.txt

Observed behavior and steps to reproduce

I navigate to the map screen 2 or 3 times, sometime even more and the app crashes

Expected behavior

The app should not crash

Notes / preliminary analysis

I think the app has a memory leaks and it fails to clean up the map after the user navigates away from the map screen.

freeAllBuffers: 1 buffers were freed while being dequeued! getSlotFromBufferLocked: unknown buffer: 0x0 onDropViewInstance: view [1639] has a context that is not a ThemedReactContext: com.app.name.MainActivity@f1b4b5

Additional links and references

Screen.Recording.2025-07-02.at.6.12.56.PM.mov

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions