この記事は、【 可茂IT塾 Advent Calendar 2021 】の11日目の記事です。
プロジェクト通して学んだことをアウトプットしようと思い記事にしました。
この記事はriverpodについて詳しく説明するものではなく、備忘録としてriverpodの使い方をdropdown_button
を通してサクッと大まかに説明した記事です。
今回のプロジェクトのファイル構造は以下の画像のようになっております。厳密にこの構造にしないといけないと言うわけではなく、ご自身の好みのファイル構造で大丈夫です。
controller ←状態(データ)を管理
ui ←user interfaceの部分を記述
constants ←様々な定数を記述(今回のプロジェクトではFruitとそれに該当する数字を記述)
main ←dartのプログラムを実行した際に、最初に呼び出される関数
今回のプロジェクトでは、immutable(後から変更できない)にするためにfreezed
のパッケージを使います。ここではfreezed
についてはあまり触れないので、freezed
について知りたい方は他のサイト等を参考にして頂ければと思います。
riverpod
やfreezed
は外部のパッケージであるため、それを今回のプロジェクトで使うためには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
dependencies
やdev_dependencies
の箇所に上記のように記述したら、画面上のpub get
ボタン、もしくはターミナルでflutter pub get
を入力して実行します。
メッセージやターミナルでエラーが吐かれなければ、アプリにriverpod
やfreezed
、state_notifier
のインストールが完了したことになります。
※state_notifier
はriverpod
と組み合わせて使われ、Widget
から状態(データ)とロジックを簡単に分離して、通知することができるライブラリーです。
続いて、dropdown_button
のUIを作成していきます。今回のdropdown_button
のUIは以下の画像のようになっています。今回はdropdown_button
の一例として、画像のようなレイアウトにしていますが、dropdown_button
は様々なレイアウトにできますので、ご自身の好みのUIにして頂ければと思います。
今回の記事では、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(),
],
),
);
}
}
これで一度アプリを立ち上げると、先ほど紹介した画像のような画面が表示されたと思います。
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
を実行するとエラーが解消されると思います。
最後に、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()
は主にメソッド内で使われることが多いようです。
これで、選択した値によって画面が変更できるようになったのではないかと思います。因みに、selectedFood
をprint
すると選択したFoodが、selectedFruit
をprint
すると選択した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
可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More