image_picker

image_picker は、カメラで撮影したり、ギャラリーから画像を選択するパッケージです!
比喩で言うと: スマホの写真屋さんのようなもの。「カメラで撮って!」「アルバムから選んで!」とお願いできます! 📸

🤔 何ができるの?
できること:
✅ カメラで撮影 – 写真・動画
✅ ギャラリーから選択 – 写真・動画
✅ 複数枚選択 – 一度に複数の画像
✅ 画質設定 – 圧縮率を調整
✅ サイズ制限 – 最大幅・高さを指定

使う場面の例:

用途:

  • プロフィール画像・・・アバター設定
  • 写真投稿・・・SNS風アプリ
  • 領収書撮影・・・経費管理アプリ
  • 作品写真・・・ポートフォリオ
  • 商品写真・・・フリマアプリ
  • イベント写真・・・記録・共有

インストール方法

pubspec.yaml に追加

dependencies:
  flutter:
    sdk: flutter
  
  image_picker: ^1.0.0

パッケージを取得

flutter pub get

import する

import 'package:image_picker/image_picker.dart';
import 'dart:io';  // File を使うため

基本的な使い方

STEP
ImagePicker インスタンスを作る
final ImagePicker picker = ImagePicker();
STEP
画像を選択/撮影
// ギャラリーから選択
final XFile? image = await picker.pickImage(source: ImageSource.gallery);

// カメラで撮影
final XFile? image = await picker.pickImage(source: ImageSource.camera);
STEP
画像を表示

CC、BCCも設定:

if (image != null) {
  File imageFile = File(image.path);
  
  // 画像を表示
  Image.file(imageFile);
}

実践例

1: ギャラリーから画像を選択

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

class ImagePickerDemo extends StatefulWidget {
  @override
  _ImagePickerDemoState createState() => _ImagePickerDemoState();
}

class _ImagePickerDemoState extends State<ImagePickerDemo> {
  File? _image;
  final ImagePicker _picker = ImagePicker();
  
  // ギャラリーから選択
  Future<void> _pickImageFromGallery() async {
    final XFile? image = await _picker.pickImage(
      source: ImageSource.gallery,
    );
    
    if (image != null) {
      setState(() {
        _image = File(image.path);
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('画像選択')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 画像を表示
            _image != null
                ? Image.file(_image!, height: 300)
                : Text('画像が選択されていません'),
            SizedBox(height: 20),
            
            // 選択ボタン
            ElevatedButton.icon(
              icon: Icon(Icons.photo_library),
              label: Text('ギャラリーから選択'),
              onPressed: _pickImageFromGallery,
            ),
          ],
        ),
      ),
    );
  }
}
 

2: カメラで撮影

class CameraDemo extends StatefulWidget {
  @override
  _CameraDemoState createState() => _CameraDemoState();
}

class _CameraDemoState extends State<CameraDemo> {
  File? _image;
  final ImagePicker _picker = ImagePicker();
  
  // カメラで撮影
  Future<void> _takePicture() async {
    final XFile? image = await _picker.pickImage(
      source: ImageSource.camera,
    );
    
    if (image != null) {
      setState(() {
        _image = File(image.path);
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('カメラ撮影')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _image != null
                ? Image.file(_image!, height: 300)
                : Icon(Icons.camera_alt, size: 100, color: Colors.grey),
            SizedBox(height: 20),
            
            ElevatedButton.icon(
              icon: Icon(Icons.camera_alt),
              label: Text('写真を撮る'),
              onPressed: _takePicture,
            ),
          ],
        ),
      ),
    );
  }
}
 

3: 選択肢を表示(ギャラリー or カメラ)

ユーザーに選ばせる、より親切な実装:

class ImageSourceSelector extends StatefulWidget {
  @override
  _ImageSourceSelectorState createState() => _ImageSourceSelectorState();
}

class _ImageSourceSelectorState extends State<ImageSourceSelector> {
  File? _image;
  final ImagePicker _picker = ImagePicker();
  
  // 画像ソースを選択
  Future<void> _showImageSourceDialog() async {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('画像を選択'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: Icon(Icons.camera_alt),
              title: Text('カメラで撮影'),
              onTap: () {
                Navigator.pop(context);
                _pickImage(ImageSource.camera);
              },
            ),
            ListTile(
              leading: Icon(Icons.photo_library),
              title: Text('ギャラリーから選択'),
              onTap: () {
                Navigator.pop(context);
                _pickImage(ImageSource.gallery);
              },
            ),
          ],
        ),
      ),
    );
  }
  
  Future<void> _pickImage(ImageSource source) async {
    final XFile? image = await _picker.pickImage(source: source);
    
    if (image != null) {
      setState(() {
        _image = File(image.path);
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('画像選択')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _image != null
                ? Image.file(_image!, height: 300)
                : Icon(Icons.add_photo_alternate, size: 100, color: Colors.grey),
            SizedBox(height: 20),
            
            ElevatedButton.icon(
              icon: Icon(Icons.add_a_photo),
              label: Text('画像を追加'),
              onPressed: _showImageSourceDialog,
            ),
          ],
        ),
      ),
    );
  }
}
 

