Flutterでローカルの画像やカメラを起動して撮影した画像を使用するimage_pickerを使ってみた

2024.04.08

こんにちは、ゲームソリューション部のsoraです。
今回は、Flutterでローカルの画像やカメラを起動して撮影した画像を使用するimage_pickerを使ってみたことについて書いていきます。

実装した画面

ローカルの画像やカメラを起動して撮影した画像を使用して、画像を変更するシンプルなアプリです。
以下画像は貼っていませんが、カメラで撮影ボタンを押すことで、カメラの起動・撮影した画像の取得もできます。

利用する主要なパッケージ

image_picker
image_picker | Flutter package
使用したバージョン(pubspec.yamlから抜粋):image_picker: ^1.0.7
ローカルに保存されている画像やカメラを起動して撮影した画像を使用するためのパッケージ

flutter_riverpod
flutter_riverpod | Flutter package
使用したバージョン(pubspec.yamlから抜粋):flutter_riverpod: ^2.4.10
状態管理パッケージ
多くの情報が落ちている人気なパッケージのため、詳しい説明は割愛します。

コードの解説

コードは以下です。
main.dartでメインの画面、provider/after_background_image.dartprovider/before_background_image.dartで画像変更前後のそれぞれの状態管理をしています。

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'provider/after_background_image.dart';
import 'provider/now_background_image.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final afterImageValue = ref.watch(afterBackgroundImageProvider);
    final afterImageNotifier = ref.watch(afterBackgroundImageProvider.notifier);
    final nowImageValue = ref.watch(nowBackgroundImageProvider);
    final nowImageNotifier = ref.watch(nowBackgroundImageProvider.notifier);
    final imagePicker = ImagePicker();

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('image_picker Test'),
        centerTitle: true,
      ),
      body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                '現在の画像',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18,
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                width: 200,
                height: 200,
                // 現在の画像パスを取得して、空であればデフォルト画像、値があれば画像の表示
                child: nowImageValue == ''
                    ? Image.asset('assets/images/default.png')
                    : Image.file(nowImageValue),
              ),
              const SizedBox(height: 40),
              const Text(
                '変更後の画像',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18,
                ),
              ),
              SizedBox(
                width: 200,
                height: 200,
                // 変更後の画像パスを取得して、空であればデフォルト画像、値があれば画像の表示
                child: afterImageValue == ''
                    ? const Text('')
                    : Image.file(afterImageValue),
              ),
              const SizedBox(height: 40),
              ElevatedButton(
                child: const Text('ギャラリーから選択'),
                onPressed: () async {
                  final imageFilePath = await imagePicker.pickImage(source: ImageSource.gallery);
                  afterImageNotifier.setImage(imageFilePath);
                },
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                child: const Text('カメラで撮影'),
                onPressed: () async {
                  final imageFilePath = await imagePicker.pickImage(source: ImageSource.camera);
                  afterImageNotifier.setImage(imageFilePath);
                },
              ),
              const SizedBox(height: 10),
              Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: (){
                        afterImageNotifier.clearImage();
                        },
                      child: const Text('画像クリア'),
                    ),
                    const SizedBox(width: 20),
                    ElevatedButton(
                      child: const Text('画像変更'),
                      onPressed: () {
                        if (afterImageValue != '') {
                          showDialog(
                              context: context,
                              builder: (_) {
                                return AlertDialog(
                                  content: const Text('画像を変更しても良いですか?'),
                                  actions: <Widget>[
                                    GestureDetector(
                                      child: const Text('いいえ'),
                                      onTap: () {
                                        Navigator.pop(context);
                                        },
                                    ),
                                    GestureDetector(
                                      child: const Text('はい'),
                                      onTap: () {
                                        nowImageNotifier.setImage(afterImageValue);
                                        afterImageNotifier.clearImage();
                                        Navigator.pop(context);
                                        showDialog(
                                          context: context,
                                          builder: (_) {
                                            return AlertDialog(
                                                content: const Text('変更しました'),
                                                actions: <Widget>[
                                                  Builder(builder: (context) {
                                                    return GestureDetector(
                                                      child: const Text('はい'),
                                                      onTap: () {
                                                        Navigator.pop(context);
                                                        },
                                                    );
                                                    },
                                                  )
                                                ]
                                            );
                                            },
                                        );
                                        },
                                    )
                                  ],
                                );
                              }
                              );
                        } else {
                          showDialog(
                            context: context,
                            builder: (_) {
                              return AlertDialog(
                                  content: const Text('変更後の画像が選択されていません'),
                                  actions: <Widget>[
                                    GestureDetector(
                                      child: const Text('はい'),
                                      onTap: () {
                                        Navigator.pop(context);
                                        },
                                    )
                                  ]
                              );
                              },
                          );
                        }
                        },
                    )
                  ]
              ),
            ],
          )
      ),
    );
  }
}

provider/after_background_image.dart

import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AfterBackgroundImage extends Notifier<dynamic> {
  @override
  String build(){
    return '';
  }
  void setImage(dynamic imageFilePath) {
    // XFile型からFile型に変換して、pathの形で状態を更新
    final imagePath = File(imageFilePath.path);
    state = imagePath;
  }
  void clearImage() {
    state = '';
  }
}

