Search code examples
androidflutterkotlinarcgisarcgis-runtime

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


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.


Solution

  • 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.