I have the following problem, I am making a Flutter application using the library implementation "com.esri:arcgis-maps-kotlin:200.1.0"using Platform Channel and Platform Views.
I am trying to use the FeatureLayers download methods used in this example from Esri repositories: link.
My Platform Channel method in Flutter is this:
Future<void> downloadServiceFeatureTableList(List<ArcGISMapServiceFeatureLayer> arcGISMapServiceFeatureLayerList) async {
try {
setState(() { isDowloading = true; });
final response = await _channel.invokeListMethod(
'downloadServiceFeatureTable',
arcGISMapServiceFeatureLayerList.map((layer) => layer.toMap()).toList()
);
print(response);
} on PlatformException catch(e) {
if(kDebugMode) print(e);
}
setState(() { isDowloading = false; });
}
My MainActivity in the Android folder:
package com.example.app
import com.example.app.entities.ArcGISMapServiceFeatureLayer
import com.example.app.utils.DownloaderFromArcGISOnline
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import org.json.JSONObject
const val BASECHANNEL = "com.example.app:"
const val ARCGISMAPCHANNEL = "ArcGISMap-"
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.platformViewsController.registry.registerViewFactory(
"ArcGISMapView",
ArcgisMapViewFactory(lifecycle, flutterEngine.dartExecutor.binaryMessenger)
)
val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BASECHANNEL)
methodChannel.setMethodCallHandler { call, channelResult ->
when(call.method) {
"downloadServiceFeatureTable" -> {
@Suppress("UNCHECKED_CAST")
val arguments = call.arguments as List<Map<*, *>>
val arcGISMapServiceFeatureTableList = mutableListOf<ArcGISMapServiceFeatureLayer>();
arguments.forEach {
arcGISMapServiceFeatureTableList.add(Gson().fromJson(JSONObject(it).toString(), ArcGISMapServiceFeatureLayer::class.java))
}
val downloaderFromArcGISOnline = DownloaderFromArcGISOnline(context, "Offline Features", arcGISMapServiceFeatureTableList)
downloaderFromArcGISOnline.download { downloadStatus ->
val jsonDownloadStatus = mutableListOf<String>()
val gson = Gson()
downloadStatus.forEach { item ->
jsonDownloadStatus.add(gson.toJson(item))
}
channelResult.success(jsonDownloadStatus)
}
}
else -> channelResult.notImplemented()
}
}
}
}
So far all this is working correctly, the problem is inside the execution of the function downloaderFromArcGISOnline.download{...}.
This is the code of the DownloaderFromArcGISOnline class:
package com.example.app.utils
import android.content.Context
import android.util.Log
import com.arcgismaps.mapping.PortalItem
import com.example.app.entities.ArcGISMapServiceFeatureLayer
import com.example.app.entities.DownloadPortalIemStatus
import com.example.app.entities.DownloadStatus
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@OptIn(DelicateCoroutinesApi::class)
class DownloaderFromArcGISOnline(
private val context: Context,
private val folderName: String,
private val featureLayerUrls: List<ArcGISMapServiceFeatureLayer>,
private val externalScope: CoroutineScope = GlobalScope,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
fun download(callback: (List<DownloadPortalIemStatus>) -> Unit) {
externalScope.launch(dispatcher) {
callback(downloadFeatureLayerList())
}
}
private fun getDownloaderFolder(): String {
return context.getExternalFilesDir(null)?.absolutePath.toString()+File.separator+folderName
}
private suspend fun downloadFeatureLayerList(): List<DownloadPortalIemStatus> {
val downloadStatusResult = mutableListOf<DownloadPortalIemStatus>()
val folderPath = getDownloaderFolder()
val provisionFolder = File(folderPath)
if (!provisionFolder.exists()) {
provisionFolder.mkdirs()
} else {
FileUtils.cleanDirectory(provisionFolder)
}
fun setOnFailed() {
downloadStatusResult.add(DownloadPortalIemStatus(DownloadStatus.failed, ""))
}
featureLayerUrls.forEach { layerLayer ->
val portalItem = PortalItem(layerLayer.url)
Log.e("INFO", "val portalItem = PortalItem(layerLayer.url)")
portalItem.load().onSuccess {
Log.e("INFO", "portalItem.load().onSuccess {")
portalItem.fetchData().onSuccess { byteArrayData ->
Log.e("INFO", "portalItem.fetchData().onSuccess { byteArrayData ->")
var filePathUri = ""
kotlin.runCatching {
val byteArrayInputStream = ByteArrayInputStream(byteArrayData)
val data = ByteArray(1024)
var downloadCount: Int
downloadCount = byteArrayInputStream.read(data)
while (downloadCount != -1) {
downloadCount = byteArrayInputStream.read(data)
}
Log.e("INFO", "while (downloadCount != -1) {")
val destinationFilePath = provisionFolder.path + File.separator + portalItem.name
val provisionFile = File(destinationFilePath)
provisionFile.createNewFile()
Log.e("INFO", "provisionFile.createNewFile()")
val writerOutputStream = FileOutputStream(provisionFile)
writerOutputStream.write(byteArrayData)
Log.e("INFO", "writerOutputStream.write(byteArrayData)")
if (portalItem.name.contains(".zip")) {
val fileInputStream = FileInputStream(destinationFilePath)
val zipInputStream = ZipInputStream(BufferedInputStream(fileInputStream))
var zipEntry: ZipEntry? = zipInputStream.nextEntry
val buffer = ByteArray(1024)
Log.e("INFO", "val buffer = ByteArray(1024)")
while (zipEntry != null) {
if (zipEntry.isDirectory) {
File(provisionFolder.path, zipEntry.name).mkdirs()
} else {
val file = File(provisionFolder.path, zipEntry.name)
val fout = FileOutputStream(file)
var count = zipInputStream.read(buffer)
while (count != -1) {
fout.write(buffer, 0, count)
count = zipInputStream.read(buffer)
}
fout.close()
filePathUri = file.path;
}
zipInputStream.closeEntry()
zipEntry = zipInputStream.nextEntry
}
Log.e("INFO", "while (zipEntry != null) {")
zipInputStream.close()
FileUtils.delete(provisionFile)
Log.e("INFO", "FileUtils.delete(provisionFile)")
} else {
filePathUri = destinationFilePath
}
downloadStatusResult.add(DownloadPortalIemStatus(DownloadStatus.success, filePathUri))
}.onFailure { setOnFailed()
Log.e("INFO", "}.onFailure { setOnFailed()") }
}.onFailure { setOnFailed()
Log.e("INFO", ".getOrElse { setOnFailed() 1") }
}.onFailure { setOnFailed()
Log.e("INFO", ".getOrElse { setOnFailed() 2") }
}
return downloadStatusResult
}
}
The problem is that sometimes the code execution stops and never reaches the return of the downloadFeatureLayerList method. And the last printout on the console when execution stops is always Log.e("INFO", "portalItem.load().onSuccess {").
On the other hand, most of the times the download of each of the URLs of the FeatureLayers is executed correctly, the code arrives to the return of the downloadFeatureLayerList function and the callback is executed; ending with the response of the Platform Channel in my Flutter code with the path of the downloaded files.
I already used the kotlinx.coroutines.flow.Flow as they are in the Esri repository example and I have the same result.
My version of Flutter is 3.10.5 and my version of Kotlin is 1.7.10.