DEV Community

Ge Ji
Ge Ji

Posted on

Flutter Lesson 18: Device Feature Integration in Flutter

In mobile app development, integrating native device features (such as cameras, photo galleries, and location services) is crucial for enhancing user experience. Flutter provides a rich ecosystem of third-party plugins that make implementing these features straightforward. In this lesson, we'll explore the core concepts of device feature integration, including permission management, camera/gallery access, location retrieval, and demonstrate practical implementation through a comprehensive example.

1. Permission Management: The permission_handler Plugin

Before accessing device features, you must obtain user authorization. The permission_handler plugin is the most commonly used permission management tool in Flutter, supporting nearly all permission types across Android and iOS platforms.

1.1 Installation and Configuration

Step 1: Add Dependency
Add the plugin to your pubspec.yaml:

dependencies:
  permission_handler: ^12.0.1  # Use the latest version
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install.

Step 2: Platform Permission Configuration
Permissions must be declared in native configuration files; otherwise, feature access will fail.

  • Android Configuration Edit android/app/src/main/AndroidManifest.xml and add required permissions:
<!-- Camera permission -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Storage permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Enter fullscreen mode Exit fullscreen mode
  • iOS Configuration Edit ios/Runner/Info.plist and add permission descriptions (users will see these):
<!-- Camera permission description -->
<key>NSCameraUsageDescription</key>
<string>Camera access is needed to take photos</string>
<!-- Photo library permission description -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is needed to select images</string>
<!-- Location permission description -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access is needed to get your current location</string>
Enter fullscreen mode Exit fullscreen mode

1.2 Core Permission Methods

permission_handler provides a simple API for checking and requesting permissions:

import 'package:permission_handler/permission_handler.dart';

// Check permission status
Future<bool> checkPermission(Permission permission) async {
  PermissionStatus status = await permission.status;
  return status.isGranted; // Returns true if permission is granted
}

// Request permission
Future<bool> requestPermission(Permission permission) async {
  PermissionStatus status = await permission.request();
  return status.isGranted;
}

// Open app settings (when user denies permission with "Don't ask again")
Future<void> openAppSet() async {
  await openAppSettings();
}
Enter fullscreen mode Exit fullscreen mode

1.3 Common Permission Constants

The Permission class contains all supported permissions. Common ones include:

  • Permission.camera: Camera access
  • Permission.photos / Permission.storage: Photo library/storage access
  • Permission.locationWhenInUse: Location access while app is in use
  • Permission.locationAlways: Always allow location access

2. Accessing Camera/Gallery: The image_picker Plugin

image_picker is the officially recommended media selection plugin for Flutter, supporting photo/video capture from camera or selection from gallery.

2.1 Installation and Configuration

Step 1: Add Dependency

dependencies:
  image_picker: ^1.1.2  # Use the latest version
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install.

Step 2: Additional Platform Configuration (for specific scenarios)

  • iOS Video Selection: For video selection, add NSCameraUsageDescription to Info.plist (same as camera permission)
  • Android 10+ Storage: To save images to public directories, add to AndroidManifest.xml:
<application ...>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <!-- For Android 10+ -->
  <application ... android:requestLegacyExternalStorage="true">
</application>
Enter fullscreen mode Exit fullscreen mode

2.2 Core Feature Implementation

(1) Taking Photos with Camera

import 'package:image_picker/image_picker.dart';

// Get image from camera
Future<XFile?> takePhoto() async {
  final ImagePicker picker = ImagePicker();
  // Launch camera, returns XFile (contains image path and info)
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera, // Source is camera
    imageQuality: 80, // Image quality (0-100)
    maxWidth: 1080, // Maximum width
  );
  return photo;
}
Enter fullscreen mode Exit fullscreen mode

(2) Selecting Images from Gallery

// Select image from gallery
Future<XFile?> pickImageFromGallery() async {
  final ImagePicker picker = ImagePicker();
  final XFile? image = await picker.pickImage(
    source: ImageSource.gallery, // Source is gallery
    imageQuality: 80,
  );
  return image;
}
Enter fullscreen mode Exit fullscreen mode

(3) Displaying Selected Images
After obtaining an XFile, display the image using Image.file:

XFile? selectedImage; // Stores selected image

