この記事について
NotifierProviderのドキュメントを理解するために調べたことを備忘録として残した記事です。NotifierProviderのドキュメントに沿って、書いています。動作を確認したわけでなくChangeNotifierやStateNotifierProviderからの移行のついでにドキュメントを読んでまとめているので間違いがある可能性があります。その時は、ご指摘いただけるとありがたいです。
とりあえず、この記事を読めば、NotifierProviderがどんなことができて、どうやって使えばよいかを理解できるように心がけたつもりです。
この記事では以下のことに触れています。
・@freezed
について
・@riverpod
について
・ConsumerWidgetについて
・Asyncの方には触れません
freezedって何ぞや?
さて、NotifierProviderのドキュメントで最初に現れるこの@freezed
。最初はriverpodのものなのかなと思っていたのですが、全然違うパッケージのものでした。freezedの説明ページはfreezedのドキュメントにあります。
@freezed
class Todo with _$Todo {
factory Todo({
required String id,
required String description,
required bool completed,
}) = _Todo;
}
freezedの役割を簡単に説明すると, 「イミュータブルなクラスを作るときに必要となるであろうメソッドを自動で追加してくれる」というものです。
ここで、イミュータブルなクラスって言われた瞬間に、あれ?状態変更できんじゃん。見るページ間違えたか??って思うわけです。ただ、安心してください。ちゃんと、状態を変更できます。
公式ドキュメント曰く、ミュータブルなオブジェクトで情報を管理するといろいろ問題があるらしいです。
で、ここで、イミュータブルなクラスなのにどうやって状態を変更するんだっていう疑問が湧くわけですが、freezedが勝手に追加しているメソッドcopyWith
がヒントになります。copyWith
はオブジェクトをコピーするわけですが、状態を変更したいときは、このメソッドを活用します。
下は, freezedのドキュメントにあるPersonクラスの一部を抜粋して、copyWithメソッドに少し変更を加えたものです。
@immutable
class Person{
final String firstName;
final String lastName;
final int age;
const Person({
required this.firstName,
required this.lastName,
required this.age,
});
Person copyWith({
String? firstName,
String? lastName,
int? age,
}){
return Person(
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
age: age ?? this.age
);
}
}
copyWithメソッドでは、新しいPersonのインスタンスを作成して返すということを行っています。ここで、便宜上、現在すでにあるインスタンスをold Person。copyWithメソッド内で作られているインスタンスをnew Personとすると、copyWithメソッドの引数にnon NULL値が与えられた場合は、その値をnew Personに渡し、null値が与えられた場合は、old Personの値をnew Personに渡します。これにより、変更したい部分のみのメンバ変数の値が変わったPersonクラスのインスタンスの作成が可能になっています。
つまり、これを利用することでriverpodは状態変更を可能にしていると思われます。@immutable
でもイミュータブルなクラスを作成することはできますが、ここらへんのcopyWithを手動で書く必要があります。copyWithなどのメソッドを自動で作成するために、@freezed
を使っていると思われます。
riverpodとはなんぞや?
まず、気になるのは_$Todos
だと思うが、これはriverpodが状態管理をするためになんらかの関数を加えているということだと思うこととして、深くは触れないことにする。クラス名の前に"_$"をつけて、extendsしておけばよさそうである。
@riverpod
class Todos extends _$Todos {
}
そして、ここで知っておいた方がよいイメージがある。それは、状態管理できる変数は1つだけということである。具体的にはstateという変数が勝手に用意される。このstateの型はどうやって決まるんだ?という話だが、このbuildメソッドが関係している。
Widgetのbuildと混乱しそうになるが、別物である。このbuildメソッドはListという型を返している。つまり、今回管理できる状態はList型のstateのみとなる。で、buildメソッドはstateの初期値を返さないといけないので、今回は何もないリスト[]
を返しているというわけである。
@override
List<Todo> build(){
return [];
}
ここで、複数の変数を管理したい場合はどうなるんだ?って思うと思う。(自分も思った)
その場合は、その複数の変数をまとめたクラスを定義すれば解決する。例えば、先ほどの例であったPersonクラスは、3つのメンバ変数を持つ。そのため、以下のような、buildメソッドを定義すれば、いけると思われる。とりあえず、初期値は、20歳の見本太郎とした。
@override
Person build(){
return Person(
firstName: "見本",
lastName: "太郎",
age: 20,
)
}
さて、次にaddTodoを見ていく。以下に、プログラムを示す。
void addTodo(Todo todo){
state = [...state, todo];
}
上のプログラムを初めて見た時、「君はいったい何をしたいんだい??」という声が、頭中を駆け巡ったのを今でも忘れない。
さて、これを理解するためには、先ほどのイミュータブルという話を思い出してほしい。RiverPodはいろいろイミュータブルにこだわっている。そこで、stateがイミュータブルな変数と定義されていたとしたら、どうでしょうか?state.add()
とかできないっすね??それじゃあ、どうなりますか?
もともとあったリストの要素に新しいtodoを追加した新しいリストを作るしかないですね。それをやっています。
stateの前の...はリストの要素を展開するという処理らしいのでstate = [1,2,3]でtodoに入ってきていたのが4だとしたら、...stateで展開して [1,2,3,4]というリストを新しく生成して、stateに代入しているという処理になります。
上の説明を理解できたら、removeTodo
の処理も見えてきませんか?
void removeTodo(String todoId){
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
todoIdに対応する要素を除いたリストを新しく生成して、stateに代入。
最後の, toggleは、
void toggle(String todoId){
state = [
for (final todo in state)
if (todo.id == todoId)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
todoIdに対応するTodoオブジェクトのcompletedの値を反転させた新しいリストを作成して、stateに代入。
@freezed
によって自動生成されたtodo.copyWithがここで使われてますね!!
あと、ChangeNotifierの時には、NotifyListners()のような、値が変化したぞって伝える関数が定義されていたのですが、NotifierProviderの場合は, state変数の更新がトリガーになっているっぽいです。つまり、stateがイミュータブルなのは、更新されたことを認識するためっぽいかな?と私は思います。
Providerの値を取得する
とりあえず、状態を取得したいウィジットでConsumerWidgetを継承して、buildメソッドにWidgetRef ref
引数を追加しておけば、アクセスはできるようになります。ちなみに、ConsumerStatefulWidgetもありますよ。(参考)
さて、ConsumerWidgetの最初の難関はtodosProviderではないでしょうか?
List<Todo> todos = ref.watch(todosProvider);
このtodosProviderがどこからきているかというと、@riverpod
を付けたTodosクラスからきています。Providerの名前は、「riverpodを付けたクラス名をlowerキャメルケースにしたもの+Provider」になります。
watchは、これが変更されたら、ウィジットを更新するという意味になります。
どのウィジットなのかな?とか思うわけですが、このwatchが含まれているbuildメソッドが再度呼ばれるのかな?と思っています。話はそれますが、クラス内の一部の変数のみの変更を認識したいという方にはselectというものを使うことで解決できます。(参考)
あとは、
ref.read(todosProvider.notifier).toggle(todo.id)
ですかね。とりあえず、readはただ読むだけで変更を監視しないことを表します。値を読み取る側でwatchしてるから、値を変更する側ではwatchしなくていいっていうイメージでいます。(まぁ、stateを更新しているからnotifyはしてますけど...)
notifierをつけると、@riverpod
をつけたTodosクラスが返ってきて、notifierがついていない場合は、Todosクラスが管理しているstate変数が返ってきているというイメージを持てば、理解しやすくなるかなと思っています。
まとめ
RiverPod難しい...