Skip to content

Commit 6503289

Browse files
authored
Merge pull request #98 from tappeddev/fix-memory-leak
fix-memory-leak
2 parents aa37a15 + f6c954f commit 6503289

File tree

6 files changed

+67
-39
lines changed

6 files changed

+67
-39
lines changed

arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapPlugin.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
package dev.fluttercommunity.arcgis_map_sdk_android
22

3+
import android.util.Log
4+
import androidx.lifecycle.Lifecycle
35
import io.flutter.embedding.engine.plugins.FlutterPlugin
6+
import io.flutter.embedding.engine.plugins.activity.ActivityAware
7+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
8+
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
9+
10+
11+
class ArcgisMapPlugin() : FlutterPlugin, ActivityAware {
12+
private var lifecycle: Lifecycle? = null
13+
14+
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
15+
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding)
16+
}
17+
18+
override fun onDetachedFromActivityForConfigChanges() {}
19+
20+
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
21+
22+
override fun onDetachedFromActivity() {
23+
lifecycle = null
24+
}
425

5-
class ArcgisMapPlugin : FlutterPlugin {
626
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
727
flutterPluginBinding
828
.platformViewRegistry
929
.registerViewFactory(
1030
"<native_map_view>",
11-
ArcgisMapViewFactory(flutterPluginBinding)
31+
ArcgisMapViewFactory(flutterPluginBinding) { lifecycle }
1232
)
1333
}
1434

arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/ArcgisMapView.kt

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
55
import android.view.LayoutInflater
66
import android.view.View
77
import androidx.lifecycle.Lifecycle
8+
import androidx.lifecycle.LifecycleOwner
89
import androidx.lifecycle.coroutineScope
910
import com.arcgismaps.ApiKey
1011
import com.arcgismaps.ArcGISEnvironment
@@ -40,6 +41,9 @@ import io.flutter.plugin.common.EventChannel
4041
import io.flutter.plugin.common.MethodCall
4142
import io.flutter.plugin.common.MethodChannel
4243
import io.flutter.plugin.platform.PlatformView
44+
import kotlinx.coroutines.CoroutineScope
45+
import kotlinx.coroutines.Dispatchers
46+
import kotlinx.coroutines.SupervisorJob
4347
import kotlinx.coroutines.cancel
4448
import kotlinx.coroutines.launch
4549
import java.io.ByteArrayOutputStream
@@ -57,8 +61,10 @@ internal class ArcgisMapView(
5761
private val viewId: Int,
5862
private val mapOptions: ArcgisMapOptions,
5963
private val binding: FlutterPluginBinding,
60-
private val lifecycle: Lifecycle,
61-
) : PlatformView {
64+
override val lifecycle: Lifecycle,
65+
) : PlatformView, LifecycleOwner {
66+
67+
private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
6268

6369
private val view: View = LayoutInflater.from(context).inflate(R.layout.vector_map_view, null)
6470
private var mapView: MapView
@@ -97,23 +103,23 @@ internal class ArcgisMapView(
97103

98104
minScale = getMapScale(mapOptions.minZoom)
99105
maxScale = getMapScale(mapOptions.maxZoom)
100-
lifecycle.coroutineScope.launch {
106+
coroutineScope.launch {
101107
loadStatus.collect(::onLoadStatusChanged)
102108
}
103109
}
104110

105111
mapView.map = map
106112
mapView.graphicsOverlays.add(defaultGraphicsOverlay)
107113

108-
lifecycle.coroutineScope.launch {
114+
coroutineScope.launch {
109115
mapView.mapScale.collect { scale ->
110116
if (scale.isNaN()) return@collect
111117

112118
val zoomLevel = getZoomLevel(mapView)
113119
zoomStreamHandler.addZoom(zoomLevel)
114120
}
115121
}
116-
lifecycle.coroutineScope.launch {
122+
coroutineScope.launch {
117123
mapView.viewpointChanged.collect {
118124
// The viewpoint listener is executed async which means that the map
119125
// can be altered when this is called. If we reload the map or dispose the map
@@ -149,7 +155,10 @@ internal class ArcgisMapView(
149155
methodChannel.invokeMethod("onStatusChanged", status.jsonValue())
150156
}
151157

152-
override fun dispose() {}
158+
override fun dispose() {
159+
coroutineScope.cancel()
160+
mapView.onDestroy(this)
161+
}
153162

154163
// region helper
155164

@@ -220,7 +229,7 @@ internal class ArcgisMapView(
220229
}
221230

222231
private fun onStartLocationDisplayDataSource(result: MethodChannel.Result) {
223-
lifecycle.coroutineScope.launch {
232+
coroutineScope.launch {
224233
mapView.locationDisplay.dataSource.start().onSuccess {
225234
result.success(true)
226235
}.onFailure { e ->
@@ -231,7 +240,7 @@ internal class ArcgisMapView(
231240

232241

233242
private fun onStopLocationDisplayDataSource(result: MethodChannel.Result) {
234-
lifecycle.coroutineScope.launch {
243+
coroutineScope.launch {
235244
mapView.locationDisplay.dataSource.stop().onSuccess {
236245
result.success(true)
237246
}.onFailure { e ->
@@ -280,7 +289,7 @@ internal class ArcgisMapView(
280289
val provider = dataSource.currentProvider as CustomLocationProvider
281290
val optionParams = call.arguments as Map<String, Any>
282291
val position = optionParams.parseToClass<UserPosition>()
283-
lifecycle.coroutineScope.launch {
292+
coroutineScope.launch {
284293
provider.updateLocation(position)
285294
result.success(true)
286295
}
@@ -431,7 +440,7 @@ internal class ArcgisMapView(
431440
return
432441
}
433442
val newScale = getMapScale(totalZoomLevel)
434-
lifecycle.coroutineScope.launch {
443+
coroutineScope.launch {
435444
mapView.setViewpointScale(newScale).onSuccess {
436445
result.success(true)
437446
}.onFailure { e ->
@@ -456,7 +465,7 @@ internal class ArcgisMapView(
456465
return
457466
}
458467
val newScale = getMapScale(totalZoomLevel)
459-
lifecycle.coroutineScope.launch {
468+
coroutineScope.launch {
460469
mapView.setViewpointScale(newScale).onSuccess {
461470
result.success(true)
462471
}.onFailure { e ->
@@ -468,7 +477,7 @@ internal class ArcgisMapView(
468477

469478
private fun onRotate(call: MethodCall, result: MethodChannel.Result) {
470479
val angleDegrees = call.arguments as Double
471-
lifecycle.coroutineScope.launch {
480+
coroutineScope.launch {
472481
mapView.setViewpointRotation(angleDegrees).onSuccess {
473482
result.success(true)
474483
}.onFailure { e ->
@@ -518,7 +527,7 @@ internal class ArcgisMapView(
518527

519528
defaultGraphicsOverlay.graphics.addAll(newGraphic)
520529

521-
lifecycle.coroutineScope.launch {
530+
coroutineScope.launch {
522531
updateMap().onSuccess { updateResult ->
523532
result.success(updateResult)
524533
}.onFailure { e ->
@@ -540,7 +549,7 @@ internal class ArcgisMapView(
540549

541550
// Don't use removeAll because this will not trigger a redraw.
542551
graphicsToRemove.forEach(defaultGraphicsOverlay.graphics::remove)
543-
lifecycle.coroutineScope.launch {
552+
coroutineScope.launch {
544553
updateMap().onSuccess {
545554
result.success(true)
546555
}.onFailure { e ->
@@ -570,7 +579,7 @@ internal class ArcgisMapView(
570579
}
571580

572581
val initialViewPort = Viewpoint(point.latitude, point.longitude, scale)
573-
lifecycle.coroutineScope.launch {
582+
coroutineScope.launch {
574583
mapView.setViewpointAnimated(
575584
initialViewPort,
576585
(animationOptions?.duration?.toFloat() ?: 0F) / 1000,
@@ -603,7 +612,7 @@ internal class ArcgisMapView(
603612
}, SpatialReference.wgs84()
604613
)
605614

606-
lifecycle.coroutineScope.launch {
615+
coroutineScope.launch {
607616
val viewpointResult = if (padding != null) {
608617
mapView.setViewpointGeometry(polyline.extent, padding)
609618
} else {
@@ -633,7 +642,7 @@ internal class ArcgisMapView(
633642
}
634643

635644
private fun onRetryLoad(result: MethodChannel.Result) {
636-
lifecycle.coroutineScope.launch {
645+
coroutineScope.launch {
637646
mapView.map?.retryLoad()?.onSuccess {
638647
result.success(true)
639648
}?.onFailure { e ->
@@ -643,7 +652,7 @@ internal class ArcgisMapView(
643652
}
644653

645654
private fun onExportImage(result: MethodChannel.Result) {
646-
lifecycle.coroutineScope.launch {
655+
coroutineScope.launch {
647656
mapView.exportImage().onSuccess { bitmapResult ->
648657
val bitmap = bitmapResult.bitmap
649658
val stream = ByteArrayOutputStream()
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
11
package dev.fluttercommunity.arcgis_map_sdk_android
22

33
import android.content.Context
4-
import android.content.ContextWrapper
54
import androidx.lifecycle.Lifecycle
6-
import androidx.lifecycle.LifecycleOwner
75
import dev.fluttercommunity.arcgis_map_sdk_android.model.ArcgisMapOptions
8-
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
6+
import io.flutter.embedding.engine.plugins.FlutterPlugin
97
import io.flutter.plugin.common.StandardMessageCodec
108
import io.flutter.plugin.platform.PlatformView
119
import io.flutter.plugin.platform.PlatformViewFactory
1210

1311
class ArcgisMapViewFactory(
14-
private val flutterPluginBinding: FlutterPluginBinding,
12+
private val binding: FlutterPlugin.FlutterPluginBinding,
13+
private val getLifecycle: () -> Lifecycle?,
1514
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
15+
1616
override fun create(context: Context?, viewId: Int, args: Any?): PlatformView {
1717
val optionParams = args as Map<String, Any>
1818
val params = optionParams.parseToClass<ArcgisMapOptions>()
1919

20-
return ArcgisMapView(
21-
context!!, viewId, params, flutterPluginBinding, getLifecycle(context)!!
22-
)
23-
}
24-
25-
private fun getLifecycle(context: Context): Lifecycle? {
26-
var currentContext = context
27-
while (currentContext is ContextWrapper) {
28-
if (currentContext is LifecycleOwner) return currentContext.lifecycle
29-
currentContext = currentContext.baseContext
30-
}
31-
return null
20+
return ArcgisMapView(context!!, viewId, params, binding, getLifecycle()!!)
3221
}
3322
}

arcgis_map_sdk_android/android/src/main/kotlin/dev/fluttercommunity/arcgis_map_sdk_android/CustomLocationProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CustomLocationProvider : CustomLocationDataSource.LocationProvider {
1515
override val locations: Flow<Location> = _locations.asSharedFlow()
1616
override val headings: Flow<Double> = _headings.asSharedFlow()
1717

18-
suspend fun updateLocation(position: UserPosition) {
18+
fun updateLocation(position: UserPosition) {
1919
_locations.tryEmit(
2020
Location.create(
2121
position = position.latLng.toAGSPoint(),
@@ -27,7 +27,7 @@ class CustomLocationProvider : CustomLocationDataSource.LocationProvider {
2727
)
2828
)
2929
position.heading?.let {
30-
_headings.emit(it)
30+
_headings.tryEmit(it)
3131
}
3232
}
3333
}

arcgis_map_sdk_android/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies:
1515
flutter:
1616
sdk: flutter
1717

18+
flutter_plugin_android_lifecycle: ^2.0.28
19+
1820
dev_dependencies:
1921
lint: ^2.8.0
2022

example/pubspec.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ packages:
120120
description: flutter
121121
source: sdk
122122
version: "0.0.0"
123+
flutter_plugin_android_lifecycle:
124+
dependency: transitive
125+
description:
126+
name: flutter_plugin_android_lifecycle
127+
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
128+
url: "https://pub.dev"
129+
source: hosted
130+
version: "2.0.28"
123131
flutter_test:
124132
dependency: "direct dev"
125133
description: flutter
@@ -377,4 +385,4 @@ packages:
377385
version: "1.1.1"
378386
sdks:
379387
dart: ">=3.7.0 <4.0.0"
380-
flutter: ">=3.18.0-18.0.pre.54"
388+
flutter: ">=3.27.0"

0 commit comments

Comments
 (0)