
Collects, validates, and processes structured healthcare data via HL7 FHIR Questionnaires. Composable questionnaire renderer with pre-fill, validation, review page, and submit/cancel producing FHIR QuestionnaireResponses.
A Kotlin Multiplatform library for collecting, validating, and processing structured healthcare data using HL7 FHIR Questionnaires.
enableWhen and SDC expression extensions (enableWhenExpression,
calculatedExpression, variable, answerExpression)For the full conformance analysis, see the conformance doc.
The library renders and processes Questionnaire and QuestionnaireResponse resources from FHIR R4 (v4.0.1).
See FHIR Questionnaire specification conformance for the implementation status of every item type, item control, form behavior element, and standard extension.
The library implements a subset of the Structured Data Capture implementation guide STU4 (v4.0.0). Advanced rendering, form behavior and calculation, and template-based extraction are implemented. The SDC population module and the other extraction mechanisms are not.
See SDC conformance for feature-by-feature status, supported expression languages, and FHIRPath environment variables.
The library's support for different target platforms is listed in the following table:
| Target platform | Gradle target | Artifact suffix | Support |
|---|---|---|---|
| Kotlin/JVM | jvm |
-jvm |
✅ |
| Kotlin/Wasm | wasmJs |
-wasm-js |
✅ |
| Kotlin/Wasm | wasmWasi |
-wasm-wasi |
⛔ |
| Kotlin/JS | js |
-js |
✅ |
| Android applications and libraries | android |
-android |
✅ |
The library also supports the following Kotlin/Native targets:
| Gradle target | Artifact suffix | Tier | Support |
|---|---|---|---|
| iosSimulatorArm64 | -iossimulatorarm64 |
1 | ✅ |
| iosArm64 | -iosarm64 |
1 | ✅ |
The catalog module is a multiplatform demo application. To run the iOS variant see
catalog-iosApp/README.md.
To use the Kotlin FHIR Data Capture library in your project, you need to add the library dependency
to your project. To do that, first make sure to include the mavenCentral()1 repository in the
build.gradle.kts file in your project root.
// build.gradle.kts
repositories {
// Other repositories such as gradlePluginPortal() and google()
mavenCentral()
}
Next, follow the instructions for your specific project type.
For Kotlin Multiplatform projects, add the dependency to the shared commonMain source set within
the kotlin block of the module's build.gradle.kts file (e.g., composeApp/build.gradle.kts or
shared/build.gradle.kts). This makes the library available across all platforms in your project.
// e.g., composeApp/build.gradle.kts or shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.ohs.fhir:fhir-data-capture:2.0.0-alpha02")
}
}
}
For Android projects, add the dependency to the dependency block in the module's
build.gradle.kts file (e.g., app/build.gradle.kts).
// e.g., app/build.gradle.kts
dependencies {
implementation("dev.ohs.fhir:fhir-data-capture:2.0.0-alpha03")
}
Render a questionnaire using the Questionnaire composable.
val coroutineScope = rememberCoroutineScope()
Questionnaire(
questionnaireJson = myQuestionnaireJson,
questionnaireResponseJson = existingResponseJson, // optional pre-fill
config = QuestionnaireConfig(
showSubmitButton = true,
showCancelButton = true,
showReviewPage = false,
isReadOnly = false,
),
onSubmit = { getResponse ->
coroutineScope.launch {
// Validates the response first. On failure an error dialog
// is shown and the coroutine is cancelled.
val response = getResponse()
// handle QuestionnaireResponse
}
},
onCancel = {
navController.popBackStack()
},
)See QuestionnaireConfig
for all display options (review page, read-only mode, required and optional labels, long-scroll
navigation, custom submit button text, and the "submit anyway" escape hatch).
To make launch context resources such as
%patient available to the questionnaire's FHIRPath expressions, pass them as JSON via
questionnaireLaunchContextMap, keyed by the launch context name declared in the questionnaire.
Questionnaire(
questionnaireJson = myQuestionnaireJson,
questionnaireLaunchContextMap = mapOf("patient" to patientJson),
...
)Optional integration hooks are supplied through
DataCaptureConfig
via a CompositionLocal.
CompositionLocalProvider(
LocalDataCaptureConfig provides
DataCaptureConfig(
// Resolve external (non-contained) answerValueSet URIs to answer options.
valueSetResolverExternal = myValueSetResolver,
// Resolve application/x-fhir-query expressions (answerExpression, variable).
xFhirQueryResolver = myXFhirQueryResolver,
// Fetch media content referenced by URL (itemMedia).
urlResolver = myUrlResolver,
),
) {
Questionnaire(...)
}Without these hooks, external value sets resolve to no options and x-fhir-query expressions fail. See the conformance doc for which features depend on which resolver.
The Questionnaire composable validates answers as the user fills the form and on submit. To
validate a response outside the UI, use
QuestionnaireResponseValidator.
val results: Map<String, List<ValidationResult>> = // keyed by linkId
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = questionnaire,
questionnaireResponse = questionnaireResponse,
)See validation conformance for the supported constraints and their caveats.
If the questionnaire is authored for
SDC template-based extraction, extract a transaction
Bundle of FHIR resources from the completed response with
TemplateExtractionEngine.
if (TemplateExtractionEngine.canExtract(questionnaire)) {
val bundle = TemplateExtractionEngine.extract(questionnaire, questionnaireResponse)
// post the transaction bundle to your FHIR server
}Extraction is not invoked automatically by the Questionnaire composable. Call it with the
response returned from onSubmit. Definition, StructureMap, and observation based extraction are
not supported (see extraction conformance).
Tests are located in the following source sets:
commonTest: Shared tests (logical validation rules and Compose UI rendering/flows) that run
across all targets.jvmTest: JVM-specific tests verifying localized date, time, and datetime input
parsing/formatting using JVM Locales (java.util.Locale).androidDeviceTest: Android-specific instrumentation tests verifying interactions with native
Android date, time, and datetime picker dialogs (requires a connected device or emulator).The CI pipeline automatically runs checks on every push and pull request. The table below details which test source sets (listed above) are executed by each target's CI task:
| Platform | Gradle task | CI runner | Test source sets | Notes |
|---|---|---|---|---|
| JVM | :datacapture:jvmTest |
ubuntu-latest |
commonTest, jvmTest
|
Requires xvfb-run on Linux runners to host virtual framebuffer for Compose tests |
| Wasm JS (Browser) | :datacapture:wasmJsBrowserTest |
ubuntu-latest |
commonTest |
Runs in headless Chrome |
| JS (Browser) | :datacapture:jsBrowserTest |
ubuntu-latest |
commonTest |
Runs in headless Chrome |
| Android | :datacapture:testAndroidHostTest |
ubuntu-latest |
commonTest |
Runs host unit tests on JVM |
| iOS (Simulator) | :datacapture:iosSimulatorArm64Test |
macos-latest |
commonTest |
Runs in simulator environment |
| iOS Release Framework | :datacapture:linkReleaseFrameworkIosArm64 |
macos-latest |
N/A | Build-only regression check (no test source sets) guarding against the Kotlin/Native LTO OOM in #35 |
To run all CI-validated test suites locally:
./gradlew checkTo run a specific test suite locally, run the corresponding Gradle task:
./gradlew :datacapture:jvmTest
./gradlew :datacapture:wasmJsBrowserTest
./gradlew :datacapture:jsBrowserTest
./datacapture:testAndroidHostTest
./gradlew :datacapture:iosSimulatorArm64Test
./gradlew :datacapture:linkReleaseFrameworkIosArm64
The platform-specific Android UI tests (located under androidDeviceTest) are not run
automatically on CI. To run them locally:
./gradlew :datacapture:connectedAndroidDeviceTestTo publish a new release, first update mavenVersion in gradle.properties to the new version.
Then follow one of the methods below:
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew :datacapture:publishToMavenLocalPublishing to Maven Central requires two sets of credentials:
See the Kotlin Multiplatform Publishing Guide and the Maven Central Publishing Guide for more information on how to set up these credentials.
For manual publishing, store the credentials in the global ~/.gradle/gradle.properties in your
environment (not the project's gradle.properties) so they are never committed to the repository:
# Maven Central Credentials
mavenCentralUsername=YOUR_USERNAME_TOKEN
mavenCentralPassword=YOUR_PASSWORD_TOKEN
# GPG Signing (file-based)
signing.keyId=YOUR_KEY_ID
signing.password=YOUR_KEY_PASSWORD
signing.secretKeyRingFile=/path/to/secring.gpgThen run:
./gradlew :datacapture:publishToMavenCentralThe project includes a GitHub Actions workflow that publishes to Maven Central when a new GitHub release (or pre-release) is created.
The workflow requires the following GitHub organization or repository secrets (already set up):
| Secret | Description |
|---|---|
MAVEN_CENTRAL_USERNAME |
Same as mavenCentralUsername
|
MAVEN_CENTRAL_PASSWORD |
Same as mavenCentralPassword
|
GPG_KEY_CONTENTS |
Needs to be exported using the command gpg --armor --export-secret-keys YOUR_KEY_ID
|
SIGNING_PASSWORD |
Same as signing.password
|
Early versions of this library (up to 1.0.0-beta02) were published under the group ID
com.google.android.fhir and artifact ID data-capture on
Google Maven. ↩
A Kotlin Multiplatform library for collecting, validating, and processing structured healthcare data using HL7 FHIR Questionnaires.
enableWhen and SDC expression extensions (enableWhenExpression,
calculatedExpression, variable, answerExpression)For the full conformance analysis, see the conformance doc.
The library renders and processes Questionnaire and QuestionnaireResponse resources from FHIR R4 (v4.0.1).
See FHIR Questionnaire specification conformance for the implementation status of every item type, item control, form behavior element, and standard extension.
The library implements a subset of the Structured Data Capture implementation guide STU4 (v4.0.0). Advanced rendering, form behavior and calculation, and template-based extraction are implemented. The SDC population module and the other extraction mechanisms are not.
See SDC conformance for feature-by-feature status, supported expression languages, and FHIRPath environment variables.
The library's support for different target platforms is listed in the following table:
| Target platform | Gradle target | Artifact suffix | Support |
|---|---|---|---|
| Kotlin/JVM | jvm |
-jvm |
✅ |
| Kotlin/Wasm | wasmJs |
-wasm-js |
✅ |
| Kotlin/Wasm | wasmWasi |
-wasm-wasi |
⛔ |
| Kotlin/JS | js |
-js |
✅ |
| Android applications and libraries | android |
-android |
✅ |
The library also supports the following Kotlin/Native targets:
| Gradle target | Artifact suffix | Tier | Support |
|---|---|---|---|
| iosSimulatorArm64 | -iossimulatorarm64 |
1 | ✅ |
| iosArm64 | -iosarm64 |
1 | ✅ |
The catalog module is a multiplatform demo application. To run the iOS variant see
catalog-iosApp/README.md.
To use the Kotlin FHIR Data Capture library in your project, you need to add the library dependency
to your project. To do that, first make sure to include the mavenCentral()1 repository in the
build.gradle.kts file in your project root.
// build.gradle.kts
repositories {
// Other repositories such as gradlePluginPortal() and google()
mavenCentral()
}
Next, follow the instructions for your specific project type.
For Kotlin Multiplatform projects, add the dependency to the shared commonMain source set within
the kotlin block of the module's build.gradle.kts file (e.g., composeApp/build.gradle.kts or
shared/build.gradle.kts). This makes the library available across all platforms in your project.
// e.g., composeApp/build.gradle.kts or shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.ohs.fhir:fhir-data-capture:2.0.0-alpha02")
}
}
}
For Android projects, add the dependency to the dependency block in the module's
build.gradle.kts file (e.g., app/build.gradle.kts).
// e.g., app/build.gradle.kts
dependencies {
implementation("dev.ohs.fhir:fhir-data-capture:2.0.0-alpha03")
}
Render a questionnaire using the Questionnaire composable.
val coroutineScope = rememberCoroutineScope()
Questionnaire(
questionnaireJson = myQuestionnaireJson,
questionnaireResponseJson = existingResponseJson, // optional pre-fill
config = QuestionnaireConfig(
showSubmitButton = true,
showCancelButton = true,
showReviewPage = false,
isReadOnly = false,
),
onSubmit = { getResponse ->
coroutineScope.launch {
// Validates the response first. On failure an error dialog
// is shown and the coroutine is cancelled.
val response = getResponse()
// handle QuestionnaireResponse
}
},
onCancel = {
navController.popBackStack()
},
)See QuestionnaireConfig
for all display options (review page, read-only mode, required and optional labels, long-scroll
navigation, custom submit button text, and the "submit anyway" escape hatch).
To make launch context resources such as
%patient available to the questionnaire's FHIRPath expressions, pass them as JSON via
questionnaireLaunchContextMap, keyed by the launch context name declared in the questionnaire.
Questionnaire(
questionnaireJson = myQuestionnaireJson,
questionnaireLaunchContextMap = mapOf("patient" to patientJson),
...
)Optional integration hooks are supplied through
DataCaptureConfig
via a CompositionLocal.
CompositionLocalProvider(
LocalDataCaptureConfig provides
DataCaptureConfig(
// Resolve external (non-contained) answerValueSet URIs to answer options.
valueSetResolverExternal = myValueSetResolver,
// Resolve application/x-fhir-query expressions (answerExpression, variable).
xFhirQueryResolver = myXFhirQueryResolver,
// Fetch media content referenced by URL (itemMedia).
urlResolver = myUrlResolver,
),
) {
Questionnaire(...)
}Without these hooks, external value sets resolve to no options and x-fhir-query expressions fail. See the conformance doc for which features depend on which resolver.
The Questionnaire composable validates answers as the user fills the form and on submit. To
validate a response outside the UI, use
QuestionnaireResponseValidator.
val results: Map<String, List<ValidationResult>> = // keyed by linkId
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = questionnaire,
questionnaireResponse = questionnaireResponse,
)See validation conformance for the supported constraints and their caveats.
If the questionnaire is authored for
SDC template-based extraction, extract a transaction
Bundle of FHIR resources from the completed response with
TemplateExtractionEngine.
if (TemplateExtractionEngine.canExtract(questionnaire)) {
val bundle = TemplateExtractionEngine.extract(questionnaire, questionnaireResponse)
// post the transaction bundle to your FHIR server
}Extraction is not invoked automatically by the Questionnaire composable. Call it with the
response returned from onSubmit. Definition, StructureMap, and observation based extraction are
not supported (see extraction conformance).
Tests are located in the following source sets:
commonTest: Shared tests (logical validation rules and Compose UI rendering/flows) that run
across all targets.jvmTest: JVM-specific tests verifying localized date, time, and datetime input
parsing/formatting using JVM Locales (java.util.Locale).androidDeviceTest: Android-specific instrumentation tests verifying interactions with native
Android date, time, and datetime picker dialogs (requires a connected device or emulator).The CI pipeline automatically runs checks on every push and pull request. The table below details which test source sets (listed above) are executed by each target's CI task:
| Platform | Gradle task | CI runner | Test source sets | Notes |
|---|---|---|---|---|
| JVM | :datacapture:jvmTest |
ubuntu-latest |
commonTest, jvmTest
|
Requires xvfb-run on Linux runners to host virtual framebuffer for Compose tests |
| Wasm JS (Browser) | :datacapture:wasmJsBrowserTest |
ubuntu-latest |
commonTest |
Runs in headless Chrome |
| JS (Browser) | :datacapture:jsBrowserTest |
ubuntu-latest |
commonTest |
Runs in headless Chrome |
| Android | :datacapture:testAndroidHostTest |
ubuntu-latest |
commonTest |
Runs host unit tests on JVM |
| iOS (Simulator) | :datacapture:iosSimulatorArm64Test |
macos-latest |
commonTest |
Runs in simulator environment |
| iOS Release Framework | :datacapture:linkReleaseFrameworkIosArm64 |
macos-latest |
N/A | Build-only regression check (no test source sets) guarding against the Kotlin/Native LTO OOM in #35 |
To run all CI-validated test suites locally:
./gradlew checkTo run a specific test suite locally, run the corresponding Gradle task:
./gradlew :datacapture:jvmTest
./gradlew :datacapture:wasmJsBrowserTest
./gradlew :datacapture:jsBrowserTest
./datacapture:testAndroidHostTest
./gradlew :datacapture:iosSimulatorArm64Test
./gradlew :datacapture:linkReleaseFrameworkIosArm64
The platform-specific Android UI tests (located under androidDeviceTest) are not run
automatically on CI. To run them locally:
./gradlew :datacapture:connectedAndroidDeviceTestTo publish a new release, first update mavenVersion in gradle.properties to the new version.
Then follow one of the methods below:
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew :datacapture:publishToMavenLocalPublishing to Maven Central requires two sets of credentials:
See the Kotlin Multiplatform Publishing Guide and the Maven Central Publishing Guide for more information on how to set up these credentials.
For manual publishing, store the credentials in the global ~/.gradle/gradle.properties in your
environment (not the project's gradle.properties) so they are never committed to the repository:
# Maven Central Credentials
mavenCentralUsername=YOUR_USERNAME_TOKEN
mavenCentralPassword=YOUR_PASSWORD_TOKEN
# GPG Signing (file-based)
signing.keyId=YOUR_KEY_ID
signing.password=YOUR_KEY_PASSWORD
signing.secretKeyRingFile=/path/to/secring.gpgThen run:
./gradlew :datacapture:publishToMavenCentralThe project includes a GitHub Actions workflow that publishes to Maven Central when a new GitHub release (or pre-release) is created.
The workflow requires the following GitHub organization or repository secrets (already set up):
| Secret | Description |
|---|---|
MAVEN_CENTRAL_USERNAME |
Same as mavenCentralUsername
|
MAVEN_CENTRAL_PASSWORD |
Same as mavenCentralPassword
|
GPG_KEY_CONTENTS |
Needs to be exported using the command gpg --armor --export-secret-keys YOUR_KEY_ID
|
SIGNING_PASSWORD |
Same as signing.password
|
Early versions of this library (up to 1.0.0-beta02) were published under the group ID
com.google.android.fhir and artifact ID data-capture on
Google Maven. ↩