Skip to content

[Android] Crash at onMapReady: applyBridgedProps -> moveToRegion -> newLatLngBounds throws "Map size can't be 0" apiexception (Pixel 9 Pro XL / Android 17) #5978

Description

@Fabiogiovannini

Summary

Production crash reported by Google Play vitals: applying initialRegion at map-ready time crashes inside the Google Maps SDK with its obfuscated apiexception — the documented "Error using newLatLngBounds(LatLngBounds, int): Map size can't be 0. Most likely, layout has not yet occurred for the map view." error.

The crash path is onMapReady → applyBridgedProps() → moveToRegion() → map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)) — i.e. MapView.java:534 → 826 → 881 in 1.27.2 (line numbers in the stack below match the released sources exactly).

All events so far come from one device: Pixel 9 Pro XL on Android 17 (API 37), repeating (9 events, same user), with the current Play-services-delivered maps renderer (policy_maps_core_dynamite@260830210). It looks like the newest OS/renderer combination has shifted the onMapReady-vs-internal-layout timing so the long-known race now fires consistently on that hardware. Expect more reports as Android 17 rolls out.

Stack trace

com.google.maps.api.android.lib6.common.apiexception.b:
  at com.google.maps.api.android.lib6.common.v.a (:com.google.android.gms.policy_maps_core_dynamite@260830210@260830203025.947725257.947725257:15)
  at com.google.maps.api.android.lib6.phoenix.camera.t.j (...:123)
  at com.google.maps.api.android.lib6.impl.ag.a (...:10)
  at com.google.maps.api.android.lib6.phoenix.camera.t.s (...:60)
  at com.google.maps.api.android.lib6.impl.cg.v (...:26)
  at com.google.android.gms.maps.internal.j.br (...:386)
  at m140.azc.onTransact (...:21)
  at android.os.Binder.transact (Binder.java:1230)
  at com.google.android.gms.internal.maps.zza.zzc (com.google.android.gms:play-services-maps@@19.1.0:2)
  at com.google.android.gms.maps.internal.zzg.moveCamera (com.google.android.gms:play-services-maps@@19.1.0:3)
  at com.google.android.gms.maps.GoogleMap.moveCamera (com.google.android.gms:play-services-maps@@19.1.0:2)
  at com.rnmaps.maps.MapView.moveToRegion (MapView.java:881)
  at com.rnmaps.maps.MapView.applyBridgedProps (MapView.java:826)
  at com.rnmaps.maps.MapView.onMapReady (MapView.java:534)
  at com.google.android.gms.maps.zzag.zzb (com.google.android.gms:play-services-maps@@19.1.0:1)
  at com.google.android.gms.maps.internal.zzas.zza (com.google.android.gms:play-services-maps@@19.1.0:5)
  at com.google.android.gms.internal.maps.zzb.onTransact (com.google.android.gms:play-services-maps@@19.1.0:3)
  at android.os.Binder.transact (Binder.java:1230)
  ...
  at android.os.Looper.loop (Looper.java:397)
  at android.app.ActivityThread.main (ActivityThread.java:9523)

Analysis

moveToRegion guards against the pre-layout case:

if (super.getHeight() <= 0 || super.getWidth() <= 0) {
    map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10));
    boundsToMove = bounds;
} else {
    map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0));   // <-- line 881, throws
    boundsToMove = null;
}

but the guard checks the outer RN view's size. The Maps SDK's camera keeps its own internal viewport size, which is registered asynchronously after the SDK attaches its internal view hierarchy (which itself happens asynchronously once the dynamite module loads). When onMapReady is delivered before that internal size registration completes, newLatLngBounds(LatLngBounds, int) — the only size-dependent CameraUpdate — throws, and since onMapReady runs on the main looper the app dies. So the crash fires exactly when the outer view is already laid out (guard passes, else-branch taken) but the internal size is still 0.

The window is normally microscopic, which is why this has been a rare-but-persistent crash across every Google Maps wrapper for years (same class as flutter/flutter#135874, and the ancient #245 / #3154 here). What's new is that on Android 17 with the current renderer it reproduces persistently on at least one mainstream device.

Two amplifying observations:

  • In our app the map most exposed is a 1×1, offscreen, alpha-0 MapView used as an engine warm-up. Its outer size (1×1) always passes the > 0 guard, so it runs the vulnerable branch on every mount, and a never-drawn view plausibly has a much wider internal-size-registration window. A tiny/offscreen <MapView initialRegion={...}/> on an Android 17 device may be a good reproduction setup.
  • animateToRegion (MapView.java:1395) calls newLatLngBounds(bounds, 0) with no size guard at all, so any animateToRegion issued around mount time has the same exposure.

Suggested fix

Mirror the intent of the existing safe branch: wrap the size-dependent call and fall back to the deferred path, which updateExtraData already knows how to re-apply with explicit dimensions (newLatLngBounds(bounds, width, height, 0)) on the next layout:

} else {
    try {
        map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0));
        boundsToMove = null;
    } catch (RuntimeException e) {
        // Internal map size not registered yet ("Map size can't be 0") — defer to
        // updateExtraData, which re-applies with explicit dimensions on next layout.
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10));
        boundsToMove = bounds;
    }
}

(catch (RuntimeException) because the concrete exception class is an obfuscated internal type of the dynamite module and can't be referenced.) The same treatment would fit animateToRegion and the fitTo* methods.

Happy to open a PR with the above if maintainers agree with the approach.

Workaround for other users

On Android, use the camera forms instead of the region forms — CameraUpdateFactory.newCameraPosition has no layout/size restriction:

  • initialCamera instead of initialRegion (on Fabric it is even applied via GoogleMapOptions at map construction, so no post-ready camera move happens at all);
  • animateCamera instead of animateToRegion for moves that can occur near mount.

Note when constructing camera objects for Android: always include pitch and headingcameraPositionFromMap reads them with ungated getDouble, so omitting them throws NoSuchKeyException.

Environment

  • react-native-maps: 1.27.2 (crash line numbers match this release exactly); the unguarded call is still present on current master
  • react-native: 0.86.0 (New Architecture / Fabric), Expo SDK 57
  • react: 19.2.3
  • play-services-maps: 19.1.0 (the library default)
  • Maps renderer (Play-services delivered): com.google.android.gms.policy_maps_core_dynamite@260830210
  • Device: Google Pixel 9 Pro XL, Android 17 (API 37) — 9 crash events, single device, repeating; no occurrences observed on older Android versions in the same app population
  • Map: default provider (Google), initialRegion set, no region/camera props

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions