Sync offline edits

Your users can edit offline using a services pattern and later sync their edits back to a feature service when connected. Syncing offline edits requires that you've created a geodatabase using a sync-enabled feature service from ArcGIS Online or ArcGIS Enterprise, as described in Create an offline map. After users have made edits and are ready to sync their local copy of the data with the service, use GeodatabaseSyncTask to sync with the feature service. Syncing can be performed even if no edits have been made locally, to pull changes from the feature service into the local copy of the data.

Note:

A Basic license is required for editing.

To synchronize edits, do the following:

  1. Create a GeodatabaseSyncTask, passing in the URL of the feature service to be edited.
  2. Set up a callback to report on the progress while the synchronization happens by calling GeodatabaseSyncTask.addJobChangedListener().
  3. Set up a callback to report when the process finishes or fails by calling GeodatabaseSyncTask.addJobDoneListener().
  4. Set up the parameters for the synchronization task, using either the method on the sync task, or the SyncGeodatabaseParameters constructor.
  5. Call the syncGeodatabaseAsync method on GeodatabaseSyncTask to get a Job. Pass in the synchronization parameters, and the local geodatabase that contains the edits.
  6. Call start on the job to begin the sync operation.

Note:
The sync operation overwrites previously synced edits to the same features on the service.

Internally, ArcGIS Runtime uses a geodatabase transaction to wrap the synchronization process. Since only one current transaction is permitted on a geodatabase, you will receive an error if you attempt to synchronize while another transaction is active (in an editing workflow, for example). Similarly, you may get an error if you try to start a transaction while synchronization is in progress. Errors that arise during a sync operation are returned in the callback when the job is done. For descriptions of errors that can arise when syncing offline edits, see Error handling with sync.

For services backed by non-versioned data, sync operations are performed per-layer, and are always bi-directional—that is, local edits are uploaded to the service, and then new edits are downloaded from the service. For services backed by versioned data, sync operations are per-geodatabase, and you can change the synchronization parameters to determine in which direction edits are synchronized—download only, upload only, or bi-directional. Use Geodatabase.getSyncModel() to find out if a geodatabase can be synchronized per-layer or per-geodatabase. Use SyncGeodatabaseParameters.setSyncDirection() to set the synchronization direction for a sync operation. When using bi-directional sync, note that the 'last in wins'—that is, uploaded edits will overwrite changes present on the server.

Register a geodatabase in a pre-planned workflow

In a services pattern workflow sometimes known as a pre-planned workflow, you generate the geodatabase once and load copies of it onto each user's device. If you've generated the geodatabase on the user's device with the generateGeodatabaseAsync method on the GeodatabaseSyncTask class, you don't need to register a geodatabase.

In the pre-planned workflow, you use the registerSyncEnabledGeodatabaseAsync() method to register each geodatabase copy (on each device) with the feature service you used to generate the original geodatabase. Registering in this way ensures each device receives the correct updates during sync operations.

Caution:

  • Registering a geodatabase with a feature service is not supported with a versioned feature service.
  • Once you call unregister on a geodatabase, you cannot re-register the same geodatabase.
  • If the original geodatabase is ever unregistered, no additional clients can use that copy to register.

For a list of benefits of this workflow, see create an offline layer.

Example code

The following example shows you how to sync your offline edits back to a feature service.

To generate and download a geodatabase, use GeodatabaseSyncTask and GenerateGeodatabaseParameters. If you already have a geodatabase downloaded, go to the following code section.

private void createGeodatabaseSyncTask() {

  // Create a GeodatabaseSyncTask and add a done loading listener
  geodatabaseSyncTask = new GeodatabaseSyncTask(FEATURE_SERVICE_URL);
  geodatabaseSyncTask.addDoneLoadingListener(new Runnable() {
    @Override public void run() {
      // Check if it failed to load. If its not in the failed state, inside a done loading listener the only other
      // valid state is loaded
      if (geodatabaseSyncTask.getLoadStatus() == LoadStatus.FAILED_TO_LOAD) {
        dealWithException(geodatabaseSyncTask.getLoadError());
      } else {
        // Now the sync task is loaded, use it to create GenerateGeodatabaseParameters, or sync changes
        generateGeodatabaseParameters();
      }
    }
  });

  // Load the sync task
  geodatabaseSyncTask.loadAsync();
}
private void generateGeodatabaseParameters() {

  // Create GenerateGeodatabaseParameters from the GeodatabaseSyncTask using current map extent
  Envelope generateExtent = mapView.getVisibleArea().getExtent();
  final ListenableFuture<GenerateGeodatabaseParameters> generateParamsFuture =
      geodatabaseSyncTask.createDefaultGenerateGeodatabaseParametersAsync(generateExtent);
  generateParamsFuture.addDoneListener(new Runnable() {
    @Override
    public void run() {
      try {
        generateParams = generateParamsFuture.get();

        // Change GenerateGeodatabaseParameters as required - e.g. removes all layers except first from the sync
        for (int i = generateParams.getLayerOptions().size() -1; i >= 1; i--) {
          generateParams.getLayerOptions().remove(i);
        }

        // Now the parameters are prepared, use them with the sync task to generate a geodatabase
        generateGeodatabase();
      } catch (InterruptedException | ExecutionException e) {
        dealWithException(e);
      }
    }
  });
}
private void generateGeodatabase() {
  // Download a copy of the geodatabase to the specified path, using prepared parameters
  generateJob = geodatabaseSyncTask.generateGeodatabaseAsync(generateParams, GEODATABASE_PATH);

  // Add listener to check and report on the current download status
  generateJob.addJobChangedListener(new Runnable() {
    @Override
    public void run() {

      // Deal with any errors found while generating the geodatabase
      if (generateJob.getError() != null) {
        dealWithException(generateJob.getError());
      } else {
        // While job is in progress, review job messages or update progress in logs or user interface...
        updateUiWithJobProgress(generateJob);
      }
    }
  });

  // Add listener to deal with the completed job
  generateJob.addJobDoneListener(new Runnable() {
    @Override
    public void run() {

      // Check the job state is complete, deal with any errors
      if ((generateJob.getStatus() != Job.Status.SUCCEEDED) || (generateJob.getError() != null)) {
        dealWithException(generateJob.getError());
      } else {
        // Get the completed Geodatabase from the job
        if (generateJob.getResult() instanceof Geodatabase) {
          Geodatabase syncResultGdb = (Geodatabase) generateJob.getResult();
          geodatabase = syncResultGdb;

          // Use the geodatabase, for example, load it and create FeatureLayers based on the tables it contains
          loadGeodatabase();
        }
      }
    }
  });

  // Start the Job to generate the geodatabase
  generateJob.start();
}

Once a local geodatabase has been edited, for example by using FeatureTable.addFeatureAsync, edits can be synced back to the service. Create a SyncGeodatabaseParameters object, and call the GeodatabaseSyncTask.syncGeodatabaseAsync method, passing in the parameters and the local geodatabase.

private void createSyncParameters() {

  // Create SyncGeodatabaseParameters based on default parameters from the sync tasks
  final ListenableFuture<SyncGeodatabaseParameters> syncParamsFuture = geodatabaseSyncTask
      .createDefaultSyncGeodatabaseParametersAsync(geodatabase);
  syncParamsFuture.addDoneListener(new Runnable() {
    @Override
    public void run() {
      try
      {
        // Retrieve the SyncGeodatabaseParameters from the future
        syncParams = syncParamsFuture.get();

        // Continue on to sync the database using the parameters
        syncGeodatabase();
      } catch (InterruptedException | ExecutionException e) {
        dealWithException(e);
      }
    }
  });
}
private void syncGeodatabase() {

  // Start the sync operation on the geodatabase
  syncJob = geodatabaseSyncTask.syncGeodatabaseAsync(syncParams, geodatabase);

  // Add listener to check and report on the current sync status
  syncJob.addJobChangedListener(new Runnable() {
    @Override
    public void run() {

      // Deal with any errors found while syncing the geodatabase
      if (syncJob.getError() != null) {
        dealWithException(syncJob.getError());
      } else {
        // While job is in progress, review job messages or update progress in logs or user interface...
        updateUiWithJobProgress(syncJob);
      }
    }
  });

  // Add listener to deal with the completed job
  syncJob.addJobDoneListener(new Runnable() {
    @Override
    public void run() {
      // Check the job state is complete, deal with any errors
      if ((syncJob.getStatus() != Job.Status.SUCCEEDED) || (syncJob.getError() != null)) {
        dealWithException(syncJob.getError());
      } else {
        // Get the SyncLayerResults returned from the sync
        List<SyncLayerResult> syncResults = (List<SyncLayerResult>) syncJob.getResult();
        if (syncResults != null) {
          // Check sync results, for example update the UI to inform the user
          dealWithSyncResults(syncResults);
        }
      }
    }
  });

  // Start the Job to sync the geodatabase
  syncJob.start();
}