Skip to content

Extending CulicidaeLab Mobile Functionality

Overview

The CulicidaeLab mobile application is designed with extensibility in mind, providing multiple pathways for researchers, developers, and institutions to customize and extend its functionality. This guide covers plugin development, custom model integration, data pipeline extensions, and UI customization approaches.

Architecture for Extensions

Extension Points

The application provides several well-defined extension points:

graph TB
    subgraph "Core Application"
        UI[User Interface Layer]
        VM[ViewModel Layer]
        SVC[Service Layer]
        REPO[Repository Layer]
        MODEL[Model Layer]
    end

    subgraph "Extension Points"
        UI_EXT[UI Extensions]
        PLUGIN[Plugin System]
        MODEL_EXT[Custom Models]
        DATA_EXT[Data Extensions]
        API_EXT[API Extensions]
    end

    subgraph "External Systems"
        EXT_API[External APIs]
        EXT_DB[External Databases]
        EXT_ML[External ML Models]
        EXT_SVC[External Services]
    end

    UI --> UI_EXT
    VM --> PLUGIN
    SVC --> MODEL_EXT
    REPO --> DATA_EXT
    MODEL --> API_EXT

    UI_EXT --> EXT_SVC
    PLUGIN --> EXT_API
    MODEL_EXT --> EXT_ML
    DATA_EXT --> EXT_DB
    API_EXT --> EXT_API

Plugin Architecture

The core plugin system provides a standardized way to extend functionality:

// Core plugin interface
abstract class CulicidaeLabPlugin {
  /// Unique plugin identifier
  String get pluginId;

  /// Human-readable plugin name
  String get name;

  /// Plugin version
  String get version;

  /// List of features this plugin provides
  List<String> get supportedFeatures;

  /// Plugin dependencies
  List<String> get dependencies;

  /// Initialize the plugin
  Future<void> initialize(PluginContext context);

  /// Process data through the plugin
  Future<PluginResult> processData(PluginInput input);

  /// Get plugin configuration UI
  Widget? getConfigurationWidget();

  /// Cleanup plugin resources
  Future<void> dispose();

  /// Plugin metadata
  Map<String, dynamic> get metadata;
}

// Plugin context provides access to core services
class PluginContext {
  final DatabaseService databaseService;
  final ClassificationService classificationService;
  final UserService userService;
  final Map<String, dynamic> appConfiguration;
  final PluginLogger logger;

  PluginContext({
    required this.databaseService,
    required this.classificationService,
    required this.userService,
    required this.appConfiguration,
    required this.logger,
  });
}

// Standardized plugin input/output
class PluginInput {
  final String operation;
  final Map<String, dynamic> data;
  final Map<String, dynamic> metadata;

  PluginInput({
    required this.operation,
    required this.data,
    this.metadata = const {},
  });
}

class PluginResult {
  final bool success;
  final Map<String, dynamic> data;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  PluginResult({
    required this.success,
    this.data = const {},
    this.errorMessage,
    this.metadata = const {},
  });

  factory PluginResult.success(Map<String, dynamic> data) {
    return PluginResult(success: true, data: data);
  }

  factory PluginResult.error(String message) {
    return PluginResult(success: false, errorMessage: message);
  }
}

Custom Classification Models

Model Integration Interface

// Interface for custom classification models
abstract class CustomClassificationModel {
  /// Model identifier
  String get modelId;

  /// Model version
  String get version;

  /// Supported species list
  List<String> get supportedSpecies;

  /// Model metadata
  ModelMetadata get metadata;

  /// Load the model
  Future<void> loadModel();

  /// Classify an image
  Future<ClassificationResult> classify(File imageFile);

  /// Batch classification
  Future<List<ClassificationResult>> classifyBatch(List<File> imageFiles);

  /// Get model performance metrics
  Future<ModelPerformanceMetrics> getPerformanceMetrics();

  /// Unload the model
  Future<void> unloadModel();

  /// Check if model is loaded
  bool get isLoaded;
}

class ModelMetadata {
  final String name;
  final String description;
  final String author;
  final DateTime createdAt;
  final String license;
  final List<String> trainingDatasets;
  final Map<String, dynamic> hyperparameters;
  final ModelPerformanceMetrics performanceMetrics;

  ModelMetadata({
    required this.name,
    required this.description,
    required this.author,
    required this.createdAt,
    required this.license,
    required this.trainingDatasets,
    required this.hyperparameters,
    required this.performanceMetrics,
  });
}

class ModelPerformanceMetrics {
  final double accuracy;
  final double precision;
  final double recall;
  final double f1Score;
  final Map<String, double> perClassAccuracy;
  final int totalParameters;
  final double modelSizeMB;
  final double averageInferenceTimeMs;

  ModelPerformanceMetrics({
    required this.accuracy,
    required this.precision,
    required this.recall,
    required this.f1Score,
    required this.perClassAccuracy,
    required this.totalParameters,
    required this.modelSizeMB,
    required this.averageInferenceTimeMs,
  });
}