画質とサイズの設定

画質を調整(圧縮):

Future<void> _pickImageWithQuality() async {
  final XFile? image = await _picker.pickImage(
    source: ImageSource.gallery,
    imageQuality: 85,  // 0-100 (100が最高品質)
  );
  
  if (image != null) {
    setState(() {
      _image = File(image.path);
    });
  }
}

imageQuality:

100 – 最高品質(ファイルサイズ大)
85 – 高品質(おすすめ!)
50 – 中品質
25 – 低品質(ファイルサイズ小)

サイズを制限:

Future<void> _pickImageWithSize() async {
  final XFile? image = await _picker.pickImage(
    source: ImageSource.gallery,
    maxWidth: 1920,   // 最大幅
    maxHeight: 1080,  // 最大高さ
    imageQuality: 85,
  );
  
  if (image != null) {
    setState(() {
      _image = File(image.path);
    });
  }
}

用途別おすすめサイズ:

用途maxWidthmaxHeightimageQuality
プロフィール画像51251285
投稿写真1920108085
サムネイル25625670
高画質保存なしなし100

4: 複数画像を選択

class MultipleImagePicker extends StatefulWidget {
  @override
  _MultipleImagePickerState createState() => _MultipleImagePickerState();
}

class _MultipleImagePickerState extends State<MultipleImagePicker> {
  List<File> _images = [];
  final ImagePicker _picker = ImagePicker();
  
  // 複数画像を選択
  Future<void> _pickMultipleImages() async {
    final List<XFile> images = await _picker.pickMultiImage(
      imageQuality: 85,
      maxWidth: 1920,
      maxHeight: 1080,
    );
    
    if (images.isNotEmpty) {
      setState(() {
        _images = images.map((image) => File(image.path)).toList();
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('複数画像選択'),
        actions: [
          // 選択枚数を表示
          Center(
            child: Padding(
              padding: EdgeInsets.only(right: 16),
              child: Text('${_images.length}枚'),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // 選択ボタン
          Padding(
            padding: EdgeInsets.all(16),
            child: ElevatedButton.icon(
              icon: Icon(Icons.add_photo_alternate),
              label: Text('画像を選択'),
              onPressed: _pickMultipleImages,
            ),
          ),
          
          // 画像グリッド表示
          Expanded(
            child: _images.isEmpty
                ? Center(child: Text('画像が選択されていません'))
                : GridView.builder(
                    padding: EdgeInsets.all(8),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 3,
                      crossAxisSpacing: 8,
                      mainAxisSpacing: 8,
                    ),
                    itemCount: _images.length,
                    itemBuilder: (context, index) {
                      return Stack(
                        fit: StackFit.expand,
                        children: [
                          // 画像
                          Image.file(
                            _images[index],
                            fit: BoxFit.cover,
                          ),
                          
                          // 削除ボタン
                          Positioned(
                            top: 4,
                            right: 4,
                            child: IconButton(
                              icon: Icon(Icons.close, color: Colors.white),
                              style: IconButton.styleFrom(
                                backgroundColor: Colors.black54,
                              ),
                              onPressed: () {
                                setState(() {
                                  _images.removeAt(index);
                                });
                              },
                            ),
                          ),
                        ],
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

動画も選択できる!

class VideoPicker extends StatefulWidget {
  @override
  _VideoPickerState createState() => _VideoPickerState();
}

class _VideoPickerState extends State<VideoPicker> {
  File? _video;
  final ImagePicker _picker = ImagePicker();
  
  // 動画を選択
  Future<void> _pickVideo() async {
    final XFile? video = await _picker.pickVideo(
      source: ImageSource.gallery,
      maxDuration: Duration(seconds: 30),  // 最大30秒
    );
    
    if (video != null) {
      setState(() {
        _video = File(video.path);
      });
    }
  }
  
  // 動画を撮影
  Future<void> _recordVideo() async {
    final XFile? video = await _picker.pickVideo(
      source: ImageSource.camera,
      maxDuration: Duration(seconds: 30),
    );
    
    if (video != null) {
      setState(() {
        _video = File(video.path);
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('動画選択')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _video != null
                ? Text('動画パス: ${_video!.path}')
                : Text('動画が選択されていません'),
            SizedBox(height: 20),
            
            ElevatedButton.icon(
              icon: Icon(Icons.video_library),
              label: Text('動画を選択'),
              onPressed: _pickVideo,
            ),
            SizedBox(height: 10),
            
            ElevatedButton.icon(
              icon: Icon(Icons.videocam),
              label: Text('動画を撮影'),
              onPressed: _recordVideo,
            ),
          ],
        ),
      ),
    );
  }
}

プラットフォーム別の設定

Android:

android/app/src/main/AndroidManifest.xml に追加:

<manifest>
    <!-- カメラの権限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    
    <!-- 写真の読み取り -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    
    <!-- Android 13+ 用 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    
    <application>
        <!-- ... -->
    </application>
</manifest>

iOS:

ios/Runner/Info.plist に追加:

<key>NSCameraUsageDescription</key>
<string>写真を撮影するためにカメラを使用します</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>写真を選択するためにフォトライブラリにアクセスします</string>

<key>NSMicrophoneUsageDescription</key>
<string>動画を撮影するためにマイクを使用します</string>

重要: これらのメッセージは、ユーザーに権限リクエスト時に表示されます!

画像の保存方法

アプリ内に保存(ローカル):

import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;

Future<String> _saveImageLocally(File image) async {
  // アプリのドキュメントディレクトリを取得
  final Directory appDir = await getApplicationDocumentsDirectory();
  
  // ファイル名を生成
  final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
  
  // 保存先パス
  final String savePath = path.join(appDir.path, fileName);
  
  // ファイルをコピー
  await image.copy(savePath);
  
  return savePath;
}

データベースにパスを保存:

// sqflite の例
Future<void> _savePhotoPathToDatabase(String photoPath) async {
  final db = await database;
  
  await db.insert(
    'students',
    {
      'id': studentId,
      'name': studentName,
      'photoPath': photoPath,  // パスを保存
    },
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

// 取得
Future<String?> _getPhotoPath(String studentId) async {
  final db = await database;
  
  final List<Map<String, dynamic>> maps = await db.query(
    'students',
    where: 'id = ?',
    whereArgs: [studentId],
  );
  
  if (maps.isNotEmpty) {
    return maps.first['photoPath'] as String?;
  }
  return null;
}

サーバーにアップロード:

import 'package:http/http.dart' as http;

Future<void> _uploadImage(File image) async {
  final Uri url = Uri.parse('https://example.com/api/upload');
  
  final request = http.MultipartRequest('POST', url);
  request.files.add(
    await http.MultipartFile.fromPath('image', image.path),
  );
  
  final response = await request.send();
  
  if (response.statusCode == 200) {
    print('アップロード成功!');
  } else {
    print('アップロード失敗');
  }
}

注意点とコツ

null チェックを忘れずに

// ❌ 危険
final XFile image = await _picker.pickImage(...);  // null かもしれない!

// ✅ 安全
final XFile? image = await _picker.pickImage(...);
if (image != null) {
  // 処理
}

ユーザーがキャンセルすると null が返ります!

ファイルサイズに注意

// 画像サイズを確認
File imageFile = File(image.path);
int fileSizeInBytes = await imageFile.length();
double fileSizeInMB = fileSizeInBytes / (1024 * 1024);

print('ファイルサイズ: ${fileSizeInMB.toStringAsFixed(2)} MB');

// 大きすぎる場合は警告
if (fileSizeInMB > 5) {
  print('ファイルサイズが大きすぎます!');
}

画質とサイズのバランス

// おすすめ設定
final XFile? image = await _picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1920,      // Full HD
  maxHeight: 1080,
  imageQuality: 85,    // 高品質だけどサイズ控えめ
);

権限エラーのハンドリング

Future<void> _pickImageWithErrorHandling() async {
  try {
    final XFile? image = await _picker.pickImage(
      source: ImageSource.camera,
    );
    
    if (image != null) {
      setState(() {
        _image = File(image.path);
      });
    }
  } catch (e) {
    // 権限エラーなど
    print('エラー: $e');
    
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('エラー'),
        content: Text('カメラにアクセスできません。設定で権限を許可してください。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }
}

メモリ管理
複数画像を扱うときは注意:

// ❌ メモリを大量に使う
List<Image> imageWidgets = _images.map((file) => Image.file(file)).toList();

// ✅ 必要なときだけ読み込む
ListView.builder(
  itemCount: _images.length,
  itemBuilder: (context, index) {
    return Image.file(_images[index]);  // 画面に表示されるときだけ
  },
);

画像編集パッケージとの組み合わせ

image_picker で選択した後、編集することも:

// image_cropper パッケージと組み合わせ
import 'package:image_cropper/image_cropper.dart';

Future<void> _pickAndCropImage() async {
  // 1. 画像を選択
  final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
  
  if (image != null) {
    // 2. 画像を切り抜く
    final CroppedFile? croppedFile = await ImageCropper().cropImage(
      sourcePath: image.path,
      aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),  // 正方形
      compressQuality: 85,
    );
    
    if (croppedFile != null) {
      setState(() {
        _image = File(croppedFile.path);
      });
    }
  }
}

まとめ: いつ使う?

用途image_pickercamera パッケージ
写真を選択
簡単な撮影
プレビュー付き撮影
高度なカメラ機能
動画撮影
複数選択

ルール:
シンプルな画像選択・撮影 → image_picker、
高度なカメラ機能 → camera パッケージ

一言まとめ:
image_picker はカメラ撮影とギャラリー選択ができるパッケージ!
pickImage で1枚、pickMultiImage で複数枚。
画質とサイズを調整して、ファイルサイズを最適化!
権限設定とnullチェックを忘れずに! 📸✨