[Flutter] RiverpodとHooksで簡易的な状態管理をしたい

# はじめに
Flutterの状態管理パッケージにRiverpodがあります。色々調べていたのですが、イマイチわかりづらかったので実際に簡単なTodoアプリっぽいものを作りました。その過程をメモしておきます。

# 開発物
- Listへ追加と削除を行うだけのもの
- ロジックの理解が主目的なので機能とUIは最小限

<img width="1000" alt="flutter_todo.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677573/ac8ad0bb-98bb-746c-532a-b754373194c1.gif">


# 開発環境
- Flutter 2.2.0
- Dart 2.13.0

# 画面構成

- HomeScreen
    - Listを表示する画面
- PostScreen
    - ListにItemを追加する画面

# コード
## RiverpodとHooksの導入
導入するときはRiverpodとHooksのバージョン対応に注意してください。また、uniqueなIDを生成するために```uuid```パッケージも導入しています。

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.17.0
  hooks_riverpod: ^0.14.0+4
  uuid: ^3.0.5
```
## Taskモデルの実装
Taskモデルを定義していきます。今回はDone判定の実装はしません。本格的なTodoアプリの作り方は他の方が記事をあげているのでそちらを参照してください。

```dart:task.dart
import 'package:uuid/uuid.dart';

var _uuid = Uuid();

class Task {
  Task({
    this.title,
    String? id,
  }) : id = id ?? _uuid.v4();

  final String? id;
  final String? title;
}
```

## コントローラの実装
TaskのListをコントロールするTaskListを```StateNotifier```で実装。
```Provider```の宣言もここで行っています。

```dart:task_list.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:state_notifier/state_notifier.dart';
/* models */
import './task.dart';

//Providerの宣言
final StateNotifierProvider<TaskList, dynamic> taskListProvider =
    StateNotifierProvider((ref) => TaskList([]));

class TaskList extends StateNotifier<List<Task>> {
  TaskList(List<Task> initialTask) : super(initialTask ?? []);

  //新規タスクの追加
  void addTask(String title) {
    state = [...state, Task(title: title)];
  }

  //タスクの削除
  void deleteTask(Task target) {
    state = state.where((task) => task.id != target.id).toList();
  }
}
```
タスクの追加・削除処理の詳細については省略しますが、単純にListを作り直しているだけですね。

## Task追加画面の実装
先ほど作ったTaskListをインポートします。

```dart:post_screen.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/* models */
import '../models/task_list.dart';

class PostScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String _title = '';
    return Scaffold(
      appBar: AppBar(
        title: Text("Add"),
      ),
      body: Container(
        padding: EdgeInsets.all(64),
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              onChanged: (String value) {
                _title = value;
              },
            ),
            ElevatedButton(
              onPressed: () => {
                  context.read(taskListProvider.notifier).addTask(_title),
                  Navigator.pop(context)
              },
              child: Text("add"),
            ),
          ],
        ),
      ),
    );
  }
}
```

TaskListのメソッドを呼び出すには```context.read(taskListProvider.notifier)```で```StateNotifier```を取得します。TaskListの```addTask()```を呼び出して、TextFieldから取得した値を引数に渡します。これでTaskListの状態を更新できます。

## リスト画面の実装

```dart:home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/* models */
import 'package:todo_test/models/task_list.dart';

class HomeScreen extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final taskList = useProvider(taskListProvider);
    return Center(
      child: ListView.builder(
        itemCount: taskList.length,
        itemBuilder: (context, index) {
          final task = taskList[index];
          return ListTile(
            title: Text(task.title.toString()),
            onTap: () => {
              context.read(taskListProvider.notifier).deleteTask(task)
            },
            trailing: Icon(Icons.auto_delete),
          );
        },
      ),
    );
  }
}
```

ここでHooksを活用します。```useProvider```で状態の変更をキャッチし、変数```taskList```に代入しています。また、```ListTile```をタップすると```deleteTask()```が呼び出され、Listから削除します。

## main
ここを忘れるとRiverpodを利用できないので注意してください。Riverpodを使用するにはProviderScopeで全体をwrapする必要があります。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/* screens */
import './screens/template_screen.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TemplateScreen()
    );
  }
}
```

ここで少しハマったのですが、MaterialApp直下でNavigatorを置くとエラーになりました。間にワンクッション空けて記載した方が好ましい様です。今回はTemplateScreenを実装しました。

```dart:template_screen.dart
import 'package:flutter/material.dart';
import 'package:todo_test/screens/home_screen.dart';
/* screens */
import './home_screen.dart';
import'./post_screen.dart';

class TemplateScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("List"),
      ),
      body: HomeScreen(),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => {
          Navigator.of(context).push(
            MaterialPageRoute(builder: (context) => PostScreen()),
          ),
        },
      ),
    );
  }
}
```

以上です。

## 感想
状態管理はアプリの根幹なので、理解が曖昧なら一度テストコードを作ってみた方が良さそうです。当方Flutter初心者なので至らぬ点もありますが、ご容赦ください。

## Github
今回作ったアプリはGithubに載せておきます。
Flutterのプロジェクト名とGithubのリポジトリ名が異なっているので注意してください。
https://github.com/oyachi/riverpod_test

## 参考
- https://qiita.com/hammer0802/items/0a12d129222441a3a91c
- https://zenn.dev/suba/articles/5e6139e4ae990e
- https://qiita.com/chiaki-kjwr/items/c7bf4daef21e06972d2d