TensorFlow Lite Model Example

class CustomTensorFlowLiteModel implements CustomClassificationModel {
  late Interpreter _interpreter;
  late List<String> _labels;
  bool _isLoaded = false;

  @override
  String get modelId => 'custom_tflite_mosquito_v2';

  @override
  String get version => '2.1.0';

  @override
  List<String> get supportedSpecies => _labels;

  @override
  ModelMetadata get metadata => ModelMetadata(
    name: 'Custom TensorFlow Lite Mosquito Classifier',
    description: 'Enhanced mosquito species classification model with improved accuracy',
    author: 'Research Team XYZ',
    createdAt: DateTime(2024, 1, 15),
    license: 'Apache 2.0',
    trainingDatasets: ['mosquito_dataset_46_3139', 'custom_field_data_2024'],
    hyperparameters: {
      'learning_rate': 0.001,
      'batch_size': 32,
      'epochs': 100,
      'optimizer': 'Adam',
      'input_size': [224, 224, 3],
    },
    performanceMetrics: ModelPerformanceMetrics(
      accuracy: 0.94,
      precision: 0.93,
      recall: 0.92,
      f1Score: 0.925,
      perClassAccuracy: {
        'Aedes aegypti': 0.96,
        'Aedes albopictus': 0.94,
        'Culex pipiens': 0.91,
        // ... other species
      },
      totalParameters: 2500000,
      modelSizeMB: 9.8,
      averageInferenceTimeMs: 180.0,
    ),
  );

  @override
  bool get isLoaded => _isLoaded;

  @override
  Future<void> loadModel() async {
    try {
      // Load the TensorFlow Lite model
      final modelFile = await _loadModelAsset('assets/models/custom_model.tflite');
      _interpreter = Interpreter.fromFile(modelFile);

      // Load labels
      final labelsString = await rootBundle.loadString('assets/models/custom_labels.txt');
      _labels = labelsString.split('\n').where((label) => label.isNotEmpty).toList();

      _isLoaded = true;
      print('Custom TensorFlow Lite model loaded successfully');
    } catch (e) {
      throw ModelLoadException('Failed to load custom TensorFlow Lite model: $e');
    }
  }

  @override
  Future<ClassificationResult> classify(File imageFile) async {
    if (!_isLoaded) {
      throw ModelNotLoadedException('Model must be loaded before classification');
    }

    final stopwatch = Stopwatch()..start();

    try {
      // Preprocess image
      final input = await _preprocessImage(imageFile);

      // Run inference
      final output = List.filled(_labels.length, 0.0).reshape([1, _labels.length]);
      _interpreter.run(input, output);

      // Post-process results
      final probabilities = output[0] as List<double>;
      final maxIndex = probabilities.indexOf(probabilities.reduce(math.max));
      final confidence = probabilities[maxIndex];
      final speciesName = _labels[maxIndex];

      stopwatch.stop();

      // Create species object
      final species = await _createSpeciesFromName(speciesName);
      final relatedDiseases = await _getRelatedDiseases(species);

      return ClassificationResult(
        species: species,
        confidence: confidence,
        inferenceTime: stopwatch.elapsedMilliseconds,
        relatedDiseases: relatedDiseases,
        imageFile: imageFile,
      );
    } catch (e) {
      throw ClassificationException('Classification failed: $e');
    }
  }

  @override
  Future<List<ClassificationResult>> classifyBatch(List<File> imageFiles) async {
    final results = <ClassificationResult>[];

    for (final imageFile in imageFiles) {
      try {
        final result = await classify(imageFile);
        results.add(result);
      } catch (e) {
        print('Failed to classify ${imageFile.path}: $e');
        // Continue with other images
      }
    }

    return results;
  }

  Future<List<List<List<List<double>>>>> _preprocessImage(File imageFile) async {
    // Load and decode image
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw ImageProcessingException('Failed to decode image');
    }

    // Resize to model input size
    final resized = img.copyResize(image, width: 224, height: 224);

    // Convert to normalized float array
    final input = List.generate(
      1,
      (batch) => List.generate(
        224,
        (y) => List.generate(
          224,
          (x) => List.generate(3, (c) {
            final pixel = resized.getPixel(x, y);
            switch (c) {
              case 0: return (pixel.r / 255.0 - 0.485) / 0.229; // Red channel
              case 1: return (pixel.g / 255.0 - 0.456) / 0.224; // Green channel
              case 2: return (pixel.b / 255.0 - 0.406) / 0.225; // Blue channel
              default: return 0.0;
            }
          }),
        ),
      ),
    );

