riverpodでDropdownButtonを実装する

image

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

プロジェクト通して学んだことをアウトプットしようと思い記事にしました。

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

Version

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

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

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

img2

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

  • ui ←user interfaceの部分を記述

  • constants ←様々な定数を記述(今回のプロジェクトではFruitとそれに該当する数字を記述)

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

riverpodを使用するための設定

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

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

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

続いて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_notifierのインストールが完了したことになります。

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

dropdown_buttonのUIを作成

続いて、dropdown_buttonのUIを作成していきます。今回のdropdown_buttonのUIは以下の画像のようになっています。今回はdropdown_buttonの一例として、画像のようなレイアウトにしていますが、dropdown_buttonは様々なレイアウトにできますので、ご自身の好みのUIにして頂ければと思います。

img1

今回の記事では、2パターンのdropdown_buttonを紹介していきます。一つ目は、Food(食べ物)のリストから選んだ値をそのまま渡すdropdown_buttonです。もう一つは、Fruit(果物)のリストから選んだ値に該当する数字を渡すdropdown_buttonです。

(例)

Food: 焼肉を選ぶ → 渡す値は焼肉

Fruit: りんごを選ぶ → 渡す値は1 と言う感じです。

二つ目のパターンを実装するにあたってlibの直下にconstantsファイル(lib/constants.dart)を追加します。そして、constantsファイルに以下のように記述します。

const kFruit = {
  1: 'りんご',
  2: 'ぶどう',
  3: 'もも',
};

このようにkFruitを予めmap型で定義することによって、りんごを選ぶと1、ぶどうを選ぶと2が渡されるようになります。因みにkFruitはdropdown_button.dartファイル内にimportして使用します。

次は、lib直下のmain.dart(lib/main.dart)についてです。main.dartファイルには以下のように記述していきます。

import 'package:dropdown_button/ui/dropdown_button.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';


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

class MainApp extends StatelessWidget {
  const MainApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'dropdown_button',
      debugShowCheckedModeBanner: false,
      home: DropdownButtonPage(),
    );
  }
}

runApp()内に記述しているProviderScope()はriverpodが使える範囲を指定しています。そのため、今回のプロジェクトはMainApp()より下のWidgetツリーでriverpodが使えることになります。

次に、dropdown_button.dart(lib/ui/dropdown_button.dart)についてです。dropdown_button.dartファイルには以下のように記述していきます。

import 'package:dropdown_button/constants.dart';
import 'package:flutter/material.dart';

