milsat_modules_flutter v1.3.0
A comprehensive Flutter package for geospatial data collection, user management, dynamic form rendering, data export/sync, and hierarchical location services.
A production-ready Flutter package built with Clean Architecture principles, providing 6 independent yet integrated modules for building sophisticated data collection and geospatial applications.
π Key Features
Common Foundation Module
Core utilities, error handling, network layer, and shared entities.
- Result pattern for error handling
- Automatic token refresh
- Network utilities with Dio
- Configuration management
User Authentication & Users
Complete user management and authentication system.
- Email & OTP authentication
- Profile management
- Project compendium
- Address verification
Form Dynamic Forms
Powerful dynamic form builder with validation and conditional logic.
- 8+ form widget types
- Conditional field logic
- Multi-page forms
- Script evaluation
Data Data Management
Robust data management with local SQLite and cloud sync.
- CRUD operations
- Cloud export & sync
- Validation workflows
- Image synchronization
Location Hierarchical Locations
Nigerian location services with 6.8MB of CSV data.
- State β LGA β Ward β PU hierarchy
- INEC polling units
- NIPOST localities
- Custom location support
Map Mapbox Integration
Advanced mapping with geometry drawing and visualization.
- Point, Line, Polygon drawing
- Work area visualization
- Undo/redo support
- Dynamic icons
ποΈ Architecture Overview
Built with Feature-first Clean Architecture, each module follows a consistent pattern:
- Domain Layer: Entities, repository interfaces, service interfaces
- Data Layer: Models, data sources, repository implementations
- Presentation Layer: Widgets, UI components, state management
This package is ideal for developers building:
- Field data collection applications
- Survey and enumeration systems
- Geospatial data management platforms
- Location-based services in Nigeria
- Enterprise data collection workflows
Getting Started
Quick start guide to integrate milsat_modules_flutter into your Flutter application.
π¦ Installation
Add milsat_modules_flutter to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
milsat_modules_flutter:
git:
url: https://github.com/HaywhyD/milsat_modules_flutter
ref: main # or specific version tag
Then run:
flutter pub get
β‘ Quick Start
Initialize the package in your main.dart file:
import 'package:flutter/material.dart';
import 'package:milsat_modules_flutter/milsat_modules_flutter.dart';
void main() async {
// CRITICAL: Initialize bindings first
WidgetsFlutterBinding.ensureInitialized();
// Initialize all modules
await ModulesConfig.initialize(
baseUrl: "https://api.yourserver.com/",
ppqBaseUrl: "https://ppq.yourserver.com/",
isDebugMode: kDebugMode,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Data Collection App',
home: HomeScreen(),
);
}
}
- Always call
WidgetsFlutterBinding.ensureInitialized()before ModulesConfig - Use
awaitwhen calling ModulesConfig.initialize() - Initialize BEFORE
runApp() - Provide both
baseUrlandppqBaseUrl
π Accessing Services
Services are accessed via Config classes:
import 'package:milsat_modules_flutter/milsat_modules_flutter.dart';
class MyScreen extends StatefulWidget {
@override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State {
// Use late final for lazy initialization
late final UserService _userService = UserConfig.userService;
late final DataService _dataService = DataConfig.dataService;
Future _performLogin() async {
final result = await _userService.loginWithEmail(email, password);
result.fold(
onSuccess: (user) {
// Handle successful login
print('Welcome ${user.profile.firstName}!');
},
onFailure: (error) {
// Handle error
print('Login failed: ${error.message}');
},
);
}
}
π First Steps Checklist
1οΈβ£ Setup Configuration
Initialize ModulesConfig with your API endpoints
2οΈβ£ Implement Authentication
Use UserService for login/logout workflows
3οΈβ£ Import Survey Config
Use DataService to import form configurations
4οΈβ£ Build Forms
Use FormBuilder widget for data collection
5οΈβ£ Manage Data
CRUD operations with DataService
6οΈβ£ Export & Sync
Sync data to cloud with exportToCloud
Common Common Module
Foundation module providing shared utilities, error handling, network layer, and configuration management.
π Overview
The Common module serves as the foundation for all other modules, providing:
- Result Pattern: Type-safe error handling
- Network Layer: Dio-based HTTP client with automatic token refresh
- Error Types: Comprehensive error classification
- Core Entities: Shared data models across modules
- Configuration: Centralized package configuration
βοΈ Configuration
ModulesConfig.initialize({required String baseUrl, required String ppqBaseUrl, bool? isDebugMode, FormConfig? formConfig})
Initializes all modules with the provided configuration.
Parameters:
Main API endpoint URL
PPQ (Plant Protection & Quarantine) API endpoint
Enable debug logging (defaults to false)
Optional form widget customization
β Result Pattern
All service methods return a Result<T> type for type-safe error handling:
// Result has two states: Success or Failure
final result = await userService.loginWithEmail(email, password);
// Use fold() to handle both cases
result.fold(
onSuccess: (CurrentUserEntity user) {
// Success - use the data
print('Logged in as: ${user.profile.firstName}');
Navigator.push(context, MaterialPageRoute(builder: (_) => Dashboard()));
},
onFailure: (AppError error) {
// Failure - handle the error
print('Error: ${error.message}');
showErrorDialog(error.message);
},
);
// Alternative: Check result directly
if (result.wasSuccessful) {
final user = result.data!;
// Use user data
} else {
final error = result.errorOrNull;
// Handle error
}
π¨ Error Types
| Error Type | When It Occurs | Common Causes |
|---|---|---|
NetworkError |
HTTP/network failures | No internet, timeout, server down |
AuthError |
Authentication failures | Invalid credentials, token expired |
UnauthorizedError |
Session expired | Token expired, user logged out |
ValidationError |
Input validation failed | Invalid data format, missing fields |
ServerError |
Backend errors (5xx) | Server crashes, database errors |
UnknownError |
Unexpected errors | Uncaught exceptions |
π Network Layer
The network layer provides:
- Automatic Token Refresh: Refreshes tokens 60 seconds before expiry
- Request Logging: Debug mode logs all requests/responses
- Timeout Handling: 30-second default timeout
- Error Normalization: Converts HTTP errors to AppError types
The package automatically:
- Stores access and refresh tokens securely
- Calculates token expiry time
- Refreshes tokens before they expire (60s buffer)
- Retries failed requests after token refresh
- Logs out users if refresh fails
User User Module
Complete authentication and user management system with profile, client, and project compendium support.
π Authentication Methods
Login with Email
Future<Result<CurrentUserEntity>> loginWithEmail(String email, String password)
Authenticates a user with email and password.
Real-world Example from Test App:
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
late final UserService _userService = UserConfig.userService;
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _formKey = GlobalKey();
bool _isLoading = false;
String _errorMessage = '';
Future _login() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final result = await _userService.loginWithEmail(
_emailController.text.trim(),
_passwordController.text,
);
result.fold(
onSuccess: (currentUser) {
// Navigate to Main Dashboard
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainMenuDashboard()),
);
},
onFailure: (error) {
setState(() {
_errorMessage = error.message;
_isLoading = false;
});
},
);
} catch (e) {
setState(() {
_errorMessage = 'Login failed: $e';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) => value?.isEmpty ?? true ? 'Required' : null,
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) => value?.isEmpty ?? true ? 'Required' : null,
),
if (_errorMessage.isNotEmpty)
Text(_errorMessage, style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: _isLoading ? null : _login,
child: _isLoading
? CircularProgressIndicator()
: Text('Login'),
),
],
),
),
);
}
}
π€ Other Authentication Methods
loginWithOtp(String phoneNumber, String otp)- OTP-based logingenerateOtp(String phoneNumber)- Generate OTP for userlogout()- Clear session and log outisLoggedIn()- Check if user is currently logged invalidateOtp(String email, String otp)- Validate OTP codecreateAccount(CreateAccountModel model)- Register new accountresetPassword(ResetPasswordModel model)- Reset user password
π Profile Management
// Get current logged-in user
final currentUser = _userService.currentUser;
print('Welcome ${currentUser.profile.firstName}!');
// Access user properties
print('Email: ${currentUser.profile.email}');
print('Phone: ${currentUser.profile.phoneNumber}');
print('Client: ${currentUser.client.name}');
print('Projects: ${currentUser.client.projectCompendiums.length}');
// Refresh user profile from server
final result = await _userService.refreshUserProfile();
// Update local profile
await _userService.updateLocalUserProfile(updatedProfile);
// Upload profile photo
await _userService.uploadUserProfilePhoto(imageFile, token, userId);
π’ Client & Project Compendium
// Access client information
final client = currentUser.client;
print('Client ID: ${client.id}');
print('Client Name: ${client.name}');
// Access project compendiums
final projects = currentUser.client.projectCompendiums;
for (final project in projects) {
print('Project: ${project.name}');
print('Form ID: ${project.formId}');
print('Frame URL: ${project.frameUrl}'); // For map boundaries
}
// Get specific project
final myProject = projects.firstWhereOrNull(
(p) => p.formId.contains(surveyId),
);
userId- Unique user identifieraccessToken- Authentication tokenprofile- UserProfileEntity (name, email, phone, etc.)client- ClientEntity (organization info)
Form Form Module
Dynamic form rendering engine with validation, conditional logic, and 8+ customizable widget types.
π¨ FormBuilder Widget
The core widget for rendering dynamic forms from survey configurations.
FormBuilder({
required SurveyFormEntity surveyForm,
required Map uiFieldValues,
required Map uiFieldMap,
required Function(String, dynamic) onUIFieldValueChange,
required Function(BaseUIField) onUIFieldsChange,
required Function(FormNavigationState) onNavigationStateChanged,
required Function(FormNavigationActions) onNavigationActionsChanged,
GlobalKey? formKey,
FormBuilderInput? input,
DataService? dataService,
Function(bool)? onFormEnded,
bool applyPadding = true,
bool fillMaxSize = true,
String? frameUrl,
// Widget-specific configurations
TextFieldConfig? defaultTextFieldConfig,
DropdownConfig? defaultDropdownConfig,
DatePickerConfig? defaultDatePickerConfig,
TimePickerConfig? defaultTimePickerConfig,
PictureConfig? defaultPictureConfig,
SignatureConfig? defaultSignatureConfig,
})
π§ Complete Usage Example (from Test App)
class AddEntryScreen extends StatefulWidget {
@override
_AddEntryScreenState createState() => _AddEntryScreenState();
}
class _AddEntryScreenState extends State {
late final DataService _dataService = DataConfig.dataService;
// Required state for FormBuilder
final Map _uiFieldValues = {};
final Map _uiFieldMap = {};
final GlobalKey _formKey = GlobalKey();
SurveyFormEntity? _currentSurvey;
FormNavigationState? _navigationState;
FormNavigationActions? _navigationActions;
@override
void initState() {
super.initState();
_loadSurvey();
}
Future _loadSurvey() async {
_currentSurvey = _dataService.currentSurvey?.forms.first;
_initializeAllFieldsInMap();
setState(() {});
}
void _initializeAllFieldsInMap() {
// Initialize all fields to null
for (final page in _currentSurvey?.pages ?? []) {
for (final field in page.fields) {
if (!_uiFieldValues.containsKey(field.columnName)) {
_uiFieldValues[field.columnName] = null;
}
}
}
}
// Callback when field value changes
void _onUIFieldValuesChanged(String key, dynamic value) {
_uiFieldValues[key] = value;
setState(() {});
}
// Callback when field object changes
void _onUIFieldsChange(BaseUIField uiField) {
final fieldName = uiField.field.columnName;
_uiFieldMap[fieldName] = uiField;
}
// Callback when navigation state changes
void _onNavigationStateChanged(FormNavigationState state) {
setState(() {
_navigationState = state;
});
}
// Callback when navigation actions are available
void _onNavigationActionsChanged(FormNavigationActions actions) {
_navigationActions = actions;
}
// Save form data
Future _saveEntry() async {
final formData = await _navigationActions!.saveForm();
if (formData != null) {
// Form is valid - save it
final result = await _dataService.createEntry(
formName: _currentSurvey!.formName,
data: formData,
);
if (result.wasSuccessful) {
Navigator.pop(context);
}
} else {
// Form has validation errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please fix all form errors')),
);
}
}
@override
Widget build(BuildContext context) {
if (_currentSurvey == null) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: Text('Add Entry')),
body: Column(
children: [
Expanded(
child: FormBuilder(
surveyForm: _currentSurvey!,
uiFieldValues: _uiFieldValues,
uiFieldMap: _uiFieldMap,
onUIFieldValueChange: _onUIFieldValuesChanged,
onUIFieldsChange: _onUIFieldsChange,
dataService: _dataService,
onFormEnded: (ended) {},
formKey: _formKey,
onNavigationStateChanged: _onNavigationStateChanged,
onNavigationActionsChanged: _onNavigationActionsChanged,
applyPadding: true,
fillMaxSize: false,
),
),
// Navigation controls
if (_navigationState != null) _buildNavigationControls(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _saveEntry,
child: Icon(Icons.save),
),
);
}
Widget _buildNavigationControls() {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Previous button
if (!_navigationState!.isFirstPage)
ElevatedButton(
onPressed: () => _navigationActions!.goToPreviousPage(),
child: Text('Previous'),
),
// Page indicator
Text(
'Page ${_navigationState!.currentPage + 1} of ${_navigationState!.totalPages}',
),
// Next button
if (!_navigationState!.isLastPage)
ElevatedButton(
onPressed: () async {
final success = await _navigationActions!.goToNextPage();
if (!success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fix errors before continuing')),
);
}
},
child: Text('Next'),
),
],
),
);
}
}
π― Available Form Widgets
| Widget | Purpose | Configuration |
|---|---|---|
| TextFieldWidget | Text input with validation | TextFieldConfig |
| DropdownWidget | Enhanced dropdown with search | DropdownConfig |
| LocationDropdownWidget | Hierarchical location selection | DropdownConfig |
| DatePickerWidget | Date selection | DatePickerConfig |
| TimePickerWidget | Time selection | TimePickerConfig |
| PictureWidget | Camera/gallery image capture | PictureConfig |
| SignatureWidget | Signature pad | SignatureConfig |
| MultichoiceWidget | Multi-select options | BaseFieldConfig |
Forms can be customized at 3 levels:
- Default: Built-in widget defaults
- Type-specific: FormConfig for all widgets of a type
- Widget-specific: Per-field override via FormBuilder params
β Form Validation
// Validate entire form
final (isValid, errors) = await _navigationActions!.validateForm();
if (isValid) {
// Form is valid
final formData = await _navigationActions!.saveForm();
} else {
// Show errors
print('Validation errors: $errors');
}
Data Data Module
Comprehensive data management with SQLite storage, cloud sync, and validation workflows.
π₯ Config Import
// Import from project compendium (cloud)
final result = await _dataService.importConfig(
currentUser: currentUser,
projectCompendiumId: selectedProject.id,
localConfigFile: null,
onProgressUpdate: (message) {
print('Progress: $message');
// Messages: "Downloading...", "Validating...", "Parsing...", "Complete"
},
);
result.fold(
onSuccess: (_) {
// Access imported survey
final survey = _dataService.currentSurvey;
print('Imported: ${survey?.name}');
},
onFailure: (error) {
print('Import failed: ${error.message}');
},
);
// Import from local file
final file = File('/path/to/config.json');
await _dataService.importConfig(
currentUser: currentUser,
projectCompendiumId: 0,
localConfigFile: file,
onProgressUpdate: (message) => print(message),
);
βοΈ CRUD Operations
Create Entry
final result = await _dataService.createEntry(
formName: "survey_form",
data: {
'name': 'John Doe',
'age': 25,
'location': 'Lagos',
// ... other field values
},
status: EntryStatus.draft, // Optional
geometry: '{"type": "Point", "coordinates": [3.4, 6.5]}', // Optional
);
if (result.wasSuccessful) {
final entry = result.data!;
print('Entry created with ID: ${entry.id}');
}
Read Entries
// Get all entries for a form
final result = await _dataService.getAllEntries(formName: "survey_form");
result.fold(
onSuccess: (List entries) {
print('Found ${entries.length} entries');
for (final entry in entries) {
print('ID: ${entry.id}');
print('Status: ${entry.status}');
print('Data: ${entry.properties}'); // Map
print('Created by: ${entry.constants.userName}');
print('Location: ${entry.constants.userLat}, ${entry.constants.userLong}');
}
},
onFailure: (error) => print('Error: ${error.message}'),
);
// Get entry count
final countResult = await _dataService.getEntriesCount(formName: "survey_form");
print('Total entries: ${countResult.data}');
// Get unexported count
final unexportedResult = await _dataService.getUnexportedEntriesCount(
formName: "survey_form",
);
print('Unexported: ${unexportedResult.data}');
Update Entry
final result = await _dataService.updateEntry(
formName: "survey_form",
entryId: entry.id,
data: {
'name': 'Jane Doe', // Updated value
'age': 26,
// ... other updated fields
},
);
if (result.wasSuccessful) {
print('Entry updated successfully');
}
Delete Entry
final result = await _dataService.deleteEntry(
formName: "survey_form",
entry: entryToDelete,
);
result.fold(
onSuccess: (_) => print('Entry deleted'),
onFailure: (error) => print('Delete failed: ${error.message}'),
);
βοΈ Export to Cloud
// Export data to cloud
final result = await _dataService.exportToCloud(
currentUser: currentUser,
surveyId: survey.id,
formId: form.id,
onProgressUpdate: (ExportProgressModel progress) {
print('Exporting batch ${progress.currentBatch}');
print('Records exported: ${progress.exportedRecords}/${progress.totalRecords}');
print('Status: ${progress.status}');
},
);
result.fold(
onSuccess: (exportResult) {
print('Export completed!');
print('Entries exported: ${exportResult.entriesExported}');
print('Data export: ${exportResult.detailedResults.dataExportSuccess}');
print('Summary export: ${exportResult.detailedResults.summaryExportSuccess}');
// Check individual results
final added = exportResult.detailedResults.addedEntriesResult;
print('Added: ${added.successful}/${added.attempted}');
final updated = exportResult.detailedResults.updatedEntriesResult;
print('Updated: ${updated.successful}/${updated.attempted}');
},
onFailure: (error) {
print('Export failed: ${error.message}');
},
);
π Validation Workflow
// Get validation fields (query parameters)
final fieldsResult = await _dataService.getValidationFields(
configId: survey.id,
);
// Get validation data from cloud
final result = await _dataService.getValidationData(
currentUser: currentUser,
surveyId: survey.id,
saveData: true, // Save to local database
onProgressUpdate: (progress) {
print('Fetching batch ${progress.currentBatch}');
print('Fetched: ${progress.fetchedRecords}');
},
);
result.fold(
onSuccess: (finalProgress) {
print('Validation data fetched!');
print('Total records: ${finalProgress.totalRecords}');
},
onFailure: (error) => print('Fetch failed: ${error.message}'),
);
π Status Codes Reference
| Code | Status | Description | Export Behavior |
|---|---|---|---|
| 0 | Draft | Entry not yet exported | Will be exported as new |
| 1 | Exported | Successfully exported to cloud | Will be skipped |
| 2 | Export Updated | Updated after initial export | Will be exported as update |
| 3 | Exported (alt) | Alternative exported status | Can be re-exported |
| 4 | Export Updated (alt) | Updated after export | Will be exported as update |
| 5 | DL Unvalidated | Downloaded from cloud, not validated | Read-only |
| 6 | DL Validated | Downloaded and validated | Can be exported |
| 7 | DL Exported | Downloaded data, then exported | Will be skipped |
| 8 | DL Revalidated | Downloaded, validated again | Will be exported |
Status codes automatically change based on operations:
- New entries start at status 0 (Draft)
- After export, status changes to 1 (Exported)
- Updating an exported entry changes status to 2 (Export Updated)
- Downloaded entries have status 5-8
πΌοΈ Image Sync
// Get unsynced images
final result = await _dataService.getUnsyncedImages(formName: "survey_form");
result.fold(
onSuccess: (List images) {
print('Unsynced images: ${images.length}');
// Sync each image
for (final image in images) {
await _dataService.syncImage(
imageId: image.imageId,
imageUrl: image.imageUrl,
token: currentUser.accessToken,
);
}
},
onFailure: (error) => print('Error: ${error.message}'),
);
Location Location Module
Hierarchical Nigerian location services with 6.8MB of CSV data covering zones, states, LGAs, wards, and polling units.
π Location Hierarchy
The module provides a 5-level location hierarchy:
- Zone β Geopolitical zone (6 zones)
- State β State/region (36 states + FCT)
- LGA β Local Government Area (774 LGAs)
- Ward β Electoral ward
- Polling Unit β Polling station
ποΈ Location Service Types
| Service | Data Source | Coverage |
|---|---|---|
| InecLocationService | INEC (Electoral Commission) | Polling units, wards |
| NipostLocationService | NIPOST (Postal Service) | Localities, postal districts |
| MilsatLocationService | Milsat custom data | Custom location units |
π Basic Usage
late final LocationService _locationService = LocationConfig.locationService;
// Get all states
final states = await _locationService.getAllStates();
print('States: ${states.length}'); // 37 (36 + FCT)
// Get LGAs for a state
final lgas = await _locationService.getLgasByState('Lagos');
print('LGAs in Lagos: ${lgas.length}'); // 20
// Get wards for an LGA
final wards = await _locationService.getWardsBy(
state: 'Lagos',
lga: 'Ikeja',
);
print('Wards in Ikeja: ${wards.length}');
// Get polling units
final pollingUnits = await _locationService.getPollingUnitsBy(
state: 'Lagos',
lga: 'Ikeja',
ward: 'Ward 1',
);
print('Polling units: ${pollingUnits.length}');
The package includes 6.8MB of pre-loaded location data:
inec_pu.csv- 6.1MB - INEC polling unitsnipost_loc.csv- 204KB - NIPOST localitiesmilsat.csv- 470KB - Milsat custom locationszones.csv- 672 bytes - Geopolitical zones
Map Map Module
Advanced Mapbox integration for interactive mapping, geometry drawing, and work area visualization.
πΊοΈ MapService Initialization
final mapService = MapService(
geometryType: MapConstants.polygon, // or .point, .line
onMapReady: () {
print('Map is ready');
},
onMapClicked: (lat, lng) {
print('Map clicked at: $lat, $lng');
return true; // Handle the click
},
onGeometryClicked: (geometryId) {
print('Geometry $geometryId clicked');
},
onMapActionPerformed: (undoDepth, redoDepth) {
print('Undo: $undoDepth, Redo: $redoDepth');
},
);
// Initialize with Mapbox map instance
await mapService.initialize(mapboxMap);
βοΈ Drawing Operations
// Start drawing mode
await mapService.startDrawing();
// Stop drawing mode
await mapService.stopDrawing();
// Clear current geometry
await mapService.clearCurrentGeometry();
// Undo last action
await mapService.undoLastAction();
// Redo last undone action
await mapService.redoLastAction();
π― Geometry Management
// Show a geometry on the map
final geometry = GeometryEntity(
id: 1,
type: 'Polygon',
coordinates: [/* coordinate arrays */],
);
await mapService.showGeometry(geometry);
// Hide a geometry
await mapService.hideGeometry(geometryId);
// Clear all geometries
await mapService.clearAllGeometries();
// Get all shown geometries
final geometries = mapService.getShownGeometries();
π Work Area Visualization
// Show work area boundaries
final workArea = WorkAreaModel(
id: 'area_1',
name: 'Survey Area 1',
geometry: '{"type": "Polygon", "coordinates": [...]}',
);
await mapService.showWorkArea(workArea);
// Hide work area
await mapService.hideWorkArea('area_1');
// Clear all work areas
await mapService.clearAllWorkAreas();
π· Camera Controls
// Animate camera to location
await mapService.animateCamera(
latitude: 6.5244,
longitude: 3.3792,
zoom: 16.0,
);
// Zoom to specific feature
await mapService.zoomToFeature(featureId);
// Change base map style
await mapService.setBaseMap(
'mapbox://styles/mapbox/satellite-v9',
);
Commonly used map constants:
MapConstants.point- "point"MapConstants.line- "line"MapConstants.polygon- "polygon"MapConstants.defaultZoom- 16.0MapConstants.clickDebounceMs- 300
Best Practices & Gotchas
Critical tips, common mistakes, and recommended patterns for using milsat_modules_flutter.
π¨ Critical Setup Steps
- Forgetting
WidgetsFlutterBinding.ensureInitialized()before ModulesConfig - Not using
awaiton ModulesConfig.initialize() - Initializing after
runApp()instead of before - Accessing services before initialization
- Not handling both
onSuccessandonFailurein Result.fold()
β Recommended Patterns
1. Service Injection
// β
CORRECT - Use late final
late final UserService _userService = UserConfig.userService;
// β WRONG - Don't initialize directly
final UserService _userService = UserConfig.userService; // May fail!
2. Error Handling
// β
CORRECT - Always handle both cases
result.fold(
onSuccess: (data) { /* handle success */ },
onFailure: (error) { /* handle error */ },
);
// β WRONG - Don't ignore errors
result.fold(
onSuccess: (data) { /* ... */ },
onFailure: (_) {}, // Empty handler!
);
3. Form Validation
// β
CORRECT - Check if form data is valid
final formData = await _navigationActions!.saveForm();
if (formData != null) {
// Form is valid, proceed
} else {
// Form has errors, show message
}
// β WRONG - Don't assume saveForm always returns data
final formData = await _navigationActions!.saveForm();
await _dataService.createEntry(data: formData!); // May crash!
4. Field Initialization
// β
CORRECT - Initialize all fields before FormBuilder
void _initializeAllFieldsInMap() {
for (final page in survey.pages) {
for (final field in page.fields) {
_uiFieldValues[field.columnName] = null;
}
}
}
// β WRONG - Don't use FormBuilder with empty uiFieldValues
FormBuilder(
uiFieldValues: {}, // Empty! Will cause issues
// ...
)
β‘ Performance Tips
- Use
late finalfor service injection (lazy initialization) - Initialize form fields once in
initState() - Avoid unnecessary
setState()calls - Use pagination when fetching large datasets
- Sync images separately before export to avoid timeouts
π Security Considerations
- Tokens are automatically refreshed 60 seconds before expiry
- Tokens stored in secure storage (flutter_secure_storage)
- Always validate user input in forms
- Don't log sensitive data in production (use
isDebugMode: false)
API Reference & Appendix
Complete reference tables, dependencies, and troubleshooting resources.
π Module Comparison
| Module | Purpose | Key Features | Main Dependencies |
|---|---|---|---|
| Common | Foundation | Error handling, Network, Config | dio, flutter_secure_storage |
| User | Auth & Users | Login, Profile, Projects | Inherits Common |
| Form | Dynamic Forms | FormBuilder, Widgets, Validation | image_picker, dropdown_textfield |
| Data | Data Management | CRUD, Export, Sync | sqflite, uuid |
| Location | Hierarchical Locations | States, LGAs, Wards, PUs | 6.8MB CSV data |
| Map | Mapping | Mapbox, Geometry, Drawing | mapbox_maps_flutter |
π¦ Complete Dependencies
dependencies:
# Core
dio: ^5.9.0
equatable: ^2.0.7
get_it: ^8.2.0
# Storage
flutter_secure_storage: ^9.2.4
shared_preferences: ^2.5.3
sqflite: ^2.4.2
path_provider: ^2.1.5
# State Management
flutter_bloc: ^9.1.1
provider: ^6.1.5+1
# UI & Forms
image_picker: ^1.2.0
file_picker: ^10.3.3
dropdown_textfield: ^1.2.0
flutter_svg: ^2.2.0
# Utils
intl: ^0.20.2
uuid: ^4.5.1
path: ^1.9.1
expressions: ^0.2.5+2
json_schema: ^5.2.1
permission_handler: ^12.0.1
device_info_plus: ^12.1.0
# Mapping
mapbox_maps_flutter: ^2.11.0
β Troubleshooting FAQ
Q: "Services are null or throwing errors"
A: Ensure you called ModulesConfig.initialize() with await before accessing services.
Q: "FormBuilder not showing fields"
A: Initialize uiFieldValues with all field column names before rendering FormBuilder.
Q: "Export fails with timeout"
A: Sync images separately first using syncImage(), then export data.
Q: "Location services return empty"
A: Ensure the CSV assets are included in your app's pubspec.yaml (they should be automatically included).
Q: "Token expired errors"
A: The package automatically refreshes tokens. If you see this error, the refresh failed. Check your refresh token configuration.
π Useful Links
You now have everything you need to build sophisticated data collection applications with milsat_modules_flutter. Happy coding!