    return input;
  }

  @override
  Future<ModelPerformanceMetrics> getPerformanceMetrics() async {
    return metadata.performanceMetrics;
  }

  @override
  Future<void> unloadModel() async {
    if (_isLoaded) {
      _interpreter.close();
      _isLoaded = false;
      print('Custom TensorFlow Lite model unloaded');
    }
  }

  Future<File> _loadModelAsset(String assetPath) async {
    final byteData = await rootBundle.load(assetPath);
    final tempDir = await getTemporaryDirectory();
    final file = File('${tempDir.path}/custom_model.tflite');
    await file.writeAsBytes(byteData.buffer.asUint8List());
    return file;
  }
}

// Custom exceptions
class ModelLoadException implements Exception {
  final String message;
  ModelLoadException(this.message);
  @override
  String toString() => 'ModelLoadException: $message';
}

class ModelNotLoadedException implements Exception {
  final String message;
  ModelNotLoadedException(this.message);
  @override
  String toString() => 'ModelNotLoadedException: $message';
}

class ClassificationException implements Exception {
  final String message;
  ClassificationException(this.message);
  @override
  String toString() => 'ClassificationException: $message';
}

class ImageProcessingException implements Exception {
  final String message;
  ImageProcessingException(this.message);
  @override
  String toString() => 'ImageProcessingException: $message';
}

ONNX Model Integration

class ONNXClassificationModel implements CustomClassificationModel {
  late OnnxDart _session;
  late List<String> _labels;
  bool _isLoaded = false;

  @override
  String get modelId => 'onnx_mosquito_classifier_v1';

  @override
  String get version => '1.0.0';

  @override
  List<String> get supportedSpecies => _labels;

  @override
  bool get isLoaded => _isLoaded;

  @override
  Future<void> loadModel() async {
    try {
      // Load ONNX model
      final modelBytes = await rootBundle.load('assets/models/mosquito_classifier.onnx');
      _session = OnnxDart.createSession(modelBytes.buffer.asUint8List());

      // Load labels
      final labelsString = await rootBundle.loadString('assets/models/onnx_labels.txt');
      _labels = labelsString.split('\n').where((label) => label.isNotEmpty).toList();

      _isLoaded = true;
      print('ONNX model loaded successfully');
    } catch (e) {
      throw ModelLoadException('Failed to load ONNX model: $e');
    }
  }

  @override
  Future<ClassificationResult> classify(File imageFile) async {
    if (!_isLoaded) {
      throw ModelNotLoadedException('ONNX model must be loaded before classification');
    }

    final stopwatch = Stopwatch()..start();

    try {
      // Preprocess image for ONNX
      final inputTensor = await _preprocessImageForONNX(imageFile);

      // Run ONNX inference
      final outputs = _session.run({'input': inputTensor});
      final probabilities = outputs['output'] as List<double>;

      // Find best prediction
      final maxIndex = probabilities.indexOf(probabilities.reduce(math.max));
      final confidence = probabilities[maxIndex];
      final speciesName = _labels[maxIndex];

      stopwatch.stop();

      // Create result
      final species = await _createSpeciesFromName(speciesName);
      final relatedDiseases = await _getRelatedDiseases(species);

      return ClassificationResult(
        species: species,
        confidence: confidence,
        inferenceTime: stopwatch.elapsedMilliseconds,
        relatedDiseases: relatedDiseases,
        imageFile: imageFile,
      );
    } catch (e) {
      throw ClassificationException('ONNX classification failed: $e');
    }
  }

  Future<List<List<List<double>>>> _preprocessImageForONNX(File imageFile) async {
    // ONNX-specific preprocessing
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw ImageProcessingException('Failed to decode image for ONNX');
    }

    final resized = img.copyResize(image, width: 224, height: 224);

    // ONNX expects CHW format (Channel, Height, Width)
    final input = List.generate(3, (c) =>
      List.generate(224, (h) =>
        List.generate(224, (w) {
          final pixel = resized.getPixel(w, h);
          switch (c) {
            case 0: return (pixel.r / 255.0 - 0.485) / 0.229;
            case 1: return (pixel.g / 255.0 - 0.456) / 0.224;
            case 2: return (pixel.b / 255.0 - 0.406) / 0.225;
            default: return 0.0;
          }
        })
      )
    );

    return input;
  }

  @override
  Future<void> unloadModel() async {
    if (_isLoaded) {
      _session.release();
      _isLoaded = false;
      print('ONNX model unloaded');
    }
  }
}

Data Pipeline Extensions

Custom Data Processors

// Interface for data processing plugins
abstract class DataProcessor {
  String get processorId;
  String get name;
  List<String> get supportedDataTypes;

  Future<ProcessingResult> processData(ProcessingInput input);
  Future<void> initialize();
  Future<void> dispose();
}

// Environmental data processor example
class EnvironmentalDataProcessor implements DataProcessor {
  late WeatherAPI _weatherAPI;
  late SoilAPI _soilAPI;

  @override
  String get processorId => 'environmental_data_processor';

  @override
  String get name => 'Environmental Data Processor';