class DropdownButtonPage extends StatelessWidget {
  const DropdownButtonPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('DropdownButton'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Divider(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: DropdownButtonHideUnderline(
              child: DropdownButton<String>(
                isExpanded: true,
                hint: const Text('Food'),
                style: const TextStyle(fontSize: 16, color: Colors.black),
                icon: const Icon(Icons.expand_more),
                onChanged: (newValue) {},
                items: ['焼肉', '寿司', 'パンケーキ']
                    .map<DropdownMenuItem<String>>((value) {
                  return DropdownMenuItem(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ),
          ),
          const Divider(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: DropdownButtonHideUnderline(
              child: DropdownButton<int?>(
                isExpanded: true,
                hint: const Text('Fruit'),
                style: const TextStyle(fontSize: 16, color: Colors.black),
                icon: const Icon(Icons.expand_more),
                onChanged: (newValue) {},
                items: kFruit.keys.map<DropdownMenuItem<int?>>((value) {
                  return DropdownMenuItem(
                    value: value,
                    child: Text(kFruit[value].toString()),
                  );
                }).toList(),
              ),
            ),
          ),
          const Divider(),
        ],
      ),
    );
  }
}

これで一度アプリを立ち上げると、先ほど紹介した画像のような画面が表示されたと思います。

Controllerを記述

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

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

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

part 'dropdown_button_controller.freezed.dart';


class DropdownButtonPageState with _$DropdownButtonPageState {
  const factory DropdownButtonPageState({
    int? selectFruit,
    String? selectFood,
  }) = _DropdownButtonPageState;
}

final dropdownButtonPageProvider = StateNotifierProvider.autoDispose<
    DropdownButtonPageController, DropdownButtonPageState>((ref) {
  return DropdownButtonPageController();
});

class DropdownButtonPageController
    extends StateNotifier<DropdownButtonPageState> {
  DropdownButtonPageController() : super(const DropdownButtonPageState());

  void selectedFruit(int? selectFruit) {
    state = state.copyWith(selectFruit: selectFruit);
  }

  void selectedFood(String? selectFood) {
    state = state.copyWith(selectFood: selectFood);
  }
}

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

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

part 'test_controller.freezed.dart';

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

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

まず、ターミナルでflutter packages pub run build_runner buildを実行します。Succeededと表示されるとdropdown_button_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を実行するとエラーが解消されると思います。

dropdown_button.dartをriverpod対応にする

最後に、UIの画面であるdropdown_button.dartファイルをriverpod対応にします。dropdown_button.dartファイルを以下のように変更します。

import 'package:dropdown_button/constants.dart';
import 'package:dropdown_button/controller/dropdown_button_controller.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class DropdownButtonPage extends ConsumerWidget { //StatelessWidgetからConsumerWidgetに変更
  const DropdownButtonPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedFruit = ref
        .watch(dropdownButtonPageProvider.select((state) => state.selectFruit)); //追加
    final selectedFood = ref
        .watch(dropdownButtonPageProvider.select((state) => state.selectFood)); //追加
    return Scaffold(
      appBar: AppBar(
        title: const Text('DropdownButton'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Divider(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: DropdownButtonHideUnderline(
              child: DropdownButton<String>(
                isExpanded: true,
                hint: const Text('未選択'),
                value: selectedFood, //追加
                style: const TextStyle(fontSize: 16, color: Colors.black),
                icon: const Icon(Icons.expand_more),
                onChanged: ref.read(dropdownButtonPageProvider.notifier).selectedFood, //追加
                items: ['焼肉', '寿司', 'パンケーキ']
                    .map<DropdownMenuItem<String>>((value) {
                  return DropdownMenuItem(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ),
          ),
          const Divider(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: DropdownButtonHideUnderline(
              child: DropdownButton<int?>(
                isExpanded: true,
                hint: const Text('未選択'),
                value: selectedFruit, //追加
                style: const TextStyle(fontSize: 16, color: Colors.black),
                icon: const Icon(Icons.expand_more),
                onChanged: ref.read(dropdownButtonPageProvider.notifier).selectedFruit, //追加
                items: kFruit.keys.map<DropdownMenuItem<int?>>((value) {
                  return DropdownMenuItem(
                    value: value,
                    child: Text(kFruit[value].toString()),
                  );
                }).toList(),
              ),
            ),
          ),
          const Divider(),
        ],
      ),
    );
  }
}

dropdown_button.dartファイルをriverpodに対応するために、

final selectedFruit = ref
        .watch(dropdownButtonPageProvider.select((state) => state.selectFruit));

を追加しました。

ref.watch(provider)では、refの部分で他のproviderにアクセスできるようにして、watch(provider)の部分で値が変更された時にproviderを再生成するようにして、状態(データ)が変わるごとに画面が変更するようにしています。

ref.watch()は主にbuild内で使用しますが、ref.read()は主にメソッド内で使われることが多いようです。

これで、選択した値によって画面が変更できるようになったのではないかと思います。因みに、selectedFoodprintすると選択したFoodが、selectedFruitprintすると選択したFruitに該当する数字がターミナルに出力されるようになっていると思います。

最後に

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

riverpodを使ってdropdown_buttonの実装を紹介しました。今回紹介した機能を応用すれば、アンケートアプリ等が作れるようになるため、作れるアプリの幅を広げることができます。

私自身もまだまだ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

お知らせ

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