【Flutter】実践！state_notifier + freezedでGoogle Books APIを叩いてみよう【初心者向け】

# 2021/7/3 追記

[provider](https://pub.dev/packages/provider)の上位版ライブラリである[Riverpod](https://pub.dev/packages/riverpod)で書き換えたサンプルも作りました！
Null安全でも(一応)動くように修正してあります。
StateNotifierの宣言もファイル内に書くだけという簡単仕様になるなど、色々とメリットがあるため、今後はこちらで書いたほうが良さそうですね！

[https://github.com/alpha2048/riverpod_test](https://github.com/alpha2048/riverpod_test)




-----------------------

こんにちは、Flutter民です:v:

## 概要
ここ最近でFlutterで熱いパッケージとして、以下のパッケージがあります

- [freezed](https://pub.dev/packages/freezed)
  - **いわゆるdataクラス的なものをサクッと作れるパッケージ**
  - json周りのコードを自動生成してくれるのでAPIの受け皿にも使えます
- [state_notifier](https://pub.dev/packages/state_notifier)
  - **状態管理をやってくれるパッケージ**
  - 参照しやすいViewModel的なものを簡単に作れます

これらを使い、実際にGoogle Books APIを叩いて情報を表示するアプリを作りました。
(巷にはカウンターアプリのサンプルはたくさんあるのに、なんでAPI呼ぶようなサンプルはないんや・・と思いながら作りました。。

## 前提

各パッケージの細かい説明は省略しましたが、以下の記事が参考になります

- [Flutter freezedパッケージいい感じかも](https://qiita.com/_masaokb/items/19a93be6079a80ac2d85)
- [state_notifier と freezed を使って、Flutterのカウンターアプリをつくるよ](https://qiita.com/karamage/items/4b1aff984b1af7541b73)
- [FlutterのStateNotifierパターンでカウントアップアプリ作ってみた](https://qiita.com/RyoShimizu/items/a625bb96f93b759f67b8)
- [Flutter state_notifierいい感じなので使ったほうが良いですよ](https://qiita.com/_masaokb/items/fe77495db0aeba226d2a)

## Sample

[https://github.com/alpha2048/StateNotifierTest](https://github.com/alpha2048/StateNotifierTest)

↓ iOSの表示例

<img width="250" alt="スクリーンショット 2020-07-25 10.09.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99605/65d7753c-987d-c554-9eca-718d293090e2.png">


## 解説

### パッケージ類のインストール

なにはともあれパッケージ類のインストールです。
ここで[flutter_state_notifier](https://pub.dev/packages/flutter_state_notifier)と[provider](https://pub.dev/packages/provider)も一緒にインストールすることで、より快適にstate_notifierを使えるようになります。
API呼び出しを行うため、[http](https://pub.dev/packages/http)も入れます。

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.3

  state_notifier: ^0.5.0
  flutter_state_notifier: ^0.4.2
  freezed_annotation: ^0.11.0
  provider: ^4.3.1
  json_annotation: ^3.0.1
  http: ^0.12.2
  url_launcher: ^5.5.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable: ^3.3.0
  build_runner: ^1.10.0
  freezed: ^0.11.4
```

### model

#### Response
freezedで作っていきます。
作り終わったら、`flutter pub run build_runner build` を叩いてbuildしましょう。

```dart:GoogleBooksResponse.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier_test/model/GoogleBookResponse.dart';
part 'GoogleBooksResponse.freezed.dart';
part 'GoogleBooksResponse.g.dart';

@freezed
abstract class GoogleBooksResponse with _$GoogleBooksResponse {
  const factory GoogleBooksResponse({
    String kind,
    int totalItems,
    List<GoogleBookResponse> items
  }) = _GoogleBooksResponse;

  factory GoogleBooksResponse.fromJson(Map<String, dynamic> json) => _$GoogleBooksResponseFromJson(json);
}
```

#### API
httpで呼び出し、先程freezedで作ったResponseクラスでdecodeします。

```dart:GoogleBooksAPIService.dart
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;
import 'package:state_notifier_test/model/GoogleBooksResponse.dart';

Future<GoogleBooksResponse> getBooks(String keyword) async {

  var url = 'https://www.googleapis.com/books/v1/volumes?q=$keyword';
  print(url);

  final response = await http.get(url);
  if (response.statusCode == 200) {
    return GoogleBooksResponse.fromJson(convert.jsonDecode(response.body));
  } else {
    throw Exception('Failed to connect to webservice');
  }
}
```

### ViewModel

#### State

StateNotifierが管理するStateです。
こちらも作り終わったらbuildしましょう。

実際にView側で使うレスポンスの他、状態を通知するEnumを定義しています。

```dart:MainViewModelData.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier_test/model/GoogleBooksResponse.dart';

part 'MainViewModelData.freezed.dart';

enum MainViewModelState { normal, loading, error }

@freezed
abstract class MainViewModelData with _$MainViewModelData {
  const factory MainViewModelData({
    GoogleBooksResponse response,
    MainViewModelState viewModelState
  }) = _MainViewModelData;
}
```

#### StateNotifier

StateNotifierの実装です。
シンプルでわかりやすいですね。

```dart:MainViewModel.dart
import 'package:state_notifier/state_notifier.dart';
import 'package:state_notifier_test/model/GoogleBooksAPIService.dart';
import 'package:state_notifier_test/view/MainViewModelData.dart';

class MainViewModel extends StateNotifier<MainViewModelData> {
  MainViewModel(): super(MainViewModelData());

  void fetch(String keyword) {
    state = state.copyWith(viewModelState: MainViewModelState.loading);
    getBooks(keyword)
        .then((res) {
          state = state.copyWith(response: res, viewModelState: MainViewModelState.normal);
        }).catchError((_) {
          state = state.copyWith(response: null, viewModelState: MainViewModelState.error);
        });
  }
}
```

### View

#### StateNotifier生成

ViewModelを使うWidgetの親側のどこかで、StateNotifierを生成します。
いろんなWidgetで参照したい場合は、なるべく上層側で定義してあげましょう。

```dart:main.dart
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Google Book State Notifier Sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: StateNotifierProvider<MainViewModel, MainViewModelData>(
        create: (_) => MainViewModel(),
        child: MyHomePage(title: 'Google Book State Notifier Sample'),
      ),
    );
  }
}
```

#### StateNotifierのStateを読む

`context.select<Data, T>`でデータを読み出します。
`select`を使うと、データに変化があった場合に自動でrebuildしてくれます。

```dart:main.dart
final response = context.select<MainViewModelData, GoogleBooksResponse>((data) => data.response);
final state = context.select<MainViewModelData, MainViewModelState>((data) => data.viewModelState);
final List<GoogleBookResponse> bookList = response != null ? response.items : [];
```

あとは読みだしたデータを参考にViewを作っていくだけです！

## おわりに

FlutterはBLoCパターンなどアーキテクチャ周りがやや難しいな、という印象がありましたが、state_notifierを使えばMVVMパターンをスッキリ書けるようになり、個人的にはとても嬉しい進化を遂げています。
今後はstate_notifierが一般的になっていくのでしょうか？

## 参考資料
- [Flutter freezedパッケージいい感じかも](https://qiita.com/_masaokb/items/19a93be6079a80ac2d85)
- [state_notifier と freezed を使って、Flutterのカウンターアプリをつくるよ](https://qiita.com/karamage/items/4b1aff984b1af7541b73)
- [FlutterのStateNotifierパターンでカウントアップアプリ作ってみた](https://qiita.com/RyoShimizu/items/a625bb96f93b759f67b8)
- [Flutter state_notifierいい感じなので使ったほうが良いですよ](https://qiita.com/_masaokb/items/fe77495db0aeba226d2a)
- [Flutter 全部俺 Advent Calendar 2019](https://adventar.org/calendars/4140)