  @override
  List<String> get supportedDataTypes => [
    'weather_data',
    'soil_data',
    'vegetation_data',
    'water_quality_data'
  ];

  @override
  Future<void> initialize() async {
    _weatherAPI = WeatherAPI(apiKey: await _getWeatherAPIKey());
    _soilAPI = SoilAPI(apiKey: await _getSoilAPIKey());
  }

  @override
  Future<ProcessingResult> processData(ProcessingInput input) async {
    final location = Location.fromJson(input.data['location']);
    final timestamp = DateTime.parse(input.data['timestamp']);

    try {
      final results = <String, dynamic>{};

      // Collect weather data
      if (input.requestedTypes.contains('weather_data')) {
        results['weather_data'] = await _collectWeatherData(location, timestamp);
      }

      // Collect soil data
      if (input.requestedTypes.contains('soil_data')) {
        results['soil_data'] = await _collectSoilData(location);
      }

      // Collect vegetation data
      if (input.requestedTypes.contains('vegetation_data')) {
        results['vegetation_data'] = await _collectVegetationData(location);
      }

      return ProcessingResult.success(results);
    } catch (e) {
      return ProcessingResult.error('Environmental data collection failed: $e');
    }
  }

  Future<Map<String, dynamic>> _collectWeatherData(Location location, DateTime timestamp) async {
    final weatherData = await _weatherAPI.getWeatherData(
      latitude: location.lat,
      longitude: location.lng,
      timestamp: timestamp,
    );

    return {
      'temperature_celsius': weatherData.temperature,
      'humidity_percent': weatherData.humidity,
      'pressure_hpa': weatherData.pressure,
      'wind_speed_ms': weatherData.windSpeed,
      'wind_direction_degrees': weatherData.windDirection,
      'precipitation_mm': weatherData.precipitation,
      'cloud_cover_percent': weatherData.cloudCover,
      'uv_index': weatherData.uvIndex,
      'visibility_km': weatherData.visibility,
    };
  }

  Future<Map<String, dynamic>> _collectSoilData(Location location) async {
    final soilData = await _soilAPI.getSoilData(
      latitude: location.lat,
      longitude: location.lng,
    );

    return {
      'soil_type': soilData.type,
      'ph_level': soilData.phLevel,
      'organic_matter_percent': soilData.organicMatter,
      'nitrogen_ppm': soilData.nitrogen,
      'phosphorus_ppm': soilData.phosphorus,
      'potassium_ppm': soilData.potassium,
      'moisture_percent': soilData.moisture,
      'temperature_celsius': soilData.temperature,
    };
  }

  Future<Map<String, dynamic>> _collectVegetationData(Location location) async {
    // Use satellite imagery or local databases for vegetation data
    return {
      'vegetation_type': 'mixed_forest',
      'canopy_cover_percent': 75.0,
      'dominant_species': ['Oak', 'Pine', 'Birch'],
      'ndvi_index': 0.65,
      'leaf_area_index': 4.2,
    };
  }

  @override
  Future<void> dispose() async {
    await _weatherAPI.dispose();
    await _soilAPI.dispose();
  }
}

class ProcessingInput {
  final Map<String, dynamic> data;
  final List<String> requestedTypes;
  final Map<String, dynamic> options;

  ProcessingInput({
    required this.data,
    required this.requestedTypes,
    this.options = const {},
  });
}

class ProcessingResult {
  final bool success;
  final Map<String, dynamic> data;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  ProcessingResult({
    required this.success,
    this.data = const {},
    this.errorMessage,
    this.metadata = const {},
  });

  factory ProcessingResult.success(Map<String, dynamic> data) {
    return ProcessingResult(success: true, data: data);
  }

  factory ProcessingResult.error(String message) {
    return ProcessingResult(success: false, errorMessage: message);
  }
}

Custom Export Formats

// Interface for custom export formats
abstract class DataExporter {
  String get exporterId;
  String get name;
  String get fileExtension;
  List<String> get supportedDataTypes;

  Future<ExportResult> exportData(ExportRequest request);
  Future<bool> validateData(List<dynamic> data);
}

// Research-specific exporter
class ResearchDataExporter implements DataExporter {
  @override
  String get exporterId => 'research_data_exporter';

  @override
  String get name => 'Research Data Exporter';

  @override
  String get fileExtension => '.rdf'; // Research Data Format

  @override
  List<String> get supportedDataTypes => [
    'observations',
    'classifications',
    'environmental_data',
    'species_data'
  ];

