riverpodでimage_pickerを実装

image

この記事は、【 可茂IT塾 Advent Calendar 2021 】の18日目の記事です。

image_pickerをriverpodで実装した記事がなかなか見つけられなかったので、アウトプット&備忘録として記事にしました。

この記事はriverpodについて詳しく説明するものではなく、備忘録としてriverpodの使い方をimgae_pickerを通してサクッと大まかに説明した記事です。

Version

  • Flutter 2.8.0
  • Dart 2.15.0
  • freezed_annotation: ^1.0.0
  • state_notifier: ^0.7.1
  • hooks_riverpod: ^1.0.2
  • image_picker: ^0.8.4+4
  • build_runner: ^2.1.5
  • freezed: ^1.0.2+1

今回のプロジェクトのファイル構造

今回のプロジェクトのファイル構造は以下の画像のようになっております。厳密にこの構造にしないといけないと言うわけではなく、ご自身の好みのファイル構造で大丈夫です。

img1

  • controller ←状態(データ)を管理

  • ui ←user interfaceの部分を記述

  • main ←dartのプログラムを実行した際に、最初に呼び出される関数

riverpodを使用するための設定

今回のプロジェクトでは、immutable(後から変更できない)にするためにfreezedのパッケージを使います。ここではfreezedについてはあまり触れないので、freezedについて知りたい方は他のサイト等を参考にして頂ければと思います。

riverpodfreezedimage_pickerは外部のパッケージであるため、それを今回のプロジェクトで使うためにはpubspec.yamlファイルにそれらのパッケージを使えるように記述する必要があります。

image_pickerのパッケージはこちらになります。

pubspec.yamlファイルのdependenciesに以下のように記述します。 pubspec.yamlファイルは非常にデリケートなので記述する箇所のindent等には注意が必要です。

dependencies:
  flutter:
    sdk: flutter

  freezed_annotation: ^1.0.0
  state_notifier: ^0.7.1
  hooks_riverpod: ^1.0.2
  image_picker: ^0.8.4+4

続いてpubspec.yamlファイルのdev_dependenciesに以下のように記述します。

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^1.0.0
  build_runner: ^2.1.5
  freezed: ^1.0.2+1

dependenciesdev_dependenciesの箇所に上記のように記述したら、画面上のpub getボタン、もしくはターミナルでflutter pub getを入力して実行します。

メッセージやターミナルでエラーが吐かれなければ、アプリにriverpodfreezedstate_notifierimage_pickerのインストールが完了したことになります。

state_notifierriverpodと組み合わせて使われ、Widgetから状態(データ)とロジックを簡単に分離して、通知することができるライブラリーです。

image_pickerを利用する部分のUIを作成

ライブラリーを導入することができたので次は、UIを作成していきます。今回のimage_pickerのUIは以下の画像のようになっています。今回はCircleAvatarをタップするとimage_pickerが使えるUIを実装します。

img2

今回実装したコードは以下のようになっております。記述するファイルはlib/uiのpicker_pageファイル(lib/ui/picker_page.dart)です。

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:picker_project/controller/picker_page_controller.dart';

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final imageFile = ref.watch(pickerPageProvider.select((s) => s.imageFile));
    return Scaffold(
      appBar: AppBar(
        title: const Text('image_picker'),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Stack(
              children: [
                CircleAvatar(
                  radius: 120,
                  backgroundColor: Colors.black12,
                  child: CircleAvatar(
                    radius: 118,
                    //もしimageFile(image_pickerで選択した画像)があれば、それを表示
                    //無ければ、別の画像を表示
                    backgroundImage: imageFile != null
                        ? Image.file(imageFile, fit: BoxFit.cover).image
                        : const AssetImage('assets/images/profile.jpg'),
                  ),
                ),
                //RawMaterialButtonでCircleAvatarをtapできるようにしている
                RawMaterialButton(
                  onPressed: () async {
                    final image = await ImagePicker()
                        .pickImage(source: ImageSource.gallery);
                    await ref
                        .read(pickerPageProvider.notifier)
                        .pickImage(image);
                  },
                  child: const SizedBox(
                    width: 240,
                    height: 240,
                  ),
                  shape: const CircleBorder(),
                  elevation: 0,
                ),
              ],
            ),
            const SizedBox(height: 5),
            const Text(
              'タップして画像を選択しよう',
              style: TextStyle(fontSize: 15),
            ),
          ],
        ),
      ),
    );
  }
}

image_pickerで画像を選択していない時に表示する画像を用意

image_pickerで画像を選択されていない時にCircleAvatarで表示する画像の準備をします。image_pickerで画像を選択していない時の画面UIは以下の画像のようになっています(画像は好きなものを用意してください)。

img3

先ずは、プロジェクト内に画像ファイルを用意します。用意するファイルはプロジェクト直下(libフォルダーと同じ階層)にassetsフォルダーを用意して、その中のimagesフォルダー内(プロジェクト名/assets/images/画像名)に用意します。

