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.

πŸ“¦ What is milsat_modules_flutter?

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
βœ… Who Should Use This Package?

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(),
    );
  }
}
⚠️ Important Setup Steps
  1. Always call WidgetsFlutterBinding.ensureInitialized() before ModulesConfig
  2. Use await when calling ModulesConfig.initialize()
  3. Initialize BEFORE runApp()
  4. Provide both baseUrl and ppqBaseUrl

πŸ”‘ 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:
baseUrl String Required

Main API endpoint URL

ppqBaseUrl String Required

PPQ (Plant Protection & Quarantine) API endpoint

isDebugMode bool?

Enable debug logging (defaults to false)

formConfig FormConfig?

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
πŸ” Token Management

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 login
  • generateOtp(String phoneNumber) - Generate OTP for user
  • logout() - Clear session and log out
  • isLoggedIn() - Check if user is currently logged in
  • validateOtp(String email, String otp) - Validate OTP code
  • createAccount(CreateAccountModel model) - Register new account
  • resetPassword(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),
);
βœ… CurrentUserEntity Properties
  • userId - Unique user identifier
  • accessToken - Authentication token
  • profile - 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
🎨 3-Tier Configuration System

Forms can be customized at 3 levels:

  1. Default: Built-in widget defaults
  2. Type-specific: FormConfig for all widgets of a type
  3. 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 Management

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:

  1. Zone β†’ Geopolitical zone (6 zones)
  2. State β†’ State/region (36 states + FCT)
  3. LGA β†’ Local Government Area (774 LGAs)
  4. Ward β†’ Electoral ward
  5. 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}');
πŸ’Ύ CSV Data Assets

The package includes 6.8MB of pre-loaded location data:

  • inec_pu.csv - 6.1MB - INEC polling units
  • nipost_loc.csv - 204KB - NIPOST localities
  • milsat.csv - 470KB - Milsat custom locations
  • zones.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',
);
🎨 MapConstants Reference

Commonly used map constants:

  • MapConstants.point - "point"
  • MapConstants.line - "line"
  • MapConstants.polygon - "polygon"
  • MapConstants.defaultZoom - 16.0
  • MapConstants.clickDebounceMs - 300

Best Practices & Gotchas

Critical tips, common mistakes, and recommended patterns for using milsat_modules_flutter.

🚨 Critical Setup Steps

❌ Common Mistakes to Avoid
  1. Forgetting WidgetsFlutterBinding.ensureInitialized() before ModulesConfig
  2. Not using await on ModulesConfig.initialize()
  3. Initializing after runApp() instead of before
  4. Accessing services before initialization
  5. Not handling both onSuccess and onFailure in 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 final for 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

πŸŽ‰ Ready to Build!

You now have everything you need to build sophisticated data collection applications with milsat_modules_flutter. Happy coding!