Flutter: ArcGIS Map View as a Platform View Crashes on Android

348 Views Asked by At

I am trying to host a native MapView from the ArcGIS Maps SDK for Kotlin v200.1 inside a Flutter app.

This is my main.dart file, which simply displays a custom MapView widget inside a Scaffold:

// lib/main.dart
import 'package:flutter/material.dart';
import 'map_view.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ArcGIS Map View'),
        ),
        body: const Center(
          child: MapView()
        ),
      ),
    );
  }
}

The MapView widget looks like so:

// lib/map_view.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class MapView extends StatelessWidget {
  const MapView({super.key});

  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.iOS) {
      return const UiKitView(viewType: _viewType);
    } else if (defaultTargetPlatform == TargetPlatform.android) {
      return const AndroidView(viewType: _viewType);
    } else {
      throw UnsupportedError('Platform not supported');
    }
  }

  static const _viewType = 'mapView';
}

On the Android side, I try to create the ArcGIS map view as part of a PlatformView:

// android/app/src/main/kotlin/com/example/test/NativeView.kt
package com.example.test

import android.content.Context
import android.view.View

import io.flutter.plugin.platform.PlatformView

import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.view.MapView

internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val mapView: MapView

    init {
        ArcGISEnvironment.apiKey = ApiKey.create("<hidden>")

        mapView = MapView(context)
        mapView.map = ArcGISMap(BasemapStyle.ArcGISTopographic)
        mapView.setViewpoint(Viewpoint(34.0270, -118.8050, 72000.0))
    }

    override fun getView(): View {
        return mapView
    }

    override fun dispose() {}
}

The NativeView is instantiated by a NativeViewFactory as described in the Flutter documentation (Hosting a native Android view).

When running the Flutter app in an Android emulator, it crashes on startup with an exception saying "lateinit property lifeCycleOwner has not been initialized":

A Dart VM Service on sdk gphone64 x86 64 is available at: http://127.0.0.1:65061/zcoQFFeeeZk=/
The Flutter DevTools debugger and profiler on sdk gphone64 x86 64 is available at:
http://127.0.0.1:9102?uri=http://127.0.0.1:65061/zcoQFFeeeZk=/
I/PlatformViewsController(10555): Hosting view in view hierarchy for platform view: 0
I/Choreographer(10555): Skipped 62 frames!  The application may be doing too much work on its main thread.
E/FrameEvents(10555): updateAcquireFence: Did not find frame.
W/Parcel  (10555): Expecting binder but got null!
I/TextureView(10555): onSurfaceTextureAvailable
D/AndroidRuntime(10555): Shutting down VM
E/FrameEvents(10555): updateAcquireFence: Did not find frame.
E/AndroidRuntime(10555): FATAL EXCEPTION: main
E/AndroidRuntime(10555): Process: com.example.test, PID: 10555
E/AndroidRuntime(10555): kotlin.UninitializedPropertyAccessException: lateinit property lifeCycleOwner has not been initialized
E/AndroidRuntime(10555):        at com.arcgismaps.mapping.view.GeoView.getLifeCycleOwner$api_release(GeoView.kt:110)
E/AndroidRuntime(10555):        at com.arcgismaps.mapping.view.GeoView$RenderingThread.onSurfaceTextureAvailable(GeoView.kt:1630)
E/AndroidRuntime(10555):        at android.view.TextureView.getTextureLayer(TextureView.java:466)
E/AndroidRuntime(10555):        at android.view.TextureView.draw(TextureView.java:415)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22061)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22052)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:23197)
E/AndroidRuntime(10555):        at io.flutter.plugin.platform.PlatformViewWrapper.draw(PlatformViewWrapper.java:305)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22061)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22052)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22052)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22052)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:22925)
E/AndroidRuntime(10555):        at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
E/AndroidRuntime(10555):        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
E/AndroidRuntime(10555):        at android.view.View.draw(View.java:23197)
E/AndroidRuntime(10555):        at com.android.internal.policy.DecorView.draw(DecorView.java:821)
E/AndroidRuntime(10555):        at android.view.View.updateDisplayListIfDirty(View.java:22061)
E/AndroidRuntime(10555):        at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:689)
E/AndroidRuntime(10555):        at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:695)
E/AndroidRuntime(10555):        at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:793)
E/AndroidRuntime(10555):        at android.view.ViewRootImpl.draw(ViewRootImpl.java:4670)
E/AndroidRuntime(10555):        at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4381)
E/AndroidRuntime(10555):        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3600)
E/AndroidRuntime(10555):        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2328)
E/AndroidRuntime(10555):        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9087)
E/AndroidRuntime(10555):        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1231)
E/AndroidRuntime(10555):        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1239)
E/AndroidRuntime(10555):        at android.view.Choreographer.doCallbacks(Choreographer.java:899)
E/AndroidRuntime(10555):        at android.view.Choreographer.doFrame(Choreographer.java:832)
E/AndroidRuntime(10555):        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1214)
E/AndroidRuntime(10555):        at android.os.Handler.handleCallback(Handler.java:942)
E/AndroidRuntime(10555):        at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(10555):        at android.os.Looper.loopOnce(Looper.java:201)
E/AndroidRuntime(10555):        at android.os.Looper.loop(Looper.java:288)
E/AndroidRuntime(10555):        at android.app.ActivityThread.main(ActivityThread.java:7872)
E/AndroidRuntime(10555):        at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime(10555):        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
E/AndroidRuntime(10555):        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
D/TrafficStats(10555): tagSocket(123) with statsTag=0xffffffff, statsUid=-1
I/Process (10555): Sending signal. PID: 10555 SIG: 9
Lost connection to device.