// Update image after taking photo
void _onTakePhoto() async {
  XFile? photo = await takePhoto();
  if (photo != null) {
    setState(() {
      selectedImage = photo;
    });
  }
}

// Display image in UI
Widget buildImagePreview() {
  if (selectedImage == null) {
    return Text("No image selected");
  }
  return Image.file(
    File(selectedImage!.path),
    width: 300,
    height: 300,
    fit: BoxFit.cover,
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Location Services: The geolocator Plugin

geolocator provides cross-platform location services, supporting retrieval of coordinates, altitude, speed, and location change monitoring.

3.1 Installation and Configuration

Step 1: Add Dependency

dependencies:
  geolocator: ^14.0.2  # Use the latest version
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install.

Step 2: Platform Permission Configuration

Location services require additional permissions (already partially added in the permission_handler section):

  • Android: In addition to ACCESS_FINE_LOCATION (precise location) and ACCESS_COARSE_LOCATION (approximate location), for background location:
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
Enter fullscreen mode Exit fullscreen mode

iOS:
For background location, add to Info.plist:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Continuous location access is needed for background tracking</string>
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
</array>
Enter fullscreen mode Exit fullscreen mode

3.2 Core Feature Implementation

(1) Getting Current Location

import 'package:geolocator/geolocator.dart';

// Get current coordinates
Future<Position?> getCurrentLocation() async {
  // Check if location services are enabled
  bool isLocationEnabled = await Geolocator.isLocationServiceEnabled();
  if (!isLocationEnabled) {
    // Prompt user to enable location services
    return null;
  }

  // Check location permissions
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    // Request permission
    permission = await Geolocator.requestPermission();
    if (permission != LocationPermission.whileInUse && 
        permission != LocationPermission.always) {
      // Permission denied
      return null;
    }
  }

  // Get current location (timeout after 10 seconds)
  try {
    Position position = await Geolocator.getCurrentPosition(
      desiredAccuracy: LocationAccuracy.high, // High accuracy
      timeLimit: Duration(seconds: 10),
    );
    return position; // Contains latitude and longitude
  } catch (e) {
    print("Failed to get location: $e");
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

(2) Listening for Location Changes

// Listen for location changes (updates every 10 meters or 30 seconds)
StreamSubscription<Position>? positionStream;

void startListeningLocation() {
  positionStream = Geolocator.getPositionStream(
    locationSettings: LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // Update only when moving more than 10 meters
      intervalDuration: Duration(seconds: 30), // Update at least every 30 seconds
    ),
  ).listen((Position position) {
    print("Current location: ${position.latitude}, ${position.longitude}");
  });
}

// Stop listening
void stopListeningLocation() {
  if (positionStream != null) {
    positionStream!.cancel();
    positionStream = null;
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Comprehensive Example: Photo Upload with Location

Let's implement a complete page with these features:

  1. Check and request camera and location permissions
  2. Take photos or select from gallery
  3. Get current location coordinates
  4. Simulate image upload (show upload status)

Complete Code Implementation

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';

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

  @override
  State<DeviceDemoPage> createState() => _DeviceDemoPageState();
}

class _DeviceDemoPageState extends State<DeviceDemoPage> {
  XFile? _selectedImage; // Selected image
  Position? _currentPosition; // Current location
  bool _isUploading = false; // Upload in progress

  // Check and request permission
  Future<bool> _checkAndRequestPermission(Permission permission) async {
    bool isGranted = await checkPermission(permission);
    if (!isGranted) {
      isGranted = await requestPermission(permission);
    }
    return isGranted;
  }

  // Take photo with camera
  void _takePhoto() async {
    bool hasCameraPermission = await _checkAndRequestPermission(Permission.camera);
    if (!hasCameraPermission) {
      _showSnackBar("Please grant camera permission");
      return;
    }

    XFile? photo = await ImagePicker().pickImage(
      source: ImageSource.camera,
      imageQuality: 80,
    );
    if (photo != null) {
      setState(() => _selectedImage = photo);
    }
  }

  // Select from gallery
  void _pickFromGallery() async {
    bool hasStoragePermission = await _checkAndRequestPermission(Permission.photos);
    if (!hasStoragePermission) {
      _showSnackBar("Please grant gallery permission");
      return;
    }

    XFile? image = await ImagePicker().pickImage(
      source: ImageSource.gallery,
      imageQuality: 80,
    );
    if (image != null) {
      setState(() => _selectedImage = image);
    }
  }

  // Get current location
  void _getCurrentLocation() async {
    bool hasLocationPermission = await _checkAndRequestPermission(Permission.locationWhenInUse);
    if (!hasLocationPermission) {
      _showSnackBar("Please grant location permission");
      return;
    }

    Position? position = await getCurrentLocation();
    if (position != null) {
      setState(() => _currentPosition = position);
      _showSnackBar("Location information obtained");
    } else {
      _showSnackBar("Failed to get location. Please check location services.");
    }
  }

  // Simulate image upload
  void _uploadImage() async {
    if (_selectedImage == null) {
      _showSnackBar("Please select an image first");
      return;
    }

    setState(() => _isUploading = true);
    // Simulate network request (complete after 2 seconds)
    await Future.delayed(Duration(seconds: 2));
    setState(() => _isUploading = false);
    _showSnackBar("Image uploaded successfully");
  }

  // Show notification
  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Device Features Demo")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            // Image preview
            _selectedImage == null
                ? const Text("No image selected", style: TextStyle(fontSize: 16))
                : Image.file(
                    File(_selectedImage!.path),
                    width: 300,
                    height: 300,
                    fit: BoxFit.cover,
                  ),
            const SizedBox(height: 20),

            // Action buttons
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton.icon(
                  onPressed: _takePhoto,
                  icon: const Icon(Icons.camera_alt),
                  label: const Text("Take Photo"),
                ),
                const SizedBox(width: 16),
                ElevatedButton.icon(
                  onPressed: _pickFromGallery,
                  icon: const Icon(Icons.photo_library),
                  label: const Text("Gallery"),
                ),
              ],
            ),
            const SizedBox(height: 16),

            // Location information
            ElevatedButton.icon(
              onPressed: _getCurrentLocation,
              icon: const Icon(Icons.location_on),
              label: const Text("Get Current Location"),
            ),
            if (_currentPosition != null)
              Padding(
                padding: const EdgeInsets.all(16),
                child: Text(
                  "Current Location:\nLatitude: ${_currentPosition!.latitude}\nLongitude: ${_currentPosition!.longitude}",
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 16),
                ),
              ),
            const SizedBox(height: 16),

            // Upload button
            _isUploading
                ? const CircularProgressIndicator()
                : ElevatedButton.icon(
                    onPressed: _uploadImage,
                    icon: const Icon(Icons.upload),
                    label: const Text("Upload Image"),
                  ),
          ],
        ),
      ),
    );
  }
}