次に、先ほど用意したファイルをプロジェクト内でpathを通します。やり方は、pubspec.yamlファイルのdev_dependenciesに以下のように記述するだけです。

dev_dependencies:
flutter:
 assets:
   - assets/images/画像のファイル名 (拡張子の.jpgなども忘れずに記入!)

記述し終わると再度、画面上のpub getボタン、もしくはターミナルでflutter pub getを入力して実行します。

メッセージやターミナルでエラーが吐かれなければ、プロジェクト内に画像ファイルのpathが通ったことになります。

これで、image_pickerで画像を選択していない時に表示する画像を用意することができました。

Controllerを記述

UIの画面が完成したので、次にriverpodを利用するために状態管理をするcontroller側の記述をしていきます。

まず、picker_page_controller.dartファイル(lib/controller/picker_page_contorller.dart)を用意します。そして、picker_page_controller.dartファイルには以下のように記述していきます。

import 'dart:io';

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';

part 'picker_page_controller.freezed.dart';


class PickerPageState with _$PickerPageState {
  const factory PickerPageState({
    File? imageFile,
  }) = _PickerPageState;
}

final pickerPageProvider =
    StateNotifierProvider.autoDispose<PickerPageController, PickerPageState>(
        (ref) {
  return PickerPageController();
});

class PickerPageController extends StateNotifier<PickerPageState> {
  PickerPageController() : super(const PickerPageState());

  Future<void> pickImage(XFile? image) async {
    if (image == null) return;
    state = state.copyWith(imageFile: File(image.path));
  }
}

@freezedの部分でPickerPageStateをimmutableなクラスにしています。part:〜の部分はpart:以降はcontroller側の記述をするファイル名をそのまま記述し、その後に.freezed.dartを付け足すと言う感じで記述します。

(例) test_contorller.dartであれば、以下のように記述します。

part 'test_controller.freezed.dart';

このように書くのは、freezedのパッケージがそのような仕様になっているためであるのでこんな風に書くんだなと思ってもらえればと思います。

上記のようにpicker_page_controller.dartファイル内に記述すると、至る所でエラーが出ていると思います。そのため、次にエラーを解消していきます。

まず、ターミナルでflutter packages pub run build_runner buildを実行します。Succeededと表示されるとpicker_page_controller.freezed.dartファイルが作成され、出ていたエラーが解消されたと思います。

Conflicting outputs were detected and the build is unable to prompt for permission to remove them 〜と言うエラーが生じた場合は、ターミナルでflutter packages pub run build_runner build --delete-conflicting-outputsを実行するとエラーが解消されると思います。

これでプロジェクトを実行するとエラーが出ずにプロジェクトが立ち上がったと思います。

image_pickerで画像を表示

プロジェクトが立ち上がればCircleAvatar(円の部分)をタップするとimage_pickerが呼び出されて以下の画像のような画面に遷移すると思います。

img4

画像の左上のハンバーガーメニューを押して、Downloadsを押します。そして、Downloadsの画面に表示したい画像をdrag&dropすると先ほどdrag&dropした画像が表示され、それを選択すると、プロジェクトのCircleAvatarの部分に選択した画像が表示されたと思います。

最後に

いかがでしたでしょうか。

riverpodを使ってimage_pickerの実装を紹介しました。image_pickerを使用すればアプリ内によくあるプロフィールの編集ページなどを実装することができ、作るアプリのクオリティーを一気に上げることができると思います。

私自身もまだまだriverpodについて理解できていない部分がございますので、もしこの記事内で間違った部分等がありましたらご連絡して頂けたら幸いです。

参考文献

https://qiita.com/karamage/items/4b1aff984b1af7541b73#:~:text=%E3%80%8Cstate_notifier%E3%80%8D%E3%81%AF%E3%80%81provider%E3%81%A8,%E3%81%9F%E3%82%8A%E3%81%97%E3%81%A6%E3%81%8F%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82 https://minpro.net/conflicting-outputs-were-detected-and-the-build-is-unable https://note.com/mxiskw/n/n5c06bc2dd0d5 https://note.com/hatchoutschool/n/nce574251aaae

お知らせ

8月6日開催のアプリ開発講座の参加者募集中!!

8月6日開催のアプリ開発講座の参加者募集中!!

8月6日にアプリ開発講座を開催します!会場は岐阜県美濃加茂市のコワーキングスペース「こやぁね」です。興味のある方は是非ご参加ください!

Read More
可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More

お知らせ

8月6日開催のアプリ開発講座の参加者募集中!!

8月6日開催のアプリ開発講座の参加者募集中!!

8月6日にアプリ開発講座を開催します!会場は岐阜県美濃加茂市のコワーキングスペース「こやぁね」です。興味のある方は是非ご参加ください!

Read More
可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More