[Flutter] カウンターアプリを hooks_riverpod + StateNotifier で作るチュートリアル

概要

やりたいこと

hooks_riverpod の公式 Example をコードリーディングし、機能ごとにファイル分割を行う。

題材となるプログラムは下記を参考にする。

hooks_riverpod example | Flutter package
A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.

用語

Riverpod

大したこと書いてないが前回参照。

hooks_riverpod

Riverpod のプロバイダを hooks 風に記述量を少なく書ける。

hooks_riverpod | Flutter package
A simple way to access state from anywhere in your application while robust and testable.

StateNotifier

StateNotifier は一つの immutable な状態を格納する監視可能なクラス。

Riverpod が依存する state_notifier パッケージのクラスであり、後述する StateNotifierProvider とセットで利用される。

StateNotifier class - state_notifier library - Dart API
API docs for the StateNotifier class from the state_notifier library, for the Dart programming language.

今回は StateNotifier を継承した Counter クラスを作成し、 increment メソッドでカウント部分を定義している。

この Counter は StateNotifierProvider に渡されることになる。

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

StateNotifierProvide

StateNotifierProvider は StateNotifier を監視し、公開するためのプロバイダ。

StateNotifierProvider | Riverpod
StateNotifierProvider は (Riverpod が依存する パッケージのクラス)を監視し、公開するためのプロバイダです。

今回は StateNotifier である Counter を値に持つ StateNotifierProvider を作成する。

final counterProvider = StateNotifierProvider<Counter, int>((_) => Counter());

HookConsumerWidget

HookConsumerWidget は ConsumerWidget と HookWidget の役割を併せ持つ Widget。

今回は MyHomePage メソッド内で ref オブジェクトを使用するため HookConsumerWidget から継承した。

  • counterProvider の監視 ( watch )
  • FloatingActionButton をクリックした際の値の取得と increment メソッド呼び出し
class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod counter example'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

また、build メソッド内でフック (useState) を利用できるようになるが、今回は未使用なので割愛。

チュートリアル

hooks_riverpod 公式サンプルを動かす

以下の手順でプロジェクトを作成し、hooks_riverpod を追加。

flutter create hooks_riverpod_tutorial
cd hooks_riverpod_tutorial
flutter pub add hooks_riverpod

main.dart を公式 Example に変更。

vi lib/main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

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

final counterProvider = StateNotifierProvider<Counter, int>((_) => Counter());

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod counter example'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

アプリを起動すると、いつものカウンターアプリできあがる。

flutter run

機能ごとにファイル分割

以下 4 つに分割。

  • main.dart
  • app.dart
  • widget.dart
  • provider.dart

main.dart

Root を ProviderScope で囲むところは flutter_riverpod と同じ。

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

import 'app.dart';

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

app.dart

MaterialApp を構築する部分。MyHomePage クラスを呼び出す。

import 'package:flutter/material.dart';

import 'view.dart';

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

widget.dart

ここでの役割は HookConsumerWidget による画面描画。いわゆる View 層として分割。

コードの内容は上で解説したまま。

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

import 'provider.dart';

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod counter example'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

provider.dart

ここでは Provider による状態の保持とカウントする操作をまとめた。いわゆる Model 層。

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

final counterProvider = StateNotifierProvider<Counter, int>((_) => Counter());

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

まとめ

flutter_riverpod と hooks_riverpod の書き方の違いを学べた。個人的には hooks_riverpod の書き方が好き。

コードは Github にまとめた。

GitHub - runble1/hooks_riverpod_tutorial
Contribute to runble1/hooks_riverpod_tutorial development by creating an account on GitHub.

参考

hooks_riverpod | Flutter package
A simple way to access state from anywhere in your application while robust and testable.

コメント

タイトルとURLをコピーしました