// Utility methods for permission checking and requesting (can be extracted to a utility class)
Future<bool> checkPermission(Permission permission) async {
  return (await permission.status).isGranted;
}

Future<bool> requestPermission(Permission permission) async {
  return (await permission.request()).isGranted;
}

// Location utility methods (can be extracted to a utility class)
Future<Position?> getCurrentLocation() async {
  // Check if location services are enabled
  if (!await Geolocator.isLocationServiceEnabled()) {
    return null;
  }

  // Check permissions
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission != LocationPermission.whileInUse &&
        permission != LocationPermission.always) {
      return null;
    }
  }

  // Get location
  try {
    return await Geolocator.getCurrentPosition(
      desiredAccuracy: LocationAccuracy.high,
      timeLimit: const Duration(seconds: 10),
    );
  } catch (e) {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Code Explanation

  1. Permission Management: Uses encapsulated checkPermission and requestPermission methods to handle permission logic uniformly, ensuring authorization before accessing device features.
  2. Image Handling: Implements camera capture and gallery selection using image_picker, with Image.file for displaying selected images.
  3. Location Features: Retrieves coordinates using geolocator, including location service checks and permission handling for reliable location functionality.
  4. User Experience: Adds loading states (progress indicator during upload) and notification messages (SnackBar) to enhance interaction friendliness.

5. Extended Knowledge: Other Useful Device Feature Plugins

Beyond the features covered in this lesson, these plugins are commonly used for device integration:

  • url_launcher: Launch system browser, make phone calls, send emails, etc.
  • share_plus: Share text and images to other applications.
  • flutter_local_notifications: Local notification functionality.
  • connectivity_plus: Network connection status monitoring.
  • package_info_plus: Retrieve app version, name, and other information.

Top comments (0)