Sync Data with FHIR Server
This guide explains how to synchronize data between your Android app and a FHIR server using the Android FHIR Engine. The FHIR Engine provides two ways to achieve this: one-time sync and periodic sync.
One-time sync allows you to trigger a synchronization job manually, on demand. This is useful for scenarios where you want to control when data is synchronized, such as when a user explicitly requests it.
Periodic sync allows you to schedule a job that automatically synchronizes data with the FHIR server at regular intervals. This is useful for keeping the local data in your app up-to-date with the server.
Initial setup
Before you can start syncing data, you need to perform some initial setup.
Server requirements
The FHIR server you are syncing with must support the optional capability to allow clients to PUT
resources to locations that don't yet exist (upsert).
Add dependencies and permissions
- Add the
work-runtime-ktx
dependency to your app-levelbuild.gradle
file, typicallyapp/build.gradle
:
dependencies {
// ...
implementation("androidx.work:work-runtime-ktx:2.8.1")
}
- Include the Internet permission in your
AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application ...>
...
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Configure FhirEngine
- During
FhirEngineProvider.init()
, provide aServerConfiguration
object with at least thebaseUrl
set to the URL of your FHIR server as part of theFhirEngineConfiguration
:
FhirEngineProvider.init(
FhirEngineConfiguration(
enableEncryptionIfSupported = true,
databaseErrorStrategy = DatabaseErrorStrategy.RECREATE_AT_OPEN,
serverConfiguration = ServerConfiguration("https://your-fhir-server-url"),
)
)
Advanced configuration
The ServerConfiguration
takes in other parameters as well.
Optionally, you can customize network connection parameters like timeouts and gzip compression using NetworkConfiguration
. By default, uploadWithGzip
is set to false
. If you are uploading to Google Cloud FHIR Store or a HAPI FHIR Server, we recommend setting it to true
as those two servers support gzip compression.
If your server requires authentication, implement the HttpAuthenticator
interface and provide it to ServerConfiguration
. This allows you to specify how the FHIR Engine should authenticate with the server. For example, let's say your server requires OAuth 2.0 Bearer Token authentication. To use it, implement the HttpAuthenticator
interface using the HttpAuthenticationMethod.Bearer function, which should return the bearer token string required by the server. It will be added to the header of all requests.
For debugging purposes, you can configure the httpLogger
parameter in ServerConfiguration
to log the communication between the FHIR Engine and the server.
Putting all this together, we can have a ServerConfiguration that looks like this:
ServerConfiguration(
"https://hapi.fhir.org/baseR4/",
httpLogger =
HttpLogger(
HttpLogger.Configuration(
if (BuildConfig.DEBUG) HttpLogger.Level.BODY else HttpLogger.Level.BASIC
)
) { Timber.tag("App-HttpLog").d(it) },
networkConfiguration = NetworkConfiguration(uploadWithGzip = true),
authenticator = { HttpAuthenticationMethod.Bearer("mySecureToken") }
)
Implement DownloadWorkManager
Create a subclass of DownloadWorkManager
to define how the engine should generate download requests and process responses from the server. You can refer to the TimestampBasedDownloadWorkManagerImpl
in the demo app for an example implementation.
Implement FhirSyncWorker
Finally, provide an implementation of FhirSyncWorker
. This class is responsible for defining how your app interacts with the FHIR Engine during synchronization. It has four functions to implement:
getDownloadWorkManager()
: This should return the implementation ofDownloadWorkManager
you created earlier.getConflictResolver()
: This controls how conflicts between the local and remote versions of resources are resolved. You can set it toAcceptLocalConflictResolver
if the local version should take precedence, orAcceptRemoteConflictResolver
if the remote version should.getFhirEngine()
: This should return your application'sFhirEngine
instance.getUploadStrategy()
: This defines how local changes are uploaded to the FHIR server. Currently, the only supported strategy isUploadStrategy.AllChangesSquashedBundlePut
, which squashes all local changes into a single bundle and uses thePUT
method to upload it.
Here's an example implementation:
class FhirPeriodicSyncWorker(appContext: Context, workerParams: WorkerParameters) :
FhirSyncWorker(appContext, workerParams) {
override fun getDownloadWorkManager(): DownloadWorkManager {
return DownloadWorkManagerImpl() // Replace with your implementation
}
override fun getConflictResolver() = AcceptLocalConflictResolver
override fun getFhirEngine() = FhirApplication.fhirEngine(applicationContext)
override fun getUploadStrategy(): UploadStrategy {
return UploadStrategy.AllChangesSquashedBundlePut
}
}
Remember to replace DownloadWorkManagerImpl
with your actual implementation.
See the DemoFhirSyncWorker
in the demo app for a more complete example.
Perform synchronization
Once you have completed the initial setup, you can start syncing data with the FHIR server.
One-Time Sync
Use the Sync.oneTimeSync API:
Sync.oneTimeSync<FhirPeriodicSyncWorker>(applicationContext)
This triggers a single sync job. You can collect the SyncJobStatus
from the returned Flow
to monitor the progress and result of the job.
CoroutineScope.launch {
Sync.oneTimeSync<DemoFhirSyncWorker>(applicationContext)
.shareIn(this, SharingStarted.Eagerly, 0)
.collect { /* Handle SyncJobStatus here */ }
}
Periodic Sync
Use the Sync.periodicSync API.
Sync.periodicSync<YourFhirSyncWorker>(
applicationContext,
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES)
)
)
This schedules a periodic sync job that runs according to the specified PeriodicSyncConfiguration
. You can customize the sync constraints and repeat interval as needed. Similar to one-time sync, you can collect the PeriodicSyncJobStatus
from the returned Flow
to monitor the job.
CoroutineScope.launch {
Sync.periodicSync<DemoFhirSyncWorker>(
application.applicationContext,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES)
)
)
.shareIn(this, SharingStarted.Eagerly, 10)
.collect { /**Handle SyncJobStatus Here*/ }
}
Note: Both sync methods utilize the WorkManager library under the hood. For more information on WorkManager, refer to the official documentation