diff --git a/samples/generate-geodatabase-replica-from-feature-service/README.md b/samples/generate-geodatabase-replica-from-feature-service/README.md
index 38a11ff8e..52b26d0e3 100644
--- a/samples/generate-geodatabase-replica-from-feature-service/README.md
+++ b/samples/generate-geodatabase-replica-from-feature-service/README.md
@@ -16,9 +16,9 @@ Zoom to any extent. Then click the generate button to generate a geodatabase of
1. Create a `GeodatabaseSyncTask` with the URL of the feature service and load it.
2. Create `GenerateGeodatabaseParameters` specifying the extent and whether to include attachments.
-3. Create a `GenerateGeodatabaseJob` with `geodatabaseSyncTask.generateGeodatabaseAsync(parameters, downloadPath)`. Start the job with `job.start()`.
+3. Create a `GenerateGeodatabaseJob` with `geodatabaseSyncTask.createGenerateGeodatabaseJob(parameters, downloadPath)`. Start the job with `job.start()`.
4. When the job is done, `job.result()` will return the geodatabase. Inside the geodatabase are feature tables which can be used to add feature layers to the map.
-5. Call `syncTask.unregisterGeodatabaseAsync(geodatabase)` after generation when you're not planning on syncing changes to the service.
+5. Call `geodatabaseSyncTask.unregisterGeodatabase(geodatabase)` after generation when you're not planning on syncing changes to the service.
## Relevant API
@@ -27,6 +27,10 @@ Zoom to any extent. Then click the generate button to generate a geodatabase of
* Geodatabase
* GeodatabaseSyncTask
+## Additional information
+
+This sample uses the GeoView-Compose Toolkit module to be able to implement a composable SceneView.
+
## Tags
-disconnected, local geodatabase, offline, replica, sync
+disconnected, geoview-compose, local geodatabase, offline, replica, sync, toolkit
diff --git a/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json b/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json
index 1d7dcb0be..786223a33 100644
--- a/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json
+++ b/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json
@@ -8,17 +8,21 @@
],
"keywords": [
"disconnected",
+ "geoview-compose",
"local geodatabase",
"offline",
"replica",
"sync",
+ "toolkit",
"GenerateGeodatabaseJob",
"GenerateGeodatabaseParameters",
"Geodatabase",
"GeodatabaseSyncTask"
],
"language": "kotlin",
- "redirect_from": "/android/latest/sample-code/generate-geodatabase.htm",
+ "redirect_from": [
+ "/android/latest/sample-code/generate-geodatabase.htm"
+ ],
"relevant_apis": [
"GenerateGeodatabaseJob",
"GenerateGeodatabaseParameters",
@@ -26,7 +30,9 @@
"GeodatabaseSyncTask"
],
"snippets": [
- "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt"
+ "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt"
],
"title": "Generate geodatabase replica from feature service"
-}
\ No newline at end of file
+}
diff --git a/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts b/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts
index e4871efcd..2e727c011 100644
--- a/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts
+++ b/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.arcgismaps.android.library)
+ alias(libs.plugins.arcgismaps.android.library.compose)
alias(libs.plugins.arcgismaps.kotlin.sample)
alias(libs.plugins.gradle.secrets)
}
@@ -11,9 +12,7 @@ secrets {
android {
namespace = "com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice"
- // For view based samples
buildFeatures {
- dataBinding = true
buildConfig = true
}
}
diff --git a/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png b/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png
index 90ef8e642..ca3fa557b 100644
Binary files a/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png and b/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png differ
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt
index c0d4a7b40..440e65f65 100644
--- a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt
+++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt
@@ -18,316 +18,38 @@
package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice
import android.os.Bundle
-import android.util.Log
-import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
-import androidx.databinding.DataBindingUtil
-import androidx.lifecycle.lifecycleScope
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
-import com.arcgismaps.Color
-import com.arcgismaps.data.Geodatabase
-import com.arcgismaps.data.ServiceFeatureTable
-import com.arcgismaps.geometry.Envelope
-import com.arcgismaps.geometry.SpatialReference
-import com.arcgismaps.mapping.ArcGISMap
-import com.arcgismaps.mapping.BasemapStyle
-import com.arcgismaps.mapping.layers.FeatureLayer
-import com.arcgismaps.mapping.symbology.SimpleLineSymbol
-import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
-import com.arcgismaps.mapping.view.Graphic
-import com.arcgismaps.mapping.view.GraphicsOverlay
-import com.arcgismaps.mapping.view.ScreenCoordinate
-import com.arcgismaps.tasks.geodatabase.GenerateGeodatabaseJob
-import com.arcgismaps.tasks.geodatabase.GeodatabaseSyncTask
-import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.databinding.GenerateGeodatabaseReplicaFromFeatureServiceActivityMainBinding
-import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.databinding.GenerateGeodatabaseDialogLayoutBinding
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
-import kotlinx.coroutines.launch
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.screens.GenerateGeodatabaseReplicaFromFeatureServiceScreen
+import com.esri.arcgismaps.sample.sampleslib.BuildConfig
-class MainActivity : AppCompatActivity() {
-
- // set up data binding for the activity
- private val activityMainBinding: GenerateGeodatabaseReplicaFromFeatureServiceActivityMainBinding by lazy {
- DataBindingUtil.setContentView(this, R.layout.generate_geodatabase_replica_from_feature_service_activity_main)
- }
-
- // setup data binding for the mapview
- private val mapView by lazy {
- activityMainBinding.mapView
- }
-
- // starts the geodatabase replica process
- private val generateButton by lazy {
- activityMainBinding.generateButton
- }
-
- private val resetButton by lazy {
- activityMainBinding.resetButton
- }
-
- // shows the geodatabase loading progress
- private val progressDialog by lazy {
- GenerateGeodatabaseDialogLayoutBinding.inflate(layoutInflater)
- }
-
- // local file path to the geodatabase
- private val geodatabaseFilePath by lazy {
- getExternalFilesDir(null)?.path + getString(R.string.portland_trees_geodatabase_file)
- }
-
- // keep track of the geodatabase replica generated by the feature service
- private var geodatabase: Geodatabase? = null
-
- private val downloadArea: Graphic = Graphic()
-
- // create a Trees FeatureLayer using the first layer of the ServiceFeatureTable
- private val featureLayer: FeatureLayer by lazy {
- FeatureLayer.createWithFeatureTable(
- featureTable = ServiceFeatureTable(
- uri = application.getString(R.string.feature_server_url) + "/0"
- )
- )
- }
-
- // creates a graphic overlay
- private val graphicsOverlay: GraphicsOverlay = GraphicsOverlay()
+class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
// authentication with an API key or named user is
// required to access basemaps and other location services
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
- lifecycle.addObserver(mapView)
-
- // create and add a map with a Topographic basemap style
- val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
- operationalLayers.add(featureLayer)
- }
- // set the max map extents to that of the feature service
- // representing portland area
- map.maxExtent = Envelope(
- -13687689.2185849,
- 5687273.88331375,
- -13622795.3756647,
- 5727520.22085841,
- spatialReference = SpatialReference.webMercator()
- )
- // configure mapview assignments
- mapView.apply {
- this.map = map
- // add the graphics overlay to display the boundary
- graphicsOverlays.add(graphicsOverlay)
- }
-
- // create a geodatabase sync task with the feature service url
- // This feature service shows a web map of portland street trees,
- // their attributes, as well as related inspection information
- val geodatabaseSyncTask = GeodatabaseSyncTask(getString(R.string.feature_server_url))
-
- // set the button's onClickListener
- generateButton.setOnClickListener {
- // start the geodatabase generation process
- generateGeodatabase(geodatabaseSyncTask, map, downloadArea.geometry?.extent)
- }
-
- resetButton.setOnClickListener {
- // clear any layers already on the map
- map.operationalLayers.clear()
- // clear all symbols drawn
- graphicsOverlay.graphics.clear()
- // add the download boundary
- graphicsOverlay.graphics.add(downloadArea)
- // add back the feature layer
- map.operationalLayers.add(featureLayer)
- // close the current geodatabase, if a replica was already generated
- geodatabase?.close()
- // show generate button
- generateButton.isEnabled = true
- resetButton.isEnabled = false
- }
-
- lifecycleScope.launch {
- // show the error and return if map load failed
- map.load().onFailure {
- showError("Unable to load map")
- return@launch
- }
- geodatabaseSyncTask.load().onFailure {
- // if the metadata load fails, show the error and return
- showError("Failed to fetch geodatabase metadata")
- return@launch
+ setContent {
+ SampleAppTheme {
+ GenerateGeodatabaseReplicaFromFeatureServiceApp()
}
-
- // show download area once map is loaded
- updateDownloadArea()
-
- // enable the generate button since the task is now loaded
- generateButton.isEnabled = true
-
- // create a symbol to show a box around the extent we want to download
- downloadArea.symbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.red, 2F)
- // add the graphic to the graphics overlay when it is created
- graphicsOverlay.graphics.add(downloadArea)
- // update the download area on viewpoint change
- mapView.viewpointChanged.collect {
- updateDownloadArea()
- }
- }
- }
-
- /**
- * Displays a red border on the map to signify the [downloadArea]
- */
- private fun updateDownloadArea() {
- // define screen area to create replica
- val minScreenPoint = ScreenCoordinate(200.0, 200.0)
- val maxScreenPoint = ScreenCoordinate(
- mapView.measuredWidth - 200.0,
- mapView.measuredHeight - 200.0
- )
- // convert screen points to map points
- val minPoint = mapView.screenToLocation(minScreenPoint)
- val maxPoint = mapView.screenToLocation(maxScreenPoint)
- // use the points to define and return an envelope
- if (minPoint != null && maxPoint != null) {
- val envelope = Envelope(minPoint, maxPoint)
- downloadArea.geometry = envelope
- }
- }
-
- /**
- * Starts a [geodatabaseSyncTask] with the given [map] and [extents],
- * runs a GenerateGeodatabaseJob and saves the geodatabase file into local storage
- */
- private fun generateGeodatabase(
- geodatabaseSyncTask: GeodatabaseSyncTask,
- map: ArcGISMap,
- extents: Envelope?
- ) {
- if (extents == null) {
- return showError("Download area extent is null")
- }
-
- lifecycleScope.launch {
- // create generate geodatabase parameters for the selected extents
- val defaultParameters =
- geodatabaseSyncTask.createDefaultGenerateGeodatabaseParameters(extents).getOrElse {
- // show the error and return if the task fails
- showError("Error creating geodatabase parameters")
- return@launch
- }.apply {
- // set the parameters to only create a replica of the Trees (0) layer
- layerOptions.removeIf { layerOptions ->
- layerOptions.layerId != 0L
- }
- }
-
- // set return attachments option to false
- // indicates if any attachments are added to the geodatabase from the feature service
- defaultParameters.returnAttachments = false
- // create a generate geodatabase job
- geodatabaseSyncTask.createGenerateGeodatabaseJob(defaultParameters, geodatabaseFilePath)
- .run {
- // create a dialog to show the jobs progress
- val materialDialogBuilder = createProgressDialog(this)
-
- // show the dialog and obtain a reference to it
- val jobProgressDialog = materialDialogBuilder.show()
-
- // launch a progress collector to display progress
- launch {
- progress.collect { value ->
- // update the progress bar and progress text
- progressDialog.progressBar.progress = value
- progressDialog.progressTextView.text = value.toString()
- }
- }
- // start the generateGeodatabase job
- start()
- // if the job completed successfully, get the geodatabase from the result
- val geodatabase = result().getOrElse {
- // show an error and return if job failed
- showError("Error fetching geodatabase: ${it.message}")
- // dismiss the dialog
- jobProgressDialog.dismiss()
- return@launch
- }
-
- // load and display the geodatabase
- loadGeodatabase(geodatabase, map)
- // dismiss the dialog view
- jobProgressDialog.dismiss()
- // unregister since we are not syncing
- geodatabaseSyncTask.unregisterGeodatabase(geodatabase)
- // show reset button as the task is now complete
- generateButton.isEnabled = false
- resetButton.isEnabled = true
- }
- }
- }
-
- /**
- * Loads the [replicaGeodatabase] and renders the feature layers on to the [map]
- */
- private suspend fun loadGeodatabase(replicaGeodatabase: Geodatabase, map: ArcGISMap) {
- // clear any layers already on the map
- map.operationalLayers.clear()
- // clear all symbols drawn
- graphicsOverlay.graphics.clear()
-
- // load the geodatabase
- replicaGeodatabase.load().onFailure {
- // if the load failed, show the error and return
- showError("Error loading geodatabase")
- return
- }
-
- // add all of the geodatabase feature tables to the map as feature layers
- map.operationalLayers += replicaGeodatabase.featureTables.map { featureTable ->
- FeatureLayer.createWithFeatureTable(featureTable)
}
-
- // keep track of the geodatabase to close it before generating a new replica
- geodatabase = replicaGeodatabase
}
- /**
- * Creates a new alert dialog using the progressDialog and provides
- * GenerateGeodatabaseJob cancellation on dialog cancellation
- *
- * @param generateGeodatabaseJob the job to cancel
- *
- * @return returns an alert dialog
- */
- private fun createProgressDialog(generateGeodatabaseJob: GenerateGeodatabaseJob): MaterialAlertDialogBuilder {
- // build and return a new alert dialog
- return MaterialAlertDialogBuilder(this).apply {
- // setting it title
- setTitle(getString(R.string.dialog_title))
- // allow it to be cancellable
- setCancelable(false)
- // sets negative button configuration
- setNegativeButton("Cancel") { _, _ ->
- // cancels the generateGeodatabaseJob
- lifecycleScope.launch {
- generateGeodatabaseJob.cancel()
- }
- }
- // removes parent of the progressDialog layout, if previously assigned
- progressDialog.root.parent?.let { parent ->
- (parent as ViewGroup).removeAllViews()
- }
- // set the progressDialog Layout to this alert dialog
- setView(progressDialog.root)
+ @Composable
+ private fun GenerateGeodatabaseReplicaFromFeatureServiceApp() {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ GenerateGeodatabaseReplicaFromFeatureServiceScreen(
+ sampleName = getString(R.string.generate_geodatabase_replica_from_feature_service_app_name)
+ )
}
}
-
- private fun showError(message: String) {
- Log.e(localClassName, message)
- Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show()
- }
}
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt
new file mode 100644
index 000000000..2f9dcf58b
--- /dev/null
+++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt
@@ -0,0 +1,320 @@
+/* Copyright 2024 Esri
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components
+
+import android.app.Application
+import androidx.compose.ui.unit.IntSize
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.arcgismaps.data.Geodatabase
+import com.arcgismaps.data.ServiceFeatureTable
+import com.arcgismaps.geometry.Envelope
+import com.arcgismaps.geometry.SpatialReference
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.layers.FeatureLayer
+import com.arcgismaps.mapping.symbology.SimpleLineSymbol
+import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
+import com.arcgismaps.mapping.view.Graphic
+import com.arcgismaps.mapping.view.GraphicsOverlay
+import com.arcgismaps.mapping.view.ScreenCoordinate
+import com.arcgismaps.tasks.geodatabase.GenerateGeodatabaseJob
+import com.arcgismaps.tasks.geodatabase.GeodatabaseSyncTask
+import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.io.File
+
+private const val FEATURE_SERVICE_URL =
+ "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Mobile_Data_Collection_WFL1/FeatureServer"
+
+class GenerateGeodatabaseReplicaFromFeatureServiceViewModel(
+ private val application: Application
+) : AndroidViewModel(application) {
+ // graphics overlay to display the download area
+ val graphicsOverlay = GraphicsOverlay()
+
+ // symbol used to show a box around the extent we want to download
+ private val downloadArea: Graphic = Graphic(
+ symbol = SimpleLineSymbol(
+ style = SimpleLineSymbolStyle.Solid,
+ color = com.arcgismaps.Color.red,
+ width = 2F
+ )
+ )
+
+ // a Trees FeatureLayer, using the first layer of the ServiceFeatureTable
+ private val featureLayer: FeatureLayer by lazy {
+ FeatureLayer.createWithFeatureTable(
+ featureTable = ServiceFeatureTable(
+ uri = "$FEATURE_SERVICE_URL/0"
+ )
+ )
+ }
+
+ // create a MapViewProxy, used to convert screen points to map points
+ val mapViewProxy = MapViewProxy()
+
+ // the dimensions of the MapView
+ private var mapViewSize = IntSize(0, 0)
+
+ // create a map with a Topographic basemap style
+ val arcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
+ // set the max extent to that of the feature service representing an area of Portland
+ maxExtent = Envelope(
+ -13687689.2185849,
+ 5687273.88331375,
+ -13622795.3756647,
+ 5727520.22085841,
+ spatialReference = SpatialReference.webMercator()
+ )
+
+ // add the FeatureLayer to the map
+ operationalLayers.add(featureLayer)
+ }
+
+ // a message dialog view model for handling error messages
+ val messageDialogVM = MessageDialogViewModel()
+
+ // state flow to expose the current UI state
+ private val _uiStateFlow = MutableStateFlow(UiState(appStatus = AppStatus.STARTING))
+ val uiStateFlow = _uiStateFlow.asStateFlow()
+
+ // create a GeodatabaseSyncTask with the URL of the feature service
+ private var geodatabaseSyncTask = GeodatabaseSyncTask(FEATURE_SERVICE_URL)
+
+ // job used to generate the geodatabase replica
+ private var generateGeodatabaseJob: GenerateGeodatabaseJob? = null
+
+ // the geodatabase replica
+ private var geodatabase: Geodatabase? = null
+
+ init {
+ // add the download graphic to the graphics overlay
+ graphicsOverlay.graphics.add(downloadArea)
+
+ viewModelScope.launch {
+ // load the map
+ arcGISMap.load().onSuccess {
+ // load the GeodatabaseSyncTask
+ geodatabaseSyncTask.load().onSuccess {
+ _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE)
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ title = "Failed to load GeodatabaseSyncTask",
+ description = error.message.toString()
+ )
+ }
+ }.onFailure { error ->
+ messageDialogVM.showMessageDialog(
+ title = "Failed to load map",
+ description = error.message.toString()
+ )
+ }
+ }
+ }
+
+ /**
+ * Function called when the map view size is known.
+ */
+ fun updateMapViewSize(size: IntSize) {
+ mapViewSize = size
+ }
+
+ /**
+ * Use map view's size to determine dimensions of the area to download.
+ */
+ fun calculateDownloadArea() {
+ // upper left corner of the area to take offline
+ val minScreenPoint = ScreenCoordinate(200.0, 200.0)
+
+ // lower right corner of the downloaded area
+ val maxScreenPoint = ScreenCoordinate(
+ x = mapViewSize.width - 200.0,
+ y = mapViewSize.height - 200.0
+ )
+
+ // convert screen points to map points
+ val minPoint = mapViewProxy.screenToLocationOrNull(minScreenPoint)
+ val maxPoint = mapViewProxy.screenToLocationOrNull(maxScreenPoint)
+
+ // set the download area's geometry using the calculated bounds
+ if (minPoint != null && maxPoint != null) {
+ val envelope = Envelope(minPoint, maxPoint)
+ downloadArea.geometry = envelope
+ }
+ }
+
+ /**
+ * Reset the map to its original state.
+ */
+ fun resetMap() {
+ // clear any layers and symbols already on the map
+ arcGISMap.operationalLayers.clear()
+ graphicsOverlay.graphics.clear()
+ // add the download area boundary
+ graphicsOverlay.graphics.add(downloadArea)
+ // add back the feature layer
+ arcGISMap.operationalLayers.add(featureLayer)
+ // close the current geodatabase, if a replica was already generated
+ geodatabase?.close()
+ _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE)
+ }
+
+ /**
+ * Generate the geodatabase replica.
+ */
+ fun generateGeodatabaseReplica() {
+ _uiStateFlow.value = UiState(appStatus = AppStatus.GENERATING, jobProgress = 0)
+
+ val offlineGeodatabasePath =
+ application.getExternalFilesDir(null)?.path + "/portland_trees_gdb.geodatabase"
+
+ // delete any offline geodatabase already in the cache
+ File(offlineGeodatabasePath).deleteRecursively()
+
+ // get the geometry of the download area
+ val geometry = downloadArea.geometry ?: return messageDialogVM.showMessageDialog(
+ title = "Could not get geometry of the downloadArea"
+ )
+
+ viewModelScope.launch(Dispatchers.Main) {
+ // create GenerateGeodatabaseParameters for the selected extent
+ val parameters =
+ geodatabaseSyncTask.createDefaultGenerateGeodatabaseParameters(geometry).getOrElse {
+ messageDialogVM.showMessageDialog(
+ title = "Error creating geodatabase parameters",
+ description = it.message.toString()
+ )
+ return@launch
+ }.apply {
+ // modify the parameters to only include the Trees (0) layer
+ layerOptions.removeIf { layerOptions ->
+ layerOptions.layerId != 0L
+ }
+ }
+
+ // we don't need attachments
+ parameters.returnAttachments = false
+
+ // create a GenerateGeodatabaseJob
+ val job = geodatabaseSyncTask.createGenerateGeodatabaseJob(
+ parameters = parameters,
+ pathToGeodatabaseFile = offlineGeodatabasePath
+ )
+
+ // stash the job so the cancel function can use it
+ generateGeodatabaseJob = job
+
+ // run the job
+ runGenerateGeodatabaseJob(job)
+ }
+ }
+
+ /**
+ * Run the [job], showing the progress dialog and displaying the resultant data on the map.
+ */
+ private suspend fun runGenerateGeodatabaseJob(job: GenerateGeodatabaseJob) {
+ // create a flow-collection for the job's progress
+ viewModelScope.launch(Dispatchers.Main) {
+ job.progress.collect { progress ->
+ _uiStateFlow.value = UiState(appStatus = AppStatus.GENERATING, jobProgress = progress)
+ }
+ }
+
+ // start the job and wait for Job result
+ job.start()
+ job.result().onSuccess { geodatabase ->
+ // display the data
+ loadGeodatabaseAndAddToMap(geodatabase)
+
+ // unregister the geodatabase since we will not sync changes to the service
+ geodatabaseSyncTask.unregisterGeodatabase(geodatabase).getOrElse {
+ messageDialogVM.showMessageDialog(
+ title = "Failed to unregister the geodatabase",
+ description = it.message.toString()
+ )
+ }
+ }.onFailure { error ->
+ _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE)
+ messageDialogVM.showMessageDialog(
+ title = "Error generating geodatabase",
+ description = error.message.toString()
+ )
+ }
+ }
+
+ /**
+ * Loads the [replicaGeodatabase] and renders the feature layers on to the map.
+ */
+ private suspend fun loadGeodatabaseAndAddToMap(replicaGeodatabase: Geodatabase) {
+ // clear any layers and symbols already on the map
+ arcGISMap.operationalLayers.clear()
+ graphicsOverlay.graphics.clear()
+
+ // load the geodatabase
+ replicaGeodatabase.load().onSuccess {
+ // add all the geodatabase feature tables to the map as feature layers
+ arcGISMap.operationalLayers += replicaGeodatabase.featureTables.map { featureTable ->
+ FeatureLayer.createWithFeatureTable(featureTable)
+ }
+ // keep track of the geodatabase to close it before generating a new replica
+ geodatabase = replicaGeodatabase
+ _uiStateFlow.value = UiState(appStatus = AppStatus.REPLICA_DISPLAYED)
+ }.onFailure { error ->
+ _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE)
+ messageDialogVM.showMessageDialog(
+ title = "Error loading geodatabase",
+ description = error.message.toString()
+ )
+ }
+ }
+
+ /**
+ * Cancel the current [generateGeodatabaseJob].
+ */
+ fun cancelOfflineGeodatabaseJob() {
+ viewModelScope.launch(Dispatchers.IO) {
+ generateGeodatabaseJob?.cancel()
+ }
+ _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE)
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ // close the current geodatabase, if any, to release internal resources and file locks
+ geodatabase?.close()
+ }
+}
+
+/**
+ * Data class representing the UI state.
+ */
+data class UiState(
+ val appStatus: AppStatus,
+ val jobProgress: Int = 0
+)
+
+enum class AppStatus {
+ STARTING,
+ READY_TO_GENERATE,
+ GENERATING,
+ REPLICA_DISPLAYED
+}
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt
new file mode 100644
index 000000000..9ffb89cc7
--- /dev/null
+++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt
@@ -0,0 +1,134 @@
+/* Copyright 2024 Esri
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat.getString
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.arcgismaps.LoadStatus
+import com.arcgismaps.toolkit.geoviewcompose.MapView
+import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components.GenerateGeodatabaseReplicaFromFeatureServiceViewModel
+import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.R
+import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components.AppStatus
+import com.esri.arcgismaps.sample.sampleslib.components.JobLoadingDialog
+import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+
+/**
+ * Main screen layout for the sample app.
+ */
+@Composable
+fun GenerateGeodatabaseReplicaFromFeatureServiceScreen(sampleName: String) {
+ val application = LocalContext.current.applicationContext
+ val mapViewModel: GenerateGeodatabaseReplicaFromFeatureServiceViewModel = viewModel()
+ val uiState by mapViewModel.uiStateFlow.collectAsStateWithLifecycle()
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) },
+ content = {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ ) {
+ MapView(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ // retrieve the size of the Composable MapView
+ .onSizeChanged { size ->
+ mapViewModel.updateMapViewSize(size)
+ },
+ arcGISMap = mapViewModel.arcGISMap,
+ graphicsOverlays = listOf(mapViewModel.graphicsOverlay),
+ mapViewProxy = mapViewModel.mapViewProxy,
+ onLayerViewStateChanged = {
+ // on launch, calculate the download area
+ if (mapViewModel.arcGISMap.loadStatus.value == LoadStatus.Loaded) {
+ mapViewModel.calculateDownloadArea()
+ }
+ },
+ onViewpointChangedForCenterAndScale = {
+ // recalculate the download area when viewpoint changes
+ if (mapViewModel.arcGISMap.loadStatus.value == LoadStatus.Loaded) {
+ mapViewModel.calculateDownloadArea()
+ }
+ },
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Button(
+ onClick = {
+ mapViewModel.resetMap()
+ },
+ enabled = uiState.appStatus == AppStatus.REPLICA_DISPLAYED
+ ) {
+ Text(text = getString(application, R.string.reset_map))
+ }
+
+ Button(
+ onClick = {
+ mapViewModel.generateGeodatabaseReplica()
+ },
+ enabled = uiState.appStatus == AppStatus.READY_TO_GENERATE
+ ) {
+ Text(text = getString(application, R.string.generate_button_text))
+ }
+ }
+
+ // display progress dialog while generating a geodatabase replica
+ if (uiState.appStatus == AppStatus.GENERATING) {
+ JobLoadingDialog(
+ title = getString(application, R.string.dialog_title),
+ progress = uiState.jobProgress,
+ cancelJobRequest = { mapViewModel.cancelOfflineGeodatabaseJob() }
+ )
+ }
+
+ // display a dialog if the sample encounters an error
+ mapViewModel.messageDialogVM.apply {
+ if (dialogStatus) {
+ MessageDialog(
+ title = messageTitle,
+ description = messageDescription,
+ onDismissRequest = ::dismissDialog
+ )
+ }
+ }
+ }
+ }
+ )
+}
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml
deleted file mode 100644
index bb1aeb60e..000000000
--- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml
deleted file mode 100644
index fbd959358..000000000
--- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml
index 74dacf670..eb01db93a 100644
--- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml
+++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml
@@ -1,8 +1,6 @@
Generate geodatabase replica from feature service
- https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Mobile_Data_Collection_WFL1/FeatureServer
Generate
- Fetching result
- /portland_trees_gdb.geodatabase
+ Generating geodatabase replica...
Reset map