Skip to main content

research_package 4.0.0 copy "research_package: ^4.0.0" to clipboard
research_package: ^4.0.0 copied to clipboard

A Flutter framework for obtaining informed consent, showing surveys and collecting results.

Research Package #

pub package style: effective dart github stars MIT License

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:

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.

Documentation #

There is a set of tutorials, describing:

The Research Package Flutter API is available (and maintained) as part of the package release at pub.dev.

Localization #

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.

OS permissions #

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.

Asking for permissions #

Research Package does not talk to the platform about permissions itself — the app does, by passing a requester to ResearchPackage.ensureInitialized. A dependency on a native plugin would be forced on every app using this package: the health plugin, for instance, links HealthKit, and App Store review rejects an app which links HealthKit without using it. Until a requester is set, every permission resolves to RPPermissionStatus.unsupported.

void main() {
  ResearchPackage.ensureInitialized(
    permissionRequester: (type, {healthDataTypes = const []}) async {
      switch (type) {
        case RPPermissionType.health:
          return requestHealthData(healthDataTypes);
        case RPPermissionType.camera:
          return statusOf(await Permission.camera.request());
        // ...
      }
    },
  );

  runApp(const MyApp());
}

The example app has a complete requester, built on permission_handler and health, which can be copied as is — see example/lib/permissions.dart. It also handles the cases which are not one-to-one: background location asks for foreground location first, Bluetooth on Android asks for the "Nearby devices" pair, battery optimization is unsupported on iOS, and activity recognition comes from the motion sensors on iOS.

An exception thrown by the requester is caught and recorded as unknown, so it never interrupts the consent flow.

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 RPOrderedTask containing an RPConsentReviewStep — 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 calls RPUITask.onCancel as 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 #

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.name, HealthDataType.SLEEP_ASLEEP.name],
);

Each entry of healthDataTypes is the name of a HealthDataType of the health plugin, and is passed as is to the requester. The example's requestHealthData() reads them with HealthDataType.values.byName, and also handles Google Health Connect — a separate app below Android 14 — being missing, which the plugin throws on.

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.

Platform setup #

Only needed by apps which use askPermission, and written for a requester built on permission_handler and health like the example's.

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
end

With Swift Package Manager this step is not needed — the macros are derived from the Info.plist keys above.

Health data

Only needed by apps which ask for health data, and so depend on the health plugin:

  • Android — minSdkVersion 26, and, in AndroidManifest.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>, an androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE intent filter on the main activity, and the ViewPermissionUsageActivity alias. MainActivity must extend FlutterFragmentActivity rather than FlutterActivity, or Health Connect cannot show its permission sheet on Android 14 and later.

  • iOS — an IPHONEOS_DEPLOYMENT_TARGET of 15.0 or higher, plus NSHealthShareUsageDescription and NSHealthUpdateUsageDescription in Info.plist and the HealthKit capability on the Runner target, added under "Signing & Capabilities" in Xcode. Without the capability the authorisation sheet never appears.

    Raising the deployment target is not enough on its own when the project uses Swift Package Manager. FlutterGeneratedPluginSwiftPackage is regenerated with Flutter's own minimum of iOS 13.0 whenever the plugin list is refreshed — which flutter pub get does — and is only raised to match the project's target by flutter build and flutter run. Those two therefore always work. Building straight from Xcode after a flutter pub get fails with "The package product 'health' requires minimum platform version 15.0 for the iOS platform, but this target supports 13.0"; flutter build ios --config-only repairs it. Build through Flutter rather than Xcode's own Run button to avoid it entirely.

example/ is set up this way and can be copied from — see its AndroidManifest.xml, MainActivity.kt, build.gradle.kts, Info.plist and Runner.entitlements.

Example Application #

There is an example app which demonstrates the different features of Research Package as implemented in a Flutter app.

Who is backing this project? #

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.

How can I contribute? #

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.

License #

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.

40
likes
140
points
1.54k
downloads

Documentation

API reference

Publisher

verified publishercarp.dk

Weekly Downloads

A Flutter framework for obtaining informed consent, showing surveys and collecting results.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

audioplayers, carp_serializable, carp_themes_package, flutter, intl, json_annotation, plugin_platform_interface, rxdart, signature, video_player

More

Packages that depend on research_package