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.