When instead using the older ArcGIS Runtime SDK for Android v100.15.2, it will work as expected:

// android/app/src/main/kotlin/com/example/test/NativeView.kt
package com.example.test

import android.content.Context
import android.view.View

import io.flutter.plugin.platform.PlatformView

import com.esri.arcgisruntime.ArcGISRuntimeEnvironment
import com.esri.arcgisruntime.mapping.ArcGISMap
import com.esri.arcgisruntime.mapping.view.MapView
import com.esri.arcgisruntime.mapping.BasemapStyle
import com.esri.arcgisruntime.mapping.Viewpoint

internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val mapView: MapView

    init {
        ArcGISRuntimeEnvironment.setApiKey("<hidden>")

        mapView = MapView(context)
        mapView.map = ArcGISMap(BasemapStyle.ARCGIS_TOPOGRAPHIC)
        mapView.setViewpoint(Viewpoint(34.0270, -118.8050, 72000.0))
    }

    override fun getView(): View {
        return mapView
    }

    override fun dispose() {}
}

Upon launch, I can see the map view being displayed:

Map View

I am not an experienced Android developer. Does anyone know what's wrong with the v200.1 approach?

Version info:

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.10.6, on macOS 13.4.1 22F770820d darwin-x64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.3)
[✓] VS Code (version 1.80.1)
[✓] VS Code (version 1.81.0-insider)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

EDIT: There's someone trying to do the exact same thing using React Native and getting the same runtime exception on startup (StackOverflow post). However, no one has replied to that post unfortunately.

2

There are 2 best solutions below

1
goofy4224 On BEST ANSWER

The solution is actually quite simple. As Nguyen Dinh Thanh Nhan suggested, the map view needs to be registered as a lifecycle observer. This can most easily be done be grabbing an instance to the main activity of the app and using its lifecycle property:

internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?): PlatformView {
    private val mapView: MapView

    init {
        ArcGISEnvironment.apiKey = ApiKey.create("<hidden>")

        mapView = MapView(context)
        mapView.map = ArcGISMap(BasemapStyle.ArcGISTopographic)
        mapView.setViewpoint(Viewpoint(34.0270, -118.8050, 72000.0))

        // To be added:
        MainActivity.getInstance().lifecycle.addObserver(mapView)
    }

    override fun getView(): View {
        return mapView
    }

    override fun dispose() {}
}

For this, one should let the main activity class adopt the singleton pattern:

class MainActivity: FlutterActivity() {
    init {
        instance = this
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        val factory = NativeMapViewFactory()
        flutterEngine.platformViewsController.registry.registerViewFactory("mapView", factory)
    }

    companion object {
        fun getInstance(): MainActivity {
            return instance
        }
        lateinit private var instance: MainActivity
    }
}

This way, the NativeView class does not need to inherit from AppCompatActivity.

EDIT: If developing a plugin, use the flutter_plugin_android_lifecycle package to get a lifecycle reference.

1
Nguyen Dinh Thanh Nhan On

Arcgis SDK version 200.15.2 requires your activity to extend AppCompatActivity() and have lifecycle observe your mapView

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycle.addObserver(mapView) // <= you need this
    }

I have encountered the same problem with you while using Arcgis with Platformview and come up with 2 methods:

  1. Extends both PlatformView and AppCompatActivity() -> I haven't tested this method yet
internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView, AppCompatActivity() {
    private val mapView: MapView

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  lifecycle.addObserver(mapView) // <= you need this
}
  1. Not use Platformview, instead create an Activity that extends AppCompatActivity() and use invokeMethodChannel from Flutter to open that activity -> this works for me