A comprehensive, offline-first Flutter toolkit for building robust mobile and web applications that integrate seamlessly with DHIS2.
The toolkit provides high-level abstractions for User Authentication, Offline Storage (ObjectBox), Metadata & Tracker Data Synchronization, Dynamic Form Generation with Auto-Save, Program & Validation Rule Engines, Period Generation, Modular UI Components, and In-App Auto-Updates.
- Features
- Architecture Overview
- Installation
- Getting Started
- UI Components Guide
- Engines & Utilities
- Development & Contributing
- License
- 🔐 Authentication & Multi-Account Management: Online and offline login verification with secure credential caching via
flutter_secure_storage. - 💾 High-Performance Offline Database: Backed by ObjectBox for fast, indexed queries and multi-tenant database isolation.
- 🔄 Metadata & Tracker Synchronization: Declarative background/foreground sync streams for Programs, Program Stages, Org Units, Tracked Entities, Enrollments, Events, and Data Value Sets.
- 📝 Dynamic UI Forms: Built-in support for DHIS2 Tracker Registration and Single-Event forms with section collapsible layouts, field-level validation, and auto-save capabilities.
- 🧩 Comprehensive Form Inputs: Ready-to-use inputs for Text, Numbers, Dates, Date Ranges, Coordinates with interactive OpenStreetMap views, Org Unit tree picker, Age picker, Barcode/QR scanning, Multi-select, and Radio/Select options.
- ⚡ Rule Evaluation Engines:
- Program Rule Engine: Real-time evaluation of actions (
HIDEFIELD,ASSIGN,SHOWERROR,SHOWWARNING,MANDATORY,HIDEOPTION,HIDESECTION). - Validation Rule Engine: Evaluation of aggregate validation rules with missing-value strategies and compulsory/exclusive pair checks.
- Program Rule Engine: Real-time evaluation of actions (
- 📅 DHIS2 Period Engine: Full generation and parsing of DHIS2 Fixed periods (Daily, Weekly, Monthly, Yearly, Financial, etc.), Relative periods, and custom Date Ranges.
- 🚀 GitHub In-App Auto Updates: Check releases, stream APK downloads, and trigger native package installations directly from GitHub.
graph TD
A[DHIS2 Server API] <-->|D2ClientService| B[Sync Services]
B -->|Metadata & Data Sync| C[D2ObjectBox Store]
D[D2AuthService] -->|D2UserCredential| C
C --> E[Data & Metadata Repositories]
E --> F[Controllers & Rule Engines]
F --> G[UI Forms & Custom Inputs]
Add dhis2_flutter_toolkit to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
dhis2_flutter_toolkit:
# Use Git, Path, or pub.dev dependency
git:
url: https://github.com/hisptz/dhis2-flutter-toolkit.gitRun:
flutter pub getIf you plan to use camera features (Barcode Scanner), location (Coordinate inputs), or the in-app APK auto-updater on Android:
Add the required permissions inside <manifest>:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Required if using Auto-Update -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />android.useAndroidX=true
android.enableJetifier=trueFor complete auto-update setup including FileProvider and Kotlin MainActivity MethodChannel, refer to the Auto-Update Setup Guide.
D2AuthService manages DHIS2 user sessions, secure offline storage of credentials, and switching between users:
import 'package:dhis2_flutter_toolkit/dhis2_flutter_toolkit.dart';
final authService = D2AuthService();
// Log in online or verify offline if network is unavailable
try {
final D2UserCredential credential = await authService.login(
baseURL: 'play.dhis2.org/40.0.0',
username: 'admin',
password: 'district',
offlineFirst: false, // Set to true for offline-first verification
);
print('Logged in successfully: ${credential.username}');
} catch (e) {
print('Login failed: $e');
}
// Fetch currently authenticated user credentials
final D2UserCredential? currentUser = await authService.currentUser();
// Logout
// await authService.logoutUser(deleteData: false);Each user has an isolated ObjectBox database store identified by user credentials:
import 'package:dhis2_flutter_toolkit/dhis2_flutter_toolkit.dart';
// Initialize ObjectBox store for the logged-in user
final D2ObjectBox db = await D2ObjectBox.create(currentUser!);Tip for Unit Testing: Use
await D2ObjectBox.createTest()for in-memory databases during tests.
In DHIS2, metadata is organized by domain and resource type. The toolkit allows you to perform either a full all-in-one download or selective, granular downloads depending on your application's offline footprint and bandwidth requirements.
First, instantiate the HTTP client:
final D2ClientService client = D2ClientService(currentUser);D2MetadataDownloadService handles the entire metadata synchronization sequence for initial app bootstrap. It automatically downloads system info, organisation units, assigned programs, and datasets:
final metadataService = D2MetadataDownloadService(db, client);
metadataService.stream.listen((D2SyncStatus status) {
print('${status.label}: ${status.synced}/${status.total} (${status.status})');
});
// Downloads: User -> System Info -> Org Unit Levels & Units -> Tracked Entity Types -> Relationship Types -> Assigned Programs -> Assigned Data Sets
await metadataService.download();If your application only needs specific modules or wants to synchronize resources on demand, you can download each metadata type separately via its corresponding repository.
Downloading a Tracker or Event Program automatically performs an in-depth dependency cascade, resolving and saving all associated child metadata in strict dependency order:
final programRepo = D2ProgramRepository(db);
// Configure download for specific Program UIDs
programRepo.setupDownload(client, ['IpHINAT79UW', 'WSGAb5XwJ3Y']);
programRepo.downloadStream.listen((D2SyncStatus status) {
print('Syncing Programs: ${status.synced}/${status.total}');
});
await programRepo.download();Automatic Cascade Breakdown: When a Program is downloaded, the toolkit automatically queries
/api/programs/{id}/metadataand cascades down to:
- Option Sets & Options: Picklists for tracked entity attributes and data elements.
- Legend Sets & Legends: Color scales and range thresholds for numeric indicators.
- Option Groups & Option Group Sets: Categorization for options.
- Tracked Entity Attributes & Types: Attribute schemas for registration forms.
- Program Stages & Sections: Form sections and timeline event stages.
- Program Stage Data Elements & Data Elements: Individual form inputs.
- Program Rules, Variables & Rule Actions: Client-side form logic.
- Sharing Settings: User access permissions.
Aggregate data sets are downloaded with their complete category combinations and validation rule configurations:
final dataSetRepo = D2DataSetRepository(db);
// Configure download for specific Data Set UIDs
dataSetRepo.setupDownload(client, dataSetIds: ['BfMAe6Itzgt', 'aLpVgfXiz0f']);
dataSetRepo.downloadStream.listen((D2SyncStatus status) {
print('Syncing Data Sets: ${status.synced}/${status.total}');
});
await dataSetRepo.download();Automatic Cascade Breakdown:
- Data Elements & Data Set Elements: Assigned reporting indicators.
- Category Combos, Categories & Option Combos: Multi-dimensional disaggregations.
- Data Set Sections: Layout and grouping of entry forms.
- Validation Rules: Aggregate mathematical validation rules (
leftSide,rightSide, missing-value strategies, and sliding windows).
DHIS2 DataStore allows storing arbitrary JSON configurations (e.g. app settings, module definitions, workflows). D2DataStoreRepository synchronizes namespaces to the local ObjectBox database for fast offline access:
final dataStoreRepo = D2DataStoreRepository(db);
// 1. Download specific namespaces
dataStoreRepo.setupDownload(client: client);
dataStoreRepo.downloadStream.listen((D2SyncStatus status) {
print('Syncing DataStore: ${status.label} - ${status.subProcess?.label ?? ""}');
});
await dataStoreRepo.initializeDownload(namespaces: [
'app_configuration',
'user_preferences',
]);
// 2. Read cached DataStore entries offline
dataStoreRepo.setNamespace('app_configuration');
D2DataStore? config = dataStoreRepo.getByKey('general_settings');
print('Config value: ${config?.value}');
// 3. Upload device diagnostic logs to DataStore
await dataStoreRepo.uploadLogsToDataStore('client_error_logs', client);You can sync system-level foundation metadata independently:
// 1. Organisation Units
final orgUnitRepo = D2OrgUnitRepository(db);
await orgUnitRepo.setupDownload(client).download();
// 2. System Information
final sysInfoRepo = D2SystemInfoRepository(db);
await sysInfoRepo.setupDownload(client).download();
// 3. Tracked Entity Types & Relationship Types
await D2TrackedEntityTypeRepository(db).setupDownload(client).download();
await D2RelationshipTypeRepository(db).setupDownload(client).download();All sync services emit D2SyncStatus objects through standard Dart streams. This allows building responsive progress bars and multi-stage status indicators:
programRepo.downloadStream.listen((D2SyncStatus status) {
switch (status.status) {
case D2SyncStatusEnum.initialized:
print('Starting sync for ${status.label}...');
break;
case D2SyncStatusEnum.syncing:
double progress = status.total > 0 ? (status.synced / status.total) : 0.0;
print('${status.label}: ${(progress * 100).toStringAsFixed(1)}%');
if (status.subProcess != null) {
print(' Sub-task: ${status.subProcess!.label}');
}
break;
case D2SyncStatusEnum.complete:
print('${status.label} sync completed successfully!');
break;
case D2SyncStatusEnum.error:
print('Error during ${status.label} sync: ${status.error}');
break;
}
});Once metadata is available locally, synchronize tracked entity records, enrollments, and events:
// 1. Download Tracker Data for all assigned programs
final trackerDownload = D2TrackerDataDownloadService(db, client);
trackerDownload.downloadStream.listen((D2SyncStatus status) {
print('Downloading tracker data: ${status.label}');
});
await trackerDownload.download();
// 2. Upload Offline Created/Edited Records to DHIS2
final trackerUpload = D2TrackerDataUploadService(db, client);
trackerUpload.uploadStream.listen((D2SyncStatus status) {
print('Uploading records: ${status.label}');
});
await trackerUpload.upload();The toolkit exposes typed repositories for both Metadata and Tracker Data:
// Query Programs
final programRepo = D2ProgramRepository(db);
List<D2Program> programs = programRepo.find();
D2Program? childHealthProgram = programRepo.getByUid('IpHINAT79UW');
// Query Tracked Entities by Program
final teiRepo = D2TrackedEntityRepository(db);
if (childHealthProgram != null) {
teiRepo.setProgram(childHealthProgram);
List<D2TrackedEntity> entities = teiRepo.find();
print('Loaded ${entities.length} tracked entities');
}
// Query Events
final eventRepo = D2EventRepository(db);
List<D2Event> events = eventRepo.find();Render dynamic registration and enrollment forms generated directly from a program's tracked entity attributes, with built-in program rule execution and automatic draft saving:
import 'package:flutter/material.dart';
import 'package:dhis2_flutter_toolkit/dhis2_flutter_toolkit.dart';
class RegistrationScreen extends StatefulWidget {
final D2ObjectBox db;
final D2Program program;
final String orgUnitId;
const RegistrationScreen({
super.key,
required this.db,
required this.program,
required this.orgUnitId,
});
@override
State<RegistrationScreen> createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
late final D2TrackerEnrollmentFormController _controller;
@override
void initState() {
super.initState();
_controller = D2TrackerEnrollmentFormController(
db: widget.db,
program: widget.program,
orgUnit: widget.orgUnitId,
);
}
Future<void> _handleSave() async {
try {
final D2Enrollment enrollment = await _controller.save(autoUpload: false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Enrollment created: ${enrollment.uid}')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Validation error: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Register in ${widget.program.name}')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
D2TrackerRegistrationForm(
controller: _controller,
program: widget.program,
options: const D2TrackerFormOptions(
showTitle: true,
collapsableSections: true,
),
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _handleSave,
child: const Text('Submit Registration'),
),
],
),
),
);
}
}Render tracker stage forms or single-event without registration forms:
final eventController = D2TrackerEventFormController(
db: db,
programStage: programStage,
enrollment: activeEnrollment, // Optional for single events
orgUnit: orgUnitId,
);
// In Widget build:
D2TrackerEventForm(
controller: eventController,
programStage: programStage,
options: const D2TrackerFormOptions(
showTitle: true,
collapsableSections: false,
),
color: Colors.teal,
)If you are building custom forms outside D2ControlledForm, you can use D2InputFieldContainer to render individual inputs for any DHIS2 value type:
import 'package:flutter/material.dart';
import 'package:dhis2_flutter_toolkit/dhis2_flutter_toolkit.dart';
// 1. Text Input Config
final textInputConfig = D2TextInputFieldConfig(
id: 'first_name',
label: 'First Name',
type: D2InputFieldType.text,
mandatory: true,
clearable: true,
);
// 2. Select / Dropdown Option Input Config
final genderConfig = D2SelectInputFieldConfig(
id: 'gender',
label: 'Gender',
options: [
D2InputFieldOption(name: 'Male', code: 'M'),
D2InputFieldOption(name: 'Female', code: 'F'),
],
renderOptionsAsRadio: false, // Set to true for Radio buttons
);
// 3. Org Unit Picker Config
final orgUnitConfig = D2OrgUnitInputFieldConfig(
id: 'facility',
label: 'Assigned Health Facility',
service: D2LocalOrgUnitSelectorService(db),
);
// In your Widget tree:
D2InputFieldContainer(
input: textInputConfig,
value: currentVal,
color: Colors.blue,
onChange: (dynamic newValue) {
setState(() => currentVal = newValue);
},
)| Input Config Class | Input Types / Behaviors |
|---|---|
D2TextInputFieldConfig |
Text, Long Text, Email, URL, Barcode / QR Scanner |
D2NumberInputFieldConfig |
Integer, Positive Integer, Negative Integer, Decimal, Phone Number |
D2DateInputFieldConfig |
Date picker with constraints and calendar icon |
D2DateTimeInputFieldConfig |
Timestamp and Date & Time picker |
D2DateRangeInputFieldConfig |
Date range start/end selector |
D2BooleanInputFieldConfig |
Yes / No selection |
D2TrueOnlyInputFieldConfig |
Checkbox (Yes only) |
D2SelectInputFieldConfig |
Dropdown selector or Radio group |
D2MultiSelectInputFieldConfig |
Multiple option selection |
D2OrgUnitInputFieldConfig |
Interactive hierarchical Organisation Unit tree selector |
D2GeometryInputConfig |
Coordinate input with OpenStreetMap interactive point selection |
D2AgeInputFieldConfig |
Age in years/months or Date of Birth converter |
D2PeriodSelectorInputFieldConfig |
DHIS2 Period selector |
Embed an interactive DHIS2 Period Picker supporting Fixed Periods, Relative Periods, and Date Ranges:
D2PeriodSelector(
color: Colors.blue,
showFixed: true,
showRelative: true,
showRange: true,
allowMultipleSelection: false,
onUpdate: (D2PeriodSelection selection) {
print('Category: ${selection.category}');
print('Selected period IDs: ${selection.selected}');
print('Start date: ${selection.start}, End date: ${selection.end}');
},
)Display consistent bottom sheets and confirmation dialogs:
// 1. Show Action Sheet Modal
D2AppModalUtil.showActionSheetModal(
context,
title: 'Select Action',
titleColor: Colors.blue,
initialHeightRatio: 0.4,
actionSheetContainer: ListView(
children: [
ListTile(
leading: const Icon(Icons.sync),
title: const Text('Synchronize Now'),
onTap: () => Navigator.pop(context),
),
],
),
);
// 2. Show Confirmation Pop-Up
D2AppModalUtil.showPopUpConfirmation(
context,
title: 'Delete Record',
confirmationContent: const Text('Are you sure you want to delete this event?'),
cancelActionLabel: 'Cancel',
confirmActionLabel: 'Delete',
themColor: Colors.red,
onConfirm: () {
// Perform delete action
},
);CustomButton(
label: 'Download Data',
iconData: Icons.cloud_download,
buttonType: CustomButtonType.primaryButton, // primaryButton, outlineButton, textButton
buttonPrimaryColor: Colors.blue,
onTap: (BuildContext context) {
print('Button tapped');
},
)Evaluate DHIS2 program rules in client-side forms:
final engine = D2ProgramRuleEngine(
programRules: program.programRules,
programRuleVariables: program.programRuleVariables,
trackedEntity: currentTrackedEntity,
);
final D2ProgramRuleResult results = engine.evaluateProgramRule(
formDataObject: {'attributeUid': 'Female', 'ageUid': '12'},
);
// Inspect results
print('Hidden fields: ${results.hiddenFields.values}');
print('Assigned fields: ${results.assignedFields.values}');
print('Mandatory fields: ${results.mandatoryFields.values}');
print('Error messages: ${results.errorMessages.values}');Evaluate aggregate validation rules across data element values:
final validationEngine = D2ValidationRuleEngine(
validationRules: dataSet.validationRules,
);
final D2ValidationResult result = validationEngine.validate({
'dataElement1.optCombo1': '10',
'dataElement2.optCombo1': '15',
});
if (result.hasViolations) {
for (final violation in result.violations) {
print('Violation: ${violation.description}');
print('Left: ${violation.leftSideValue}, Right: ${violation.rightSideValue}');
}
}Generate DHIS2 periods on the fly:
// Get period type by ID (e.g. 'MONTHLY', 'DAILY', 'WEEKLY', 'YEARLY', 'FINANCIAL')
D2PeriodType monthlyType = D2PeriodUtility.getPeriodTypeById('MONTHLY');
// Generate available periods for a given year
List<D2Period> periods = monthlyType.getPeriods(year: 2026);
for (var period in periods) {
print('${period.id}: ${period.name} (${period.startDate} - ${period.endDate})');
}Generate standard 11-character alphanumeric DHIS2 UIDs compliant with DHIS2 specifications:
String newUid = D2UID.generate();
print(newUid); // e.g. 'qX9vN1lKm7a'Check for new releases on GitHub, download APKs with progress reporting, and trigger installation:
final updateService = D2UpdateService(
githubOwner: 'hisptz',
githubRepo: 'my-dhis2-app',
channelName: 'com.example.myapp/installer',
);
// Check if update is available
final updateInfo = await updateService.checkForUpdate();
if (updateInfo != null) {
showDialog(
context: context,
builder: (_) => D2UpdateDialog(
version: updateInfo['version'],
releaseNote: updateInfo['releaseNote'] ?? '',
progressStream: progressStream,
onUpdate: () async {
final apkPath = await updateService.downloadAPK(
updateInfo['apk_url'],
(received, total) {
// update progress
},
);
if (apkPath != null) {
await updateService.installAPK(apkPath);
}
},
),
);
}We welcome contributions! Follow these steps to set up your local development environment:
- Flutter SDK:
>=3.38.0(Dart SDK>=3.10.0-0 <4.0.0) - Android SDK (if testing Android features)
-
Clone the repository:
git clone https://github.com/hisptz/dhis2-flutter-toolkit.git cd dhis2-flutter-toolkit -
Install dependencies:
flutter pub get
-
Run ObjectBox code generation: Whenever you add or modify
@Entity()models, regenerate ObjectBox code:dart run build_runner build --delete-conflicting-outputs
Run automated unit and widget tests:
flutter testAnalyze code for style and linter warnings:
flutter analyze- Create a branch: Follow semantic naming (e.g.
feat/new-input-type,fix/rule-engine-priority). - Write tests: Add unit tests in
test/for any new utility, model, or engine feature. - Keep code clean: Follow effective Dart guidelines and ensure
flutter analyzepasses with zero errors. - Submit a Pull Request: Provide a clear description of your changes and reference any related issues.
This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.