  @override
  Future<ExportResult> exportData(ExportRequest request) async {
    try {
      final exportData = <String, dynamic>{};

      // Add metadata
      exportData['metadata'] = {
        'export_timestamp': DateTime.now().toIso8601String(),
        'exporter_version': '1.0.0',
        'data_format_version': '2.1',
        'total_records': request.data.length,
        'export_parameters': request.parameters,
      };

      // Process different data types
      if (request.dataType == 'observations') {
        exportData['observations'] = await _exportObservations(request.data as List<Observation>);
      } else if (request.dataType == 'classifications') {
        exportData['classifications'] = await _exportClassifications(request.data as List<ClassificationResult>);
      }

      // Add analysis if requested
      if (request.parameters['include_analysis'] == true) {
        exportData['analysis'] = await _generateAnalysis(request.data);
      }

      // Convert to specified format
      final content = await _formatData(exportData, request.format);

      return ExportResult.success(content);
    } catch (e) {
      return ExportResult.error('Export failed: $e');
    }
  }

  Future<List<Map<String, dynamic>>> _exportObservations(List<Observation> observations) async {
    return observations.map((obs) => {
      'observation_id': obs.id,
      'species': {
        'scientific_name': obs.speciesScientificName,
        'taxonomic_classification': await _getTaxonomicClassification(obs.speciesScientificName),
      },
      'spatial': {
        'latitude': obs.location.lat,
        'longitude': obs.location.lng,
        'coordinate_system': 'WGS84',
        'accuracy_meters': obs.locationAccuracyM,
        'elevation_meters': null, // Could be added if available
      },
      'temporal': {
        'observed_at': obs.observedAt.toIso8601String(),
        'day_of_year': obs.observedAt.dayOfYear,
        'season': _calculateSeason(obs.observedAt),
        'time_of_day': _calculateTimeOfDay(obs.observedAt),
      },
      'identification': {
        'confidence': obs.confidence,
        'model_id': obs.modelId,
        'identification_method': obs.dataSource,
        'expert_verified': false, // Could be extended
      },
      'context': {
        'observer_notes': obs.notes,
        'specimen_count': obs.count,
        'additional_metadata': obs.metadata,
      },
    }).toList();
  }

  Future<String> _formatData(Map<String, dynamic> data, String format) async {
    switch (format.toLowerCase()) {
      case 'json':
        return jsonEncode(data);
      case 'yaml':
        return _convertToYAML(data);
      case 'xml':
        return _convertToXML(data);
      case 'csv':
        return _convertToCSV(data);
      default:
        throw ArgumentError('Unsupported format: $format');
    }
  }

  @override
  Future<bool> validateData(List<dynamic> data) async {
    // Implement data validation logic
    for (final item in data) {
      if (item is Observation) {
        if (!_isValidObservation(item)) return false;
      } else if (item is ClassificationResult) {
        if (!_isValidClassificationResult(item)) return false;
      }
    }
    return true;
  }

  bool _isValidObservation(Observation obs) {
    return obs.id.isNotEmpty &&
           obs.speciesScientificName.isNotEmpty &&
           obs.location.lat >= -90 && obs.location.lat <= 90 &&
           obs.location.lng >= -180 && obs.location.lng <= 180;
  }
}

class ExportRequest {
  final String dataType;
  final List<dynamic> data;
  final String format;
  final Map<String, dynamic> parameters;

  ExportRequest({
    required this.dataType,
    required this.data,
    required this.format,
    this.parameters = const {},
  });
}

class ExportResult {
  final bool success;
  final String? content;
  final String? errorMessage;
  final Map<String, dynamic> metadata;

  ExportResult({
    required this.success,
    this.content,
    this.errorMessage,
    this.metadata = const {},
  });

  factory ExportResult.success(String content) {
    return ExportResult(success: true, content: content);
  }

  factory ExportResult.error(String message) {
    return ExportResult(success: false, errorMessage: message);
  }
}

UI Extensions

Custom Screens and Widgets

// Interface for UI extensions
abstract class UIExtension {
  String get extensionId;
  String get name;
  List<String> get supportedScreens;

  Widget buildExtensionWidget(BuildContext context, Map<String, dynamic> parameters);
  List<NavigationItem> getNavigationItems();
  List<ActionItem> getActionItems(String screenId);
}

// Research tools UI extension
class ResearchToolsExtension implements UIExtension {
  @override
  String get extensionId => 'research_tools_extension';

  @override
  String get name => 'Research Tools';

  @override
  List<String> get supportedScreens => [
    'data_analysis',
    'field_session',
    'batch_processing',
    'export_manager'
  ];

  @override
  Widget buildExtensionWidget(BuildContext context, Map<String, dynamic> parameters) {
    final screenId = parameters['screen_id'] as String;

    switch (screenId) {
      case 'data_analysis':
        return DataAnalysisScreen();
      case 'field_session':
        return FieldSessionScreen();
      case 'batch_processing':
        return BatchProcessingScreen();
      case 'export_manager':
        return ExportManagerScreen();
      default:
        return Container(
          child: Text('Unknown screen: $screenId'),
        );
    }
  }

  @override
  List<NavigationItem> getNavigationItems() {
    return [
      NavigationItem(
        id: 'research_tools',
        title: 'Research Tools',
        icon: Icons.science,
        route: '/research-tools',
      ),
      NavigationItem(
        id: 'data_analysis',
        title: 'Data Analysis',
        icon: Icons.analytics,
        route: '/research-tools/analysis',
      ),
    ];
  }

  @override
  List<ActionItem> getActionItems(String screenId) {
    switch (screenId) {
      case 'classification_screen':
        return [
          ActionItem(
            id: 'start_field_session',
            title: 'Start Field Session',
            icon: Icons.play_arrow,
            onTap: (context) => _startFieldSession(context),
          ),
        ];
      case 'observation_details_screen':
        return [
          ActionItem(
            id: 'add_environmental_data',
            title: 'Add Environmental Data',
            icon: Icons.eco,
            onTap: (context) => _addEnvironmentalData(context),
          ),
        ];
      default:
        return [];
    }
  }

  void _startFieldSession(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => FieldSessionScreen()),
    );
  }

  void _addEnvironmentalData(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => EnvironmentalDataDialog(),
    );
  }
}

class NavigationItem {
  final String id;
  final String title;
  final IconData icon;
  final String route;

  NavigationItem({
    required this.id,
    required this.title,
    required this.icon,
    required this.route,
  });
}

class ActionItem {
  final String id;
  final String title;
  final IconData icon;
  final Function(BuildContext) onTap;

  ActionItem({
    required this.id,
    required this.title,
    required this.icon,
    required this.onTap,
  });
}

// Custom research screen example
class DataAnalysisScreen extends StatefulWidget {
  @override
  _DataAnalysisScreenState createState() => _DataAnalysisScreenState();
}

class _DataAnalysisScreenState extends State<DataAnalysisScreen> {
  List<Observation> _observations = [];
  Map<String, dynamic> _analysisResults = {};
  bool _isAnalyzing = false;

  @override
  void initState() {
    super.initState();
    _loadObservations();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Data Analysis'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _refreshAnalysis,
          ),
          IconButton(
            icon: Icon(Icons.export_notes),
            onPressed: _exportResults,
          ),
        ],
      ),
      body: Column(
        children: [
          _buildAnalysisControls(),
          Expanded(
            child: _isAnalyzing
                ? Center(child: CircularProgressIndicator())
                : _buildAnalysisResults(),
          ),
        ],
      ),
    );
  }

  Widget _buildAnalysisControls() {
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Analysis Options',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(Icons.timeline),
                    label: Text('Temporal Analysis'),
                    onPressed: () => _runAnalysis('temporal'),
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(Icons.map),
                    label: Text('Spatial Analysis'),
                    onPressed: () => _runAnalysis('spatial'),
                  ),
                ),
              ],
            ),
            SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(Icons.pie_chart),
                    label: Text('Species Distribution'),
                    onPressed: () => _runAnalysis('species'),
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(Icons.assessment),
                    label: Text('Quality Metrics'),
                    onPressed: () => _runAnalysis('quality'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAnalysisResults() {
    if (_analysisResults.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.analytics, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text(
              'Select an analysis type to begin',
              style: Theme.of(context).textTheme.titleMedium,
            ),
          ],
        ),
      );
    }

    return ListView(
      padding: EdgeInsets.all(16),
      children: [
        _buildResultCard('Summary', _analysisResults['summary']),
        _buildResultCard('Charts', _analysisResults['charts']),
        _buildResultCard('Statistics', _analysisResults['statistics']),
        _buildResultCard('Recommendations', _analysisResults['recommendations']),
      ],
    );
  }

  Widget _buildResultCard(String title, dynamic data) {
    if (data == null) return SizedBox.shrink();

    return Card(
      margin: EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            SizedBox(height: 8),
            _buildDataVisualization(title, data),
          ],
        ),
      ),
    );
  }

  Widget _buildDataVisualization(String type, dynamic data) {
    // Implement different visualizations based on data type
    switch (type) {
      case 'Charts':
        return _buildCharts(data);
      case 'Statistics':
        return _buildStatistics(data);
      case 'Summary':
        return _buildSummary(data);
      case 'Recommendations':
        return _buildRecommendations(data);
      default:
        return Text(data.toString());
    }
  }

  Future<void> _loadObservations() async {
    // Load observations from database
    final repository = locator<ClassificationRepository>();
    // Implementation would load observations
  }

  Future<void> _runAnalysis(String analysisType) async {
    setState(() {
      _isAnalyzing = true;
    });

    try {
      final analyzer = DataAnalyzer();
      final results = await analyzer.runAnalysis(analysisType, _observations);

      setState(() {
        _analysisResults = results;
        _isAnalyzing = false;
      });
    } catch (e) {
      setState(() {
        _isAnalyzing = false;
      });

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Analysis failed: $e')),
      );
    }
  }

  void _refreshAnalysis() {
    _loadObservations();
    setState(() {
      _analysisResults.clear();
    });
  }

  void _exportResults() {
    // Implement export functionality
    showDialog(
      context: context,
      builder: (context) => ExportDialog(data: _analysisResults),
    );
  }
}

Configuration and Settings

Plugin Configuration System

// Plugin configuration management
class PluginConfigurationManager {
  static const String _configKey = 'plugin_configurations';

  static Future<Map<String, dynamic>> getPluginConfiguration(String pluginId) async {
    final prefs = await SharedPreferences.getInstance();
    final configJson = prefs.getString('${_configKey}_$pluginId');

    if (configJson != null) {
      return jsonDecode(configJson);
    }

    return {};
  }

  static Future<void> savePluginConfiguration(
    String pluginId,
    Map<String, dynamic> configuration
  ) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('${_configKey}_$pluginId', jsonEncode(configuration));
  }

  static Future<List<String>> getEnabledPlugins() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getStringList('enabled_plugins') ?? [];
  }

  static Future<void> setPluginEnabled(String pluginId, bool enabled) async {
    final prefs = await SharedPreferences.getInstance();
    final enabledPlugins = await getEnabledPlugins();

    if (enabled && !enabledPlugins.contains(pluginId)) {
      enabledPlugins.add(pluginId);
    } else if (!enabled && enabledPlugins.contains(pluginId)) {
      enabledPlugins.remove(pluginId);
    }

    await prefs.setStringList('enabled_plugins', enabledPlugins);
  }
}

// Configuration UI
class PluginConfigurationScreen extends StatefulWidget {
  @override
  _PluginConfigurationScreenState createState() => _PluginConfigurationScreenState();
}

class _PluginConfigurationScreenState extends State<PluginConfigurationScreen> {
  List<CulicidaeLabPlugin> _availablePlugins = [];
  List<String> _enabledPlugins = [];

  @override
  void initState() {
    super.initState();
    _loadPluginConfiguration();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Plugin Configuration'),
      ),
      body: ListView.builder(
        itemCount: _availablePlugins.length,
        itemBuilder: (context, index) {
          final plugin = _availablePlugins[index];
          final isEnabled = _enabledPlugins.contains(plugin.pluginId);

          return Card(
            margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: ListTile(
              title: Text(plugin.name),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Version: ${plugin.version}'),
                  Text('Features: ${plugin.supportedFeatures.join(', ')}'),
                ],
              ),
              trailing: Switch(
                value: isEnabled,
                onChanged: (value) => _togglePlugin(plugin.pluginId, value),
              ),
              onTap: () => _showPluginDetails(plugin),
            ),
          );
        },
      ),
    );
  }

  Future<void> _loadPluginConfiguration() async {
    final enabledPlugins = await PluginConfigurationManager.getEnabledPlugins();

    setState(() {
      _enabledPlugins = enabledPlugins;
      // _availablePlugins would be loaded from plugin registry
    });
  }

  Future<void> _togglePlugin(String pluginId, bool enabled) async {
    await PluginConfigurationManager.setPluginEnabled(pluginId, enabled);
    await _loadPluginConfiguration();
  }

  void _showPluginDetails(CulicidaeLabPlugin plugin) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(plugin.name),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Plugin ID: ${plugin.pluginId}'),
            Text('Version: ${plugin.version}'),
            SizedBox(height: 8),
            Text('Supported Features:'),
            ...plugin.supportedFeatures.map((feature) => Text('• $feature')),
            SizedBox(height: 8),
            if (plugin.dependencies.isNotEmpty) ...[
              Text('Dependencies:'),
              ...plugin.dependencies.map((dep) => Text('• $dep')),
            ],
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Close'),
          ),
          if (plugin.getConfigurationWidget() != null)
            TextButton(
              onPressed: () => _showPluginConfiguration(plugin),
              child: Text('Configure'),
            ),
        ],
      ),
    );
  }

  void _showPluginConfiguration(CulicidaeLabPlugin plugin) {
    Navigator.pop(context); // Close details dialog

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Configure ${plugin.name}'),
        content: plugin.getConfigurationWidget(),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              // Save configuration
              Navigator.pop(context);
            },
            child: Text('Save'),
          ),
        ],
      ),
    );
  }
}

Testing Extensions

Plugin Testing Framework

// Testing utilities for plugins
class PluginTestingFramework {
  static Future<TestResult> testPlugin(CulicidaeLabPlugin plugin) async {
    final results = <String, bool>{};
    final errors = <String, String>{};

    try {
      // Test initialization
      await plugin.initialize(MockPluginContext());
      results['initialization'] = true;
    } catch (e) {
      results['initialization'] = false;
      errors['initialization'] = e.toString();
    }

    try {
      // Test data processing
      final testInput = PluginInput(
        operation: 'test',
        data: {'test_key': 'test_value'},
      );

      final result = await plugin.processData(testInput);
      results['data_processing'] = result.success;

      if (!result.success) {
        errors['data_processing'] = result.errorMessage ?? 'Unknown error';
      }
    } catch (e) {
      results['data_processing'] = false;
      errors['data_processing'] = e.toString();
    }

    try {
      // Test cleanup
      await plugin.dispose();
      results['cleanup'] = true;
    } catch (e) {
      results['cleanup'] = false;
      errors['cleanup'] = e.toString();
    }

    return TestResult(
      pluginId: plugin.pluginId,
      testResults: results,
      errors: errors,
      overallSuccess: !results.values.contains(false),
    );
  }

  static Future<List<TestResult>> testAllPlugins(List<CulicidaeLabPlugin> plugins) async {
    final results = <TestResult>[];

    for (final plugin in plugins) {
      final result = await testPlugin(plugin);
      results.add(result);
    }

    return results;
  }
}

class TestResult {
  final String pluginId;
  final Map<String, bool> testResults;
  final Map<String, String> errors;
  final bool overallSuccess;

  TestResult({
    required this.pluginId,
    required this.testResults,
    required this.errors,
    required this.overallSuccess,
  });
}

class MockPluginContext implements PluginContext {
  @override
  DatabaseService get databaseService => MockDatabaseService();

  @override
  ClassificationService get classificationService => MockClassificationService();

  @override
  UserService get userService => MockUserService();

  @override
  Map<String, dynamic> get appConfiguration => {
    'app_version': '1.0.0',
    'debug_mode': true,
  };

  @override
  PluginLogger get logger => MockPluginLogger();
}

Deployment and Distribution

Plugin Package Structure

# pubspec.yaml for a plugin
name: culicidaelab_environmental_plugin
description: Environmental data collection plugin for CulicidaeLab
version: 1.0.0

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter: ">=3.10.0"

dependencies:
  flutter:
    sdk: flutter
  culicidaelab_core: ^1.0.0  # Core app interfaces
  http: ^1.1.0
  geolocator: ^10.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.2

flutter:
  plugin:
    platforms:
      android:
        package: com.culicidaelab.environmental_plugin
        pluginClass: EnvironmentalPlugin
      ios:
        pluginClass: EnvironmentalPlugin

Plugin Registry

// Plugin registry for managing available plugins
class PluginRegistry {
  static final Map<String, PluginMetadata> _registeredPlugins = {};
  static final List<CulicidaeLabPlugin> _loadedPlugins = [];

  static void registerPlugin(PluginMetadata metadata) {
    _registeredPlugins[metadata.pluginId] = metadata;
  }

  static Future<CulicidaeLabPlugin?> loadPlugin(String pluginId) async {
    final metadata = _registeredPlugins[pluginId];
    if (metadata == null) return null;

    try {
      final plugin = await metadata.loader();
      _loadedPlugins.add(plugin);
      return plugin;
    } catch (e) {
      print('Failed to load plugin $pluginId: $e');
      return null;
    }
  }

  static List<PluginMetadata> getAvailablePlugins() {
    return _registeredPlugins.values.toList();
  }

  static List<CulicidaeLabPlugin> getLoadedPlugins() {
    return List.unmodifiable(_loadedPlugins);
  }

  static Future<void> unloadPlugin(String pluginId) async {
    final plugin = _loadedPlugins.firstWhere(
      (p) => p.pluginId == pluginId,
      orElse: () => throw ArgumentError('Plugin not loaded: $pluginId'),
    );

    await plugin.dispose();
    _loadedPlugins.remove(plugin);
  }
}

class PluginMetadata {
  final String pluginId;
  final String name;
  final String version;
  final String description;
  final String author;
  final List<String> supportedFeatures;
  final List<String> dependencies;
  final Future<CulicidaeLabPlugin> Function() loader;

  PluginMetadata({
    required this.pluginId,
    required this.name,
    required this.version,
    required this.description,
    required this.author,
    required this.supportedFeatures,
    required this.dependencies,
    required this.loader,
  });
}

// Plugin registration in main app
void registerBuiltInPlugins() {
  PluginRegistry.registerPlugin(
    PluginMetadata(
      pluginId: 'environmental_data_processor',
      name: 'Environmental Data Processor',
      version: '1.0.0',
      description: 'Collects environmental data for observations',
      author: 'CulicidaeLab Team',
      supportedFeatures: ['weather_data', 'soil_data'],
      dependencies: [],
      loader: () async => EnvironmentalDataProcessor(),
    ),
  );

  PluginRegistry.registerPlugin(
    PluginMetadata(
      pluginId: 'research_data_exporter',
      name: 'Research Data Exporter',
      version: '1.0.0',
      description: 'Exports data in research-specific formats',
      author: 'CulicidaeLab Team',
      supportedFeatures: ['data_export'],
      dependencies: [],
      loader: () async => ResearchDataExporter(),
    ),
  );
}

This comprehensive guide provides the foundation for extending the CulicidaeLab mobile application with custom functionality, models, data processors, and UI components. The modular architecture ensures that extensions can be developed, tested, and deployed independently while maintaining compatibility with the core application.