final afterBackgroundImageProvider = NotifierProvider<AfterBackgroundImage, dynamic>(() {
  return AfterBackgroundImage();
});

provider/now_background_image.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

class NowBackgroundImage extends Notifier<dynamic> {
  @override
  String build(){
    return '';
  }
  void setImage(dynamic imagePath) {
    state = imagePath;
  }
}

final nowBackgroundImageProvider = NotifierProvider<NowBackgroundImage, dynamic>(() {
  return NowBackgroundImage();
});

image_pickerを使用して画像の表示

image_pickerを使用している箇所は以下です。
await imagePicker.pickImage(source: ImageSource.gallery);でローカル画像の選択を開き、画像を選択するとその画像のパスがXFile型で取得できます。
カメラも同じ要領で、await imagePicker.pickImage(source: ImageSource.camera);でカメラが起動して撮影した画像のパスをXFile型で取得することができます。

main.dart

              ElevatedButton(
                child: const Text('ギャラリーから選択'),
                onPressed: () async {
                  final imageFilePath = await imagePicker.pickImage(source: ImageSource.gallery);
                  afterImageNotifier.setImage(imageFilePath);
                },
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                child: const Text('カメラで撮影'),
                onPressed: () async {
                  final imageFilePath = await imagePicker.pickImage(source: ImageSource.camera);
                  afterImageNotifier.setImage(imageFilePath);
                },
              ),

先ほど記載した通り、取得した情報はXFile型のため、画像を表示するためにFile型に変換してから状態を更新し、その状態を使用して画像を表示します。
ちなみに、File型とXFile型の違いは以下記事をご参考ください。
FlutterのFileとXFileとの違い

provider/now_background_image.dart

  void setImage(dynamic imageFilePath) {
    // XFile型からFile型に変換して、pathの形で状態を更新
    final imagePath = File(imageFilePath.path);
    state = imagePath;
  }

main.dart

// 現在の画像パスを取得して、空であればデフォルト画像、値があれば画像の表示
child: nowImageValue == ''
    ? Image.asset('assets/images/default.png')
    : Image.file(nowImageValue),
…
// 変更後の画像パスを取得して、空であれば何も表示せず、値があれば画像の表示
child: afterImageValue == ''
    ? const Text('')
    : Image.file(afterImageValue),

iOSでアプリリリースする場合

iOSでアプリをリリースする場合、Apple Storeへの申請をすると思いますが、その際に以下のカメラとギャラリー(ローカルの画像ライブラリ)を使用する理由と目的の記述が必要になります。
ちなみに、Androidは不要です。

ios/Runner/Info.plist

<plist version="1.0">
<dict>
…
<key>NSCameraUsageDescription</key>
<string>カメラを使う理由・用途を記述(アプリリリース時にAppleレビューを通すために記述)</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>ギャラリーを使う理由・用途を記述(アプリリリース時にAppleレビューを通すために記述)</string>
</dict>
</plist>

変更後画像への更新

画像変更ボタンを押すと、ダイアログを表示させます。
はいを押すと、現在の画像パスの状態を変更後の画像パスで更新して、変更後の画像の状態をクリアします。

main.dart

                    ElevatedButton(
                      child: const Text('画像変更'),
                      onPressed: () {
                        if (afterImageValue != '') {
                          showDialog(
                              context: context,
                              builder: (_) {
                                return AlertDialog(
                                  content: const Text('画像を変更しても良いですか?'),
                                  actions: <Widget>[
                                    GestureDetector(
                                      child: const Text('いいえ'),
                                      onTap: () {
                                        Navigator.pop(context);
                                        },
                                    ),
                                    GestureDetector(
                                      child: const Text('はい'),
                                      onTap: () {
                                        // 現在の画像パスの状態を変更後の画像パスで更新
                                        nowImageNotifier.setImage(afterImageValue);
                                        // 変更後の画像パスをクリア
                                        afterImageNotifier.clearImage();
                                        Navigator.pop(context);
                                        showDialog(
                                          context: context,
                                          builder: (_) {
                                            return AlertDialog(
                                                content: const Text('変更しました'),
                                                actions: <Widget>[
                                                  Builder(builder: (context) {
                                                    return GestureDetector(
                                                      child: const Text('はい'),
                                                      onTap: () {
                                                        Navigator.pop(context);
                                                        },
                                                    );
                                                    },
                                                  )
                                                ]
                                            );
                                            },
                                        );
                                        },
                                    )
                                  ],
                                );
                              }
                              );
                        } else {
                          showDialog(
                            context: context,
                            builder: (_) {
                              return AlertDialog(
                                  content: const Text('変更後の画像が選択されていません'),
                                  actions: <Widget>[
                                    GestureDetector(
                                      child: const Text('はい'),
                                      onTap: () {
                                        Navigator.pop(context);
                                        },
                                    )
                                  ]
                              );
                              },
                          );
                        }
                        },
                    )

最後に

今回は、Flutterでローカルの画像やカメラを起動して撮影した画像を使用するimage_pickerを使ってみたことを記事にしました。
どなたかの参考になると幸いです。