Flutter'da Kamera ve Medya: image_picker, camera ve Video Kaydı

Fotoğraf ve video işlevleri modern uygulamaların vazgeçilmez parçalarıdır. Flutter'da medya erişimi için üç ana paket öne çıkar: image_picker (galeri/kamera seçici), camera (özel kamera arayüzü) ve photo_manager (tam galeri erişimi). Bu yazıda her birini detaylıca inceleyeceğiz.

image_picker: Hızlı Galeri ve Kamera Erişimi

// pubspec:
// image_picker: ^1.1.2
// flutter_image_compress: ^2.3.0

// iOS — Info.plist
// NSPhotoLibraryUsageDescription
// NSCameraUsageDescription
// NSMicrophoneUsageDescription

// Android — AndroidManifest.xml
// <uses-permission android:name="android.permission.CAMERA" />

class MediaPickerService {
  final ImagePicker _picker = ImagePicker();

  // Tek fotoğraf seç
  Future<File?> pickImage({
    ImageSource source = ImageSource.gallery,
    int maxWidth = 1920,
    int maxHeight = 1080,
    int quality = 85,
  }) async {
    try {
      final xFile = await _picker.pickImage(
        source: source,
        maxWidth: maxWidth.toDouble(),
        maxHeight: maxHeight.toDouble(),
        imageQuality: quality,
      );
      if (xFile == null) return null;
      return File(xFile.path);
    } on PlatformException catch (e) {
      debugPrint('Medya seçimi başarısız: ${e.code}');
      return null;
    }
  }

  // Çoklu fotoğraf seç
  Future<List<File>> pickMultipleImages({int limit = 10}) async {
    final xFiles = await _picker.pickMultiImage(limit: limit);
    return xFiles.map((f) => File(f.path)).toList();
  }

  // Video seç veya kaydet
  Future<File?> pickVideo({
    ImageSource source = ImageSource.gallery,
    Duration? maxDuration,
  }) async {
    final xFile = await _picker.pickVideo(
      source: source,
      maxDuration: maxDuration ?? const Duration(minutes: 5),
    );
    return xFile != null ? File(xFile.path) : null;
  }

  // Görüntüyü sıkıştır
  Future<File> compress(File file, {int quality = 70}) async {
    final result = await FlutterImageCompress.compressAndGetFile(
      file.absolute.path,
      '${file.parent.path}/compressed_${file.uri.pathSegments.last}',
      quality: quality,
      minWidth: 1024,
      minHeight: 768,
    );
    return result != null ? File(result.path) : file;
  }
}

// Picker modal — seçenek sun
Future<File?> showMediaSourceSheet(BuildContext context) {
  return showModalBottomSheet<File?>(
    context: context,
    builder: (_) => SafeArea(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.camera_alt),
            title: const Text('Kamerayı Aç'),
            onTap: () async {
              Navigator.pop(context,
                await MediaPickerService().pickImage(source: ImageSource.camera));
            },
          ),
          ListTile(
            leading: const Icon(Icons.photo_library),
            title: const Text('Galeriden Seç'),
            onTap: () async {
              Navigator.pop(context,
                await MediaPickerService().pickImage(source: ImageSource.gallery));
            },
          ),
        ],
      ),
    ),
  );
}

camera Paketi: Özel Kamera Arayüzü

// pubspec: camera: ^0.11.0

class CustomCameraPage extends StatefulWidget {
  const CustomCameraPage({super.key});
  @override State<CustomCameraPage> createState() => _CustomCameraPageState();
}

class _CustomCameraPageState extends State<CustomCameraPage>
    with WidgetsBindingObserver {
  CameraController? _controller;
  List<CameraDescription> _cameras = [];
  int _cameraIndex = 0;
  bool _isRecording = false;
  FlashMode _flashMode = FlashMode.off;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initCamera();
  }

  Future<void> _initCamera() async {
    _cameras = await availableCameras();
    if (_cameras.isEmpty) return;
    await _setupController(_cameras[_cameraIndex]);
  }

  Future<void> _setupController(CameraDescription camera) async {
    _controller?.dispose();
    final controller = CameraController(
      camera,
      ResolutionPreset.high,
      enableAudio: true,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );
    await controller.initialize();
    if (!mounted) return;
    await controller.setFlashMode(_flashMode);
    setState(() => _controller = controller);
  }

  Future<void> _takePicture() async {
    if (_controller == null || !_controller!.value.isInitialized) return;
    try {
      final xFile = await _controller!.takePicture();
      if (!mounted) return;
      Navigator.pop(context, File(xFile.path));
    } on CameraException catch (e) {
      debugPrint('Fotoğraf çekme hatası: ${e.description}');
    }
  }

  Future<void> _toggleRecording() async {
    if (_controller == null) return;
    if (_isRecording) {
      final xFile = await _controller!.stopVideoRecording();
      setState(() => _isRecording = false);
      if (!mounted) return;
      Navigator.pop(context, File(xFile.path));
    } else {
      await _controller!.startVideoRecording();
      setState(() => _isRecording = true);
    }
  }

  void _switchCamera() {
    _cameraIndex = (_cameraIndex + 1) % _cameras.length;
    _setupController(_cameras[_cameraIndex]);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (_controller == null || !_controller!.value.isInitialized) return;
    if (state == AppLifecycleState.inactive) {
      _controller!.dispose();
    } else if (state == AppLifecycleState.resumed) {
      _setupController(_cameras[_cameraIndex]);
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_controller == null || !_controller!.value.isInitialized) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        fit: StackFit.expand,
        children: [
          // Önizleme
          CameraPreview(_controller!),

          // Üst kontroller
          SafeArea(
            child: Align(
              alignment: Alignment.topRight,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                    icon: Icon(
                      _flashMode == FlashMode.off ? Icons.flash_off : Icons.flash_on,
                      color: Colors.white,
                    ),
                    onPressed: () async {
                      final next = _flashMode == FlashMode.off
                          ? FlashMode.torch : FlashMode.off;
                      await _controller!.setFlashMode(next);
                      setState(() => _flashMode = next);
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.flip_camera_ios, color: Colors.white),
                    onPressed: _switchCamera,
                  ),
                ],
              ),
            ),
          ),

          // Alt kontroller
          Positioned(
            bottom: 32,
            left: 0, right: 0,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                // Galeri önizleme butonu
                const SizedBox(width: 56),
                // Çekim butonu
                GestureDetector(
                  onTap: _takePicture,
                  onLongPress: _toggleRecording,
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 200),
                    width: 72,
                    height: 72,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      border: Border.all(color: Colors.white, width: 4),
                      color: _isRecording ? Colors.red : Colors.white.withOpacity(0.3),
                    ),
                    child: _isRecording
                        ? const Icon(Icons.stop, color: Colors.white, size: 32)
                        : null,
                  ),
                ),
                // Kamera değiştir
                IconButton(
                  icon: const Icon(Icons.flip_camera_ios, color: Colors.white, size: 32),
                  onPressed: _switchCamera,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Görüntü Kırpma: image_cropper

// pubspec: image_cropper: ^7.0.5

Future<File?> cropImage(File imageFile) async {
  final croppedFile = await ImageCropper().cropImage(
    sourcePath: imageFile.path,
    aspectRatioPresets: [
      CropAspectRatioPreset.square,
      CropAspectRatioPreset.ratio3x2,
      CropAspectRatioPreset.original,
    ],
    uiSettings: [
      AndroidUiSettings(
        toolbarTitle: 'Kırp',
        toolbarColor: Colors.deepOrange,
        toolbarWidgetColor: Colors.white,
        initAspectRatio: CropAspectRatioPreset.square,
        lockAspectRatio: false,
        hideBottomControls: false,
      ),
      IOSUiSettings(
        title: 'Kırp',
        aspectRatioLockEnabled: false,
        resetAspectRatioEnabled: true,
      ),
    ],
  );
  return croppedFile != null ? File(croppedFile.path) : null;
}

// Tam akış: Seç → Kırp → Sıkıştır → Yükle
Future<void> uploadProfilePhoto(BuildContext context) async {
  final service = MediaPickerService();

  // 1. Seç
  final file = await service.pickImage(source: ImageSource.gallery);
  if (file == null) return;

  // 2. Kırp
  final cropped = await cropImage(file);
  if (cropped == null) return;

  // 3. Sıkıştır
  final compressed = await service.compress(cropped, quality: 75);

  // 4. Yükle
  if (!context.mounted) return;
  await context.read<ProfileCubit>().updatePhoto(compressed);
}

Medya işlemleri için izin yönetimini (iOS Info.plist ve Android manifest) baştan doğru yapın. Ağır görüntü işlemlerini compute() ile ayrı isolate'e taşıyın. Galeri erişiminde iOS 14+ ile gelen sınırlı fotoğraf erişimine dikkat edin ve photo_manager paketini tam galeri erişimi için değerlendirin.