Research Package is a Flutter package for building research study apps on Android and iOS using Flutter.
Research Package is a Flutter implementation of the Apple ResearchKit available for iOS (just like ResearchStack is for Android). The overarching goal of ResearchPackage is to enable developers and researchers to design and build cross-platform (iOS and Android) research applications using the same codebase.
Research Package is designed from the ground up to meet the requirements of most scientific research, including capturing participant consent, extensible input tasks, and the security and privacy needs necessary for IRB approval. The main features of Research Package are:
- Obtaining informed consent from participants, including support for a signature.
- Creating surveys and questionnaires with a wide range of answer formats (e.g., Likert scale, date pickers, image pickers, etc.), such as the WHO5 survey.
- Supporting localizations of surveys and informed consent.
Research Package is part of the overall Copenhagen Research Platform (CARP) with also provides a Flutter package for mobile and wearable sensing called CARP Mobile Sensing. The Pulmonary Monitor app shows how mobile sensing can be combined with collection of survey data from users.
There is a set of tutorials, describing:
- the overall software architecture of Research Package
- how to create an informed consent flow
- how to define and run user surveys
- how to enable localization
The Research Package Flutter API is available (and maintained) as part of the package release at pub.dev.
Translations live in assets/lang/<languageCode>.json and are looked up with
RPLocalizations.of(context)?.translate('key'). A key which is not translated is returned as-is, so
it is safe to pass text which may be either a key or a literal.
Nested keys. Translations may be nested and are addressed with a dot-separated path. These two files are equivalent, so nested and dot-separated files can be mixed freely and existing flat files keep working untouched:
{ "pages": { "task_list": { "title": "Tasks", "description": "Your tasks" } } }
{ "pages.task_list.title": "Tasks", "pages.task_list.description": "Your tasks" }locale.translate('pages.task_list.title'); // 'Tasks'Interpolation. {{placeholder}} values are filled in from args. A placeholder with no
matching argument is left in place, so a forgotten argument is visible rather than silently blank. A
single brace is always literal.
{ "greeting": "Hello {{name}}, you have {{n}} messages" }locale.translate('greeting', args: {'name': 'Bo', 'n': 3});
// 'Hello Bo, you have 3 messages'Plurals. Give a key one variant per plural category using the _zero, _one, _two, _few,
_many and _other suffixes, and pass a count. The category is picked using the CLDR rules of the
locale, so a language which needs _few and _many gets them. count is also available to the
translation as {{count}} without passing it in args.
{
"tasks_zero": "All done",
"tasks_one": "{{count}} task left",
"tasks_other": "{{count}} tasks left"
}locale.translate('tasks', count: 0); // 'All done'
locale.translate('tasks', count: 1); // '1 task left'
locale.translate('tasks', count: 5); // '5 tasks left'Which categories apply depends on the language — English only ever uses _one and _other. The one
exception is _zero, which is used for a count of exactly 0 in any language when present. A
category which is not translated falls back to _other, and a key with no plural variants at all
falls back to the key itself.
A consent section can declare the OS permissions its text explains the need for. When the visual
consent step opts in with askPermission: true, tapping "NEXT" on that section triggers the native
permission dialog — so the participant is asked in context, while the explanation is on screen,
which is what both Apple and Google ask for.
RPConsentSection(
type: RPConsentSectionType.Location,
summary: 'We use your location to study how you move around.',
content: 'The longer explanation shown under "Learn more"...',
permissions: [RPPermissionType.location],
);
RPVisualConsentStep(
identifier: 'visualStep',
consentDocument: consentDocument,
askPermission: true, // off by default - nothing is requested without this
);The outcome of every request is collected in an RPPermissionResult added to the RPTaskResult
under the identifier of the visual consent step. A denied permission is recorded but never blocks the
participant.
Apple requires that a screen explaining an upcoming permission request carries a single button,
leading to the system alert, and offers no way of leaving without seeing that alert — see
Human Interface Guidelines: Privacy.
The consent UI enforces this, so a flow using askPermission passes review as it is:
- An informed consent flow — any
RPOrderedTaskcontaining anRPConsentReviewStep— has no close button in the top bar and no cancel button on the consent sections. It is left by pressing "DISAGREE" on the review step, which callsRPUITask.onCancelas a cancellation always has. - The visual consent step offers a "BACK" button for re-reading earlier sections. It is hidden on the first section, and on any section which is still going to open a permission alert.
- Once a section's permissions have been asked for, "BACK" reappears on it — the alert has been seen, whatever the participant answered, so the screen is an ordinary consent section again.
Health data is the one entry in RPPermissionType which is not a single OS permission. Apple
HealthKit and Android Health Connect authorise each data type on its own — there are over a hundred
of them — so a section which lists RPPermissionType.health must also say which types it needs,
in healthDataTypes. Without them there is nothing to request and the permission resolves to
RPPermissionStatus.unsupported.
RPConsentSection(
type: RPConsentSectionType.Health,
summary: 'We read your steps and sleep to see how your activity changes.',
permissions: [RPPermissionType.health],
healthDataTypes: [HealthDataType.STEPS, HealthDataType.SLEEP_ASLEEP],
);HealthDataType comes from the health package and is re-exported
by research_package, so it needs no separate import. Read access is requested for every type
listed; types the participant has already authorised are skipped.
On iOS the recorded status is optimistic. HealthKit deliberately does not disclose whether read
access was granted — an app cannot tell "not permitted" from "no data" — so granted there means
the authorisation sheet was shown without error, not that the participant agreed. Android Health
Connect reports the real outcome. RPPermissions.requestHealthData() is public if an app needs to
ask outside a consent flow.
Only needed by apps which use askPermission.
Android — declare each permission in android/app/src/main/AndroidManifest.xml. An undeclared
permission is reported as permanently denied without showing a dialog.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>iOS — add a usage description per permission to ios/Runner/Info.plist; iOS terminates the app
if one is missing. For example NSLocationWhenInUseUsageDescription,
NSMicrophoneUsageDescription and NSMotionUsageDescription (which also covers
RPPermissionType.activityRecognition, since iOS reads activity through CoreMotion).
If the app integrates plugins with CocoaPods, each permission additionally has to be enabled in
ios/Podfile — permission_handler compiles every permission out of the build unless its macro is
set:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_LOCATION_WHENINUSE=1', # use PERMISSION_LOCATION=1 for locationAlways
'PERMISSION_MICROPHONE=1',
'PERMISSION_SENSORS=1', # activityRecognition and sensors
]
end
end
endWith Swift Package Manager this step is not needed — the macros are derived from the
Info.plist keys above.
The health plugin is a dependency of research_package, so its platform requirements apply to
every app using this package, whether or not it asks for health data:
- Android —
minSdkVersion 26. Apps which ask for health data additionally need, inAndroidManifest.xml, a<uses-permission android:name="android.permission.health.READ_*"/>per data type, a<package android:name="com.google.android.apps.healthdata"/>entry under<queries>, anandroidx.health.ACTION_SHOW_PERMISSIONS_RATIONALEintent filter on the main activity, and theViewPermissionUsageActivityalias.MainActivitymust extendFlutterFragmentActivityrather thanFlutterActivity, or Health Connect cannot show its permission sheet on Android 14 and later. - iOS —
NSHealthShareUsageDescriptionandNSHealthUpdateUsageDescriptioninInfo.plist, plus the HealthKit capability on the Runner target, added under "Signing & Capabilities" in Xcode. Without the capability the authorisation sheet never appears.
example/ is set up this way and can be copied from — see its AndroidManifest.xml,
MainActivity.kt, build.gradle.kts and Info.plist.
There is an example app which demonstrates the different features of Research Package as implemented in a Flutter app.
Research Package is made by the Copenhagen Center for Health Technology (CACHET) and is an important component in the Copenhagen Research Platform (CARP), which is used in a number of applications and studies. The current project maintainers are Mads Vedel Saaby Christensen and Jakob E. Bardram.
We are more than happy to take contributions and feedback. Use the Issues page to file an issue or feature request. Besides general help for enhancement and quality assurance (bug fixing), we welcome input on new answer types.
This software is copyright (c) Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). This software is available 'as-is' under a MIT license.