flutter_ml_helper 0.1.0 copy "flutter_ml_helper: ^0.1.0" to clipboard
flutter_ml_helper: ^0.1.0 copied to clipboard

Easy integration with TensorFlow Lite and ML Kit for Flutter applications. Supports all 6 platforms with WASM compatibility.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ml_helper/flutter_ml_helper.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;

void main() {
  runApp(const MLHelperExampleApp());
}

class MLHelperExampleApp extends StatelessWidget {
  const MLHelperExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter ML Helper Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const MLHelperExamplePage(),
    );
  }
}

class MLHelperExamplePage extends StatefulWidget {
  const MLHelperExamplePage({super.key});

  @override
  State<MLHelperExamplePage> createState() => _MLHelperExamplePageState();
}

class _MLHelperExamplePageState extends State<MLHelperExamplePage>
    with SingleTickerProviderStateMixin {
  late MLHelper _mlHelper;
  late TabController _tabController;
  String _status = 'Initializing...';
  List<MLModelInfo> _availableModels = [];
  Uint8List? _selectedImageBytes;
  img.Image? _processedImage;
  MLResult? _lastResult;
  bool _isProcessing = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 5, vsync: this);
    _initializeMLHelper();
  }

  Future<void> _initializeMLHelper() async {
    try {
      setState(() {
        _status = 'Creating ML Helper...';
      });

      _mlHelper = MLHelper(
        enableTFLite: true,
        enableMLKit: true,
        enableWASM: false, // Set to true for web
      );

      setState(() {
        _status = 'Getting available models...';
      });

      _availableModels = await _mlHelper.getAvailableModels();

      setState(() {
        _status = 'Ready! ${_availableModels.length} models available';
      });
    } catch (e) {
      setState(() {
        _status = 'Error: $e';
      });
    }
  }

  Future<void> _pickImage(ImageSource source) async {
    try {
      final ImagePicker picker = ImagePicker();
      final XFile? image = await picker.pickImage(source: source);
      if (image != null) {
        final bytes = await image.readAsBytes();
        setState(() {
          _selectedImageBytes = bytes;
          _processedImage = null;
          _lastResult = null;
        });
      }
    } catch (e) {
      _showError('Error picking image: $e');
    }
  }

  Future<void> _processImage() async {
    if (_selectedImageBytes == null) {
      _showError('Please select an image first');
      return;
    }

    setState(() {
      _isProcessing = true;
    });

    try {
      // Detect image format (for debugging)
      final format = ImageFormatDetector.detectFormat(_selectedImageBytes!);
      debugPrint('Detected image format: $format');

      // Load and process image
      final image = await _mlHelper.image.loadImageFromBytes(
        _selectedImageBytes!,
      );
      if (image != null) {
        // Preprocess for ML (you can use this tensor for TFLite inference)
        await _mlHelper.image.preprocessImageForML(
          image,
          targetSize: 224,
          normalize: true,
          convertToGrayscale: false,
        );

        setState(() {
          _processedImage = image;
          _isProcessing = false;
        });

        final formatMsg = format != null ? ' ($format)' : '';
        _showSuccess(
          'Image processed successfully! Format: ${format ?? "unknown"}$formatMsg',
        );
      } else {
        setState(() {
          _isProcessing = false;
        });
        _showError('Failed to decode image. Format detected: $format');
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      _showError('Error processing image: $e');
    }
  }

  Future<void> _testTextRecognition() async {
    if (_selectedImageBytes == null) {
      _showError('Please select an image first');
      return;
    }

    setState(() {
      _isProcessing = true;
    });

    try {
      // For ML Kit, we need InputImage format
      // This is a simplified demo - in production you'd convert properly
      final result = await _mlHelper.mlKit.runInference(
        input: _selectedImageBytes!,
        modelName: 'text_recognition',
      );

      setState(() {
        _lastResult = result;
        _isProcessing = false;
      });

      if (result.isSuccess) {
        _showSuccess('Text recognition completed!');
      } else {
        _showError('Text recognition failed: ${result.error}');
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      _showError('Error: $e');
    }
  }

  Future<void> _testFaceDetection() async {
    if (_selectedImageBytes == null) {
      _showError('Please select an image first');
      return;
    }

    setState(() {
      _isProcessing = true;
    });

    try {
      final result = await _mlHelper.mlKit.runInference(
        input: _selectedImageBytes!,
        modelName: 'face_detection',
      );

      setState(() {
        _lastResult = result;
        _isProcessing = false;
      });

      if (result.isSuccess) {
        _showSuccess(
          'Face detection completed! Found ${result.predictions.length} face(s)',
        );
      } else {
        _showError('Face detection failed: ${result.error}');
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      _showError('Error: $e');
    }
  }

  Future<void> _testImageLabeling() async {
    if (_selectedImageBytes == null) {
      _showError('Please select an image first');
      return;
    }

    setState(() {
      _isProcessing = true;
    });

    try {
      final result = await _mlHelper.mlKit.runInference(
        input: _selectedImageBytes!,
        modelName: 'image_labeling',
      );

      setState(() {
        _lastResult = result;
        _isProcessing = false;
      });

      if (result.isSuccess) {
        _showSuccess('Image labeling completed!');
      } else {
        _showError('Image labeling failed: ${result.error}');
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      _showError('Error: $e');
    }
  }

  Future<void> _testTFLite() async {
    if (_selectedImageBytes == null) {
      _showError('Please select an image first (Image Processing tab)');
      return;
    }

    setState(() {
      _isProcessing = true;
    });

    try {
      // Load model from assets first
      setState(() {
        _status = 'Loading TFLite model...';
      });

      // Copy asset to app directory
      final ByteData data = await rootBundle.load('assets/model.tflite');
      final List<int> bytes = data.buffer.asUint8List();

      final tempDir = await getTemporaryDirectory();
      final modelFile = File('${tempDir.path}/model.tflite');
      await modelFile.writeAsBytes(bytes);

      // Load the model - the model name is extracted from the filename
      // So 'model.tflite' becomes 'model' (same as PathUtils.getFileNameWithoutExtension does)
      final modelPath = modelFile.path;
      final loaded = await _mlHelper.tfLite.loadModel(modelPath);

      if (!loaded) {
        throw Exception('Failed to load TFLite model');
      }

      // Extract model name from path (same way loadModel does it)
      // 'model.tflite' -> 'model'
      final modelName = modelPath.split('/').last.split('.').first;

      // Pre-load ImageNet labels before inference (for ImageNet models)
      // This ensures labels are available when we display results
      setState(() {
        _status = 'Loading ImageNet labels...';
      });
      await ImageNetLabels.loadLabels().catchError((e) {
        debugPrint('Failed to preload ImageNet labels: $e');
        return false;
      });

      setState(() {
        _status = 'Processing image...';
      });

      // Process image for TFLite
      final image = await _mlHelper.image.loadImageFromBytes(
        _selectedImageBytes!,
      );
      if (image == null) {
        throw Exception('Failed to decode image');
      }

      // Preprocess image (resize, normalize)
      // Try '0to1' normalization first - some MobileNet models expect this
      // If results are wrong, try changing to '-1to1'
      final processedTensor = await _mlHelper.image.preprocessImageForML(
        image,
        targetSize: 224, // Common size for image classification models
        normalize: true,
        normalizeRange: '0to1', // Try '0to1' or '-1to1' based on your model
        convertToGrayscale: false,
      );

      // Run inference
      final result = await _mlHelper.tfLite.runInference(
        input: processedTensor,
        modelName: modelName,
        options: {'topK': 5}, // Get top 5 predictions
      );

      setState(() {
        _lastResult = result;
        _isProcessing = false;
        _status = 'MobileNet v3 classification completed!';
      });

      if (result.isSuccess) {
        _showSuccess(
          'MobileNet v3 classification successful! Found ${result.predictions.length} predictions',
        );
      } else {
        _showError('MobileNet v3 classification failed: ${result.error}');
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
        _status = 'Error: $e';
      });
      _showError('TFLite error: $e');
    }
  }

  Future<void> _checkPermissions() async {
    try {
      final cameraGranted = await PermissionUtils.isCameraPermissionGranted();
      final storageGranted = await PermissionUtils.isStoragePermissionGranted();

      // Check if widget is still mounted before using context
      if (!mounted) return;

      showDialog(
        context: context,
        builder: (dialogContext) => AlertDialog(
          title: const Text('Permission Status'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Camera: ${cameraGranted ? "Granted" : "Not Granted"}'),
              Text('Storage: ${storageGranted ? "Granted" : "Not Granted"}'),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(dialogContext),
              child: const Text('OK'),
            ),
            if (!cameraGranted || !storageGranted)
              TextButton(
                onPressed: () async {
                  // Capture navigator before async operation
                  final navigator = Navigator.of(dialogContext);
                  await PermissionUtils.requestAllMLPermissions();
                  // Use captured navigator to pop (avoids using BuildContext across async gap)
                  navigator.maybePop();
                  // Then check if State is still mounted before calling method that uses State's context
                  if (!mounted) return;
                  _checkPermissions();
                },
                child: const Text('Request'),
              ),
          ],
        ),
      );
    } catch (e) {
      _showError('Error checking permissions: $e');
    }
  }

  void _showError(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), backgroundColor: Colors.red),
    );
  }

  void _showSuccess(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), backgroundColor: Colors.green),
    );
  }

  @override
  void dispose() {
    _tabController.dispose();
    _mlHelper.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter ML Helper Demo'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabs: const [
            Tab(text: 'Overview'),
            Tab(text: 'Image Processing'),
            Tab(text: 'ML Kit'),
            Tab(text: 'TFLite'),
            Tab(text: 'Permissions'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildOverviewTab(),
          _buildImageProcessingTab(),
          _buildMLKitTab(),
          _buildTFLiteTab(),
          _buildPermissionsTab(),
        ],
      ),
    );
  }

  Widget _buildOverviewTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Status: $_status',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Text('TFLite Available: ${_mlHelper.isTFLiteAvailable}'),
                  Text('ML Kit Available: ${_mlHelper.isMLKitAvailable}'),
                  Text('WASM Enabled: ${_mlHelper.isWASMEnabled}'),
                  const SizedBox(height: 8),
                  Text(
                    'Platform Info:',
                    style: Theme.of(context).textTheme.titleSmall,
                  ),
                  ..._mlHelper.platformInfo.entries.map(
                    (e) => Text('${e.key}: ${e.value}'),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Text(
            'Available Models (${_availableModels.length})',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 8),
          ..._availableModels.map(
            (model) => Card(
              margin: const EdgeInsets.only(bottom: 8),
              child: ListTile(
                title: Text(model.name),
                subtitle: Text(
                  '${model.backend} - ${model.sizeDescription}\n'
                  'Input: ${model.inputShape.join("x")} → Output: ${model.outputShape.join("x")}',
                ),
                trailing: Icon(
                  model.isLoaded
                      ? Icons.check_circle
                      : Icons.radio_button_unchecked,
                  color: model.isLoaded ? Colors.green : Colors.grey,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildImageProcessingTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  const Text(
                    'Image Selection',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 16),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      ElevatedButton.icon(
                        onPressed: () => _pickImage(ImageSource.camera),
                        icon: const Icon(Icons.camera_alt),
                        label: const Text('Camera'),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _pickImage(ImageSource.gallery),
                        icon: const Icon(Icons.photo_library),
                        label: const Text('Gallery'),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          if (_selectedImageBytes != null) ...[
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    const Text(
                      'Selected Image',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 16),
                    Image.memory(
                      _selectedImageBytes!,
                      height: 200,
                      fit: BoxFit.contain,
                    ),
                    const SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: _isProcessing ? null : _processImage,
                      child: _isProcessing
                          ? const CircularProgressIndicator()
                          : const Text('Process Image'),
                    ),
                  ],
                ),
              ),
            ),
            if (_processedImage != null) ...[
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    children: [
                      const Text(
                        'Image Info',
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 16),
                      ..._mlHelper.image
                          .getImageInfo(_processedImage)
                          .entries
                          .map(
                            (e) => Padding(
                              padding: const EdgeInsets.symmetric(vertical: 4),
                              child: Row(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceBetween,
                                children: [
                                  Text(e.key.toString()),
                                  Text(e.value.toString()),
                                ],
                              ),
                            ),
                          ),
                    ],
                  ),
                ),
              ),
            ],
          ] else
            Card(
              child: Padding(
                padding: const EdgeInsets.all(32.0),
                child: Center(
                  child: Column(
                    children: [
                      Icon(Icons.image, size: 64, color: Colors.grey[400]),
                      const SizedBox(height: 16),
                      Text(
                        'No image selected',
                        style: TextStyle(color: Colors.grey[600]),
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildMLKitTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          if (_selectedImageBytes == null)
            Card(
              child: Padding(
                padding: const EdgeInsets.all(32.0),
                child: Center(
                  child: Column(
                    children: [
                      Icon(Icons.image, size: 64, color: Colors.grey[400]),
                      const SizedBox(height: 16),
                      Text(
                        'Please select an image first (Image Processing tab)',
                        style: TextStyle(color: Colors.grey[600]),
                        textAlign: TextAlign.center,
                      ),
                    ],
                  ),
                ),
              ),
            )
          else ...[
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    Image.memory(
                      _selectedImageBytes!,
                      height: 200,
                      fit: BoxFit.contain,
                    ),
                    const SizedBox(height: 16),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      alignment: WrapAlignment.center,
                      children: [
                        ElevatedButton.icon(
                          onPressed: _isProcessing
                              ? null
                              : _testTextRecognition,
                          icon: const Icon(Icons.text_fields),
                          label: const Text('Text Recognition'),
                        ),
                        ElevatedButton.icon(
                          onPressed: _isProcessing ? null : _testFaceDetection,
                          icon: const Icon(Icons.face),
                          label: const Text('Face Detection'),
                        ),
                        ElevatedButton.icon(
                          onPressed: _isProcessing ? null : _testImageLabeling,
                          icon: const Icon(Icons.label),
                          label: const Text('Image Labeling'),
                        ),
                      ],
                    ),
                    if (_isProcessing) ...[
                      const SizedBox(height: 16),
                      const CircularProgressIndicator(),
                    ],
                  ],
                ),
              ),
            ),
            if (_lastResult != null) ...[
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Results',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      const SizedBox(height: 8),
                      Text('Backend: ${_lastResult!.backend}'),
                      Text('Success: ${_lastResult!.isSuccess}'),
                      if (_lastResult!.isSuccess) ...[
                        Text(
                          'Inference Time: ${_lastResult!.inferenceTime.toStringAsFixed(2)}ms',
                        ),
                        Text('Top Prediction: ${_lastResult!.topPrediction}'),
                        Text(
                          'Top Confidence: ${(_lastResult!.topConfidence * 100).toStringAsFixed(1)}%',
                        ),
                        const SizedBox(height: 8),
                        const Text(
                          'All Predictions:',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        ..._lastResult!.predictions.asMap().entries.map(
                          (e) => Text(
                            '${e.key + 1}. ${e.value} (${(_lastResult!.confidences[e.key] * 100).toStringAsFixed(1)}%)',
                          ),
                        ),
                      ] else
                        Text(
                          'Error: ${_lastResult!.error}',
                          style: const TextStyle(color: Colors.red),
                        ),
                    ],
                  ),
                ),
              ),
            ],
          ],
        ],
      ),
    );
  }

  Widget _buildTFLiteTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Icon(
                        Icons.psychology,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          'MobileNet v3 - Image Classification',
                          style: Theme.of(context).textTheme.titleLarge,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Available: ${_mlHelper.isTFLiteAvailable}',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  const SizedBox(height: 16),

                  // Model Info Card
                  Card(
                    color: Colors.blue[50],
                    child: Padding(
                      padding: const EdgeInsets.all(12.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Icon(
                                Icons.info_outline,
                                size: 20,
                                color: Colors.blue[700],
                              ),
                              const SizedBox(width: 8),
                              const Text(
                                'About MobileNet v3',
                                style: TextStyle(fontWeight: FontWeight.bold),
                              ),
                            ],
                          ),
                          const SizedBox(height: 8),
                          const Text(
                            'MobileNet v3 is a lightweight neural network designed for mobile devices. It can classify images into 1000 different categories (ImageNet classes) including objects, animals, and scenes.',
                            style: TextStyle(fontSize: 12),
                          ),
                        ],
                      ),
                    ),
                  ),

                  const SizedBox(height: 16),

                  if (_selectedImageBytes == null)
                    Card(
                      color: Colors.orange[50],
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Column(
                          children: [
                            Icon(
                              Icons.image_not_supported,
                              size: 48,
                              color: Colors.orange[700],
                            ),
                            const SizedBox(height: 8),
                            Text(
                              'No Image Selected',
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                                color: Colors.orange[900],
                              ),
                            ),
                            const SizedBox(height: 4),
                            Text(
                              'Go to "Image Processing" tab to select an image',
                              style: TextStyle(color: Colors.orange[800]),
                              textAlign: TextAlign.center,
                            ),
                          ],
                        ),
                      ),
                    )
                  else ...[
                    Card(
                      elevation: 2,
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Column(
                          children: [
                            const Text(
                              'Selected Image',
                              style: TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(height: 12),
                            Container(
                              decoration: BoxDecoration(
                                border: Border.all(color: Colors.grey[300]!),
                                borderRadius: BorderRadius.circular(8),
                              ),
                              child: ClipRRect(
                                borderRadius: BorderRadius.circular(8),
                                child: Image.memory(
                                  _selectedImageBytes!,
                                  height: 200,
                                  fit: BoxFit.contain,
                                ),
                              ),
                            ),
                            const SizedBox(height: 16),
                            ElevatedButton.icon(
                              onPressed: _isProcessing ? null : _testTFLite,
                              icon: _isProcessing
                                  ? const SizedBox(
                                      width: 20,
                                      height: 20,
                                      child: CircularProgressIndicator(
                                        strokeWidth: 2,
                                        valueColor:
                                            AlwaysStoppedAnimation<Color>(
                                              Colors.white,
                                            ),
                                      ),
                                    )
                                  : const Icon(Icons.play_arrow),
                              label: Text(
                                _isProcessing
                                    ? 'Processing...'
                                    : 'Run Classification',
                                style: const TextStyle(fontSize: 16),
                              ),
                              style: ElevatedButton.styleFrom(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 32,
                                  vertical: 16,
                                ),
                                backgroundColor: Theme.of(
                                  context,
                                ).colorScheme.primary,
                                foregroundColor: Colors.white,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],

                  const SizedBox(height: 16),

                  // Model Specifications
                  Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Icon(
                                Icons.settings,
                                size: 20,
                                color: Colors.grey[700],
                              ),
                              const SizedBox(width: 8),
                              const Text(
                                'Model Specifications',
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: 16,
                                ),
                              ),
                            ],
                          ),
                          const SizedBox(height: 12),
                          _buildSpecRow('Model Type', 'MobileNet v3'),
                          _buildSpecRow('Model Size', '15 MB'),
                          _buildSpecRow('Input Size', '224 × 224 pixels'),
                          _buildSpecRow('Input Format', 'RGB (Normalized 0-1)'),
                          _buildSpecRow('Output Classes', '1000 (ImageNet)'),
                          _buildSpecRow('Platform', 'TensorFlow Lite'),
                        ],
                      ),
                    ),
                  ),

                  // Results
                  if (_lastResult != null &&
                      _lastResult!.backend == 'TFLite') ...[
                    const SizedBox(height: 16),
                    Card(
                      color: _lastResult!.isSuccess
                          ? Colors.green[50]
                          : Colors.red[50],
                      elevation: 3,
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Row(
                              children: [
                                Icon(
                                  _lastResult!.isSuccess
                                      ? Icons.check_circle
                                      : Icons.error,
                                  color: _lastResult!.isSuccess
                                      ? Colors.green[700]
                                      : Colors.red[700],
                                ),
                                const SizedBox(width: 8),
                                Text(
                                  'Classification Results',
                                  style: Theme.of(context).textTheme.titleMedium
                                      ?.copyWith(fontWeight: FontWeight.bold),
                                ),
                              ],
                            ),
                            const SizedBox(height: 12),
                            if (_lastResult!.isSuccess) ...[
                              _buildResultRow('Backend', _lastResult!.backend),
                              _buildResultRow(
                                'Inference Time',
                                '${_lastResult!.inferenceTime.toStringAsFixed(2)} ms',
                              ),
                              const Divider(),
                              Container(
                                padding: const EdgeInsets.all(12),
                                decoration: BoxDecoration(
                                  color: Colors.blue[100],
                                  borderRadius: BorderRadius.circular(8),
                                ),
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      'Top Prediction',
                                      style: TextStyle(
                                        fontWeight: FontWeight.bold,
                                        color: Colors.blue[900],
                                      ),
                                    ),
                                    const SizedBox(height: 4),
                                    Text(
                                      _getClassName(_lastResult!.topPrediction),
                                      style: TextStyle(
                                        fontSize: 18,
                                        fontWeight: FontWeight.bold,
                                        color: Colors.blue[900],
                                      ),
                                    ),
                                    Text(
                                      '${(_lastResult!.topConfidence * 100).toStringAsFixed(2)}% confidence',
                                      style: TextStyle(
                                        fontSize: 14,
                                        color: Colors.blue[700],
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: 12),
                              const Text(
                                'Top 5 Predictions',
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: 14,
                                ),
                              ),
                              const SizedBox(height: 8),
                              ..._lastResult!.predictions
                                  .take(5)
                                  .toList()
                                  .asMap()
                                  .entries
                                  .map(
                                    (e) => Padding(
                                      padding: const EdgeInsets.symmetric(
                                        vertical: 4,
                                      ),
                                      child: Row(
                                        children: [
                                          Container(
                                            width: 24,
                                            height: 24,
                                            decoration: BoxDecoration(
                                              color: e.key == 0
                                                  ? Colors.green[100]
                                                  : Colors.grey[200],
                                              borderRadius:
                                                  BorderRadius.circular(12),
                                            ),
                                            child: Center(
                                              child: Text(
                                                '${e.key + 1}',
                                                style: TextStyle(
                                                  fontWeight: FontWeight.bold,
                                                  fontSize: 12,
                                                  color: e.key == 0
                                                      ? Colors.green[900]
                                                      : Colors.grey[700],
                                                ),
                                              ),
                                            ),
                                          ),
                                          const SizedBox(width: 12),
                                          Expanded(
                                            child: Text(
                                              _getClassName(e.value),
                                              style: const TextStyle(
                                                fontSize: 14,
                                              ),
                                            ),
                                          ),
                                          Container(
                                            padding: const EdgeInsets.symmetric(
                                              horizontal: 8,
                                              vertical: 4,
                                            ),
                                            decoration: BoxDecoration(
                                              color: Colors.blue[100],
                                              borderRadius:
                                                  BorderRadius.circular(4),
                                            ),
                                            child: Text(
                                              '${(_lastResult!.confidences[e.key] * 100).toStringAsFixed(1)}%',
                                              style: TextStyle(
                                                fontWeight: FontWeight.bold,
                                                fontSize: 12,
                                                color: Colors.blue[900],
                                              ),
                                            ),
                                          ),
                                        ],
                                      ),
                                    ),
                                  ),
                            ] else
                              Text(
                                'Error: ${_lastResult!.error}',
                                style: const TextStyle(color: Colors.red),
                              ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSpecRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: TextStyle(color: Colors.grey[700])),
          Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
        ],
      ),
    );
  }

  Widget _buildResultRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: TextStyle(color: Colors.grey[700])),
          Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
        ],
      ),
    );
  }

  /// Gets a human-readable class name for the class index
  String _getClassName(dynamic classIndex) {
    if (classIndex is! int) {
      return 'Class $classIndex';
    }

    // Always check ImageNetLabels first (it will use cached labels if available)
    // This ensures we get the latest labels even if they loaded after inference
    final label = ImageNetLabels.getDisplayName(classIndex);
    if (!label.startsWith('Class ')) {
      return label; // Found a real label
    }

    // Try to get from result metadata as fallback
    if (_lastResult != null &&
        _lastResult!.metadata.containsKey('classLabels')) {
      final labels = _lastResult!.metadata['classLabels'] as Map<int, String>?;
      if (labels != null && labels.containsKey(classIndex)) {
        final metadataLabel = labels[classIndex]!;
        if (!metadataLabel.startsWith('Class ')) {
          return metadataLabel;
        }
      }
    }

    // Final fallback: return the generic class name
    return label; // This will be "Class X" if no label found
  }

  Widget _buildPermissionsTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Permission Management',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  const SizedBox(height: 16),
                  ElevatedButton.icon(
                    onPressed: _checkPermissions,
                    icon: const Icon(Icons.security),
                    label: const Text('Check Permissions'),
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Available Permission Methods:',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  _buildPermissionMethod(
                    'Camera',
                    'PermissionUtils.isCameraPermissionGranted()',
                  ),
                  _buildPermissionMethod(
                    'Storage',
                    'PermissionUtils.isStoragePermissionGranted()',
                  ),
                  _buildPermissionMethod(
                    'Microphone',
                    'PermissionUtils.isMicrophonePermissionGranted()',
                  ),
                  _buildPermissionMethod(
                    'Location',
                    'PermissionUtils.isLocationPermissionGranted()',
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Request Permissions:',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  _buildPermissionMethod(
                    'All ML Permissions',
                    'PermissionUtils.requestAllMLPermissions()',
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildPermissionMethod(String name, String method) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Icon(Icons.check_circle_outline, size: 16),
          const SizedBox(width: 8),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name, style: const TextStyle(fontWeight: FontWeight.w500)),
                Text(
                  method,
                  style: TextStyle(
                    fontFamily: 'monospace',
                    fontSize: 12,
                    color: Colors.grey[600],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
229
downloads

Publisher

verified publisherbechattaoui.dev

Weekly Downloads

Easy integration with TensorFlow Lite and ML Kit for Flutter applications. Supports all 6 platforms with WASM compatibility.

Repository (GitHub)
View/report issues

Topics

#flutter #machine-learning #tensorflow-lite #ml-kit #cross-platform

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, google_ml_kit, http, image, path, path_provider, permission_handler, tflite_flutter

More

Packages that depend on flutter_ml_helper

Packages that implement flutter_ml_helper