😊

【Flutter】Riverpod の特徴から導入までを一挙紹介

に公開

はじめに

みなさん、こんにちは。
Riverpod使ってますか?Flutterでの状態管理、スムーズに進んでいますか?

もしRiverpodのイメージが掴めない場合は、今回の記事が役に立つかもしれません!

今回は、Riverpod 2.0で導入されたコード生成を踏まえた最小構成サンプルを使って、その概要と基本的な書き方をご紹介します。Riverpodを理解するための第一歩になるはず!!

Riverpodの特徴

概要

  • Flutterアプリ開発で利用する状態管理パッケージの1つ
  • キャッシュによる高いパフォーマンス
  • コード生成による開発効率の向上
  • Remi Rousselet氏が作成

Flutterアプリ開発で利用する状態管理パッケージの1つ

Riverpodは、Flutterの状態管理パッケージの一つで、Riverpodを使うことで、状態を効率的に管理し、UIを自動的に更新することが可能になります。コードの可読性や保守性を高め、開発者がより効率的にアプリケーションを構築できるように設計されています。

従来の Provider パッケージの課題を解決するために開発された、より強力で柔軟な状態管理ソリューションであり、Flutter コミュニティで広く利用されています。活発な開発が続けられており、最新の Flutter の機能やベストプラクティスに追従したアップデートが頻繁に行われるため、安心して利用できます

キャッシュによる高いパフォーマンス

Riverpod は、プロバイダーが生成する状態をキャッシュすることで、データを再利用しパフォーマンスを向上させます。

状態のキャッシュと破棄、再取得は自動的に行われます。

最初にアクセスされたときに状態を生成し、その状態をキャッシュします。同じプロバイダーに再度アクセスすると、キャッシュされた状態が返されるため、同じ処理が何度も実行されるのを防ぎます。

プロバイダーを監視しているウィジェットがなくなった場合、Riverpod が自動的にそのプロバイダーの状態を破棄します。これにより、不要になった状態がメモリに残り続けるのを防ぎ、メモリリークを抑制します。

再びウィジェットからアクセスされると、プロバイダーは状態を再生成します。

コード生成による開発効率の向上

Riverpod 2.0 (2022/10)からコード生成が利用できるようになりました。このコード生成機能の導入により、Riverpod のプロバイダー定義が大幅に簡素化され、開発効率が向上しました。

Remi Rousselet氏が作成

Remi Rousselet 氏は、Riverpod 以外にも、Flutter 開発で広く利用されているパッケージを多数開発しています。Providerパッケージは Riverpod の前身となる、Flutter の状態管理を容易にするための基本的なパッケージ。RiverpodはこのProviderの課題を解決するモチベーションで開発されました。

その他にはflutter_hooksというReact Hooks の概念を Flutter に導入したパッケージや、freezedというデータクラスの生成を自動化し、ボイラープレートコードを削減するパッケージも作成しています。

どういう時に有用なパッケージなのか

概要

  • アプリ全体で状態(データ)を一元管理したい
  • 非同期処理の結果を状態管理したい

アプリ全体で状態(データ)を一元管理したい

Riverpodで作成した状態はアプリ全体からアクセス可能です。参照している状態が変更されるとそのウィジェットの表示は自動で更新されます。

状態の更新方法はプロバイダー作成時に定義するので、状態と状態を扱う処理の宣言が同一ファイル内に集約されます。これにより、状態管理に関するコードの可読性と保守性が向上します。

非同期処理の結果を状態管理したい

Riverpodは非同期処理によって得たデータを状態管理することができます。

非同期プロバイダーから提供される状態はAsyncValue型となっており自動的に「ローディング、データ、エラー」のいずれかに変化します。ウィジェットではwhenメソッドを使うことで、これらの結果に応じた画面表示を行うことができます。

このように、Riverpodの非同期プロバイダーは、非同期処理の状態の変化とUIの表示を宣言的に紐付けるための仕組みを提供しています。

さらに、非同期プロバイダーはキャッシュの機能を持ちます。これは、同じプロバイダーが複数回読み取られた場合に、非同期処理を毎回実行するのではなく、以前の結果を再利用する仕組みです。

プロバイダーを利用してるウィジェットがアンマウントされるなど、監視が一度終了するキャッシュは破棄されます。

これにより、通信を伴う非同期プロバイダーであればネットワーク負荷が軽減され、UIの応答パフォーマンスが向上します。

Riverpodの利用をサポートするツール

概要

  • Flutter Riverpod Snippets(VSCodeの拡張)
  • riverpod_lint(静的解析)
    • riverpod_lintを有効にするには、pubspec.yamlの隣にanalyze_options.yamlを追加
    • リファクタリング機能もある
  • buildをwatchモードで起動する

Flutter Riverpod Snippets(エディタの拡張機能)

Riverpodのコーディングを支援する拡張機能として「Flutter Riverpod Snippets」があります。特定のキーワードを入力することでコードの雛形を作成してくれます。

この拡張機能はVSCodeやAndroidStudio、IntelliJ IDEAで利用可能です。エディタの拡張機能のメニューから検索で探して追加すれば利用できます。

次のものは特によく使うキーワードだと思います。

キーワード 動き
riverpod 関数でのプロバイダー作成の雛形
riverpodClass クラスでのプロバイダー作成の雛形
stlessConsumer Consumerウィジェットの雛形
参考リンク集

riverpod_lint(静的解析)

コードの静的解析を行う拡張機能にriverpod_lintがあります。コード実行前に潜在的なバグやRiverpodの規約違反を検出してくれる他、リファクタリング機能も備えています。

利用するには「custom_lint」と「riverpod_lint」を追加します。

flutter pub add --dev custom_lint riverpod_lint

追加後はanalyze_options.yamlファイルにcustom_lintを有効化する設定を追記します。

analyze_options.yaml
analyzer:
  plugins:
    - custom_lint

リファクタリング機能一覧はこちらで紹介されています。

https://pub.dev/packages/riverpod_lint#all-assists

参考リンク集

利用ステップと書き方のシンプルな例

概要

  • ステップ1:パッケージを導入
  • ステップ2:riverpod_lintを有効化
  • ステップ3:main.dartでProviderScopeを設定
  • ステップ4:プロバイダーの作成
  • ステップ5:プロバイダーの使用

ステップ1:パッケージを導入

まずは必要なパッケージをプロジェクトに追加します。大きく分けてコード上で利用するもの、開発用のものを入れます。

コード上で利用する依存パッケージ

  • flutter_riverpod: Riverpodの本体
  • riverpod_annotation: Riverpod 2.0で導入されたコード生成のためのアノテーション機能

開発用の依存パッケージ

  • riverpod_generator: アノテーションを元にコード生成するツール
  • build_runner: コード生成ツールを実行するためのツール
  • custom_lint: riverpod_lint の基盤となるパッケージ
  • riverpod_lint: Riverpodの静的解析ツール

追加のためのコマンドはこちらです。プロジェクトの階層で実行します。

flutter pub add flutter_riverpod riverpod_annotation
flutter pub add --dev riverpod_generator build_runner custom_lint riverpod_lint

https://riverpod.dev/ja/docs/introduction/getting_started#installing-the-package

ステップ2:riverpod_lintを有効化

analysis_options.yamlファイル内の末尾に次の記述を追加します。

analysis_options.yaml
analyzer:
  plugins:
    - custom_lint

ステップ3:main.dartでProviderScopeを設定

main.dartファイルで ProviderScope を設定します。

Riverpodをインポートし、mainrunAppの引数にProviderScopeを記述します。ProviderScopeの引数にはウィジェットを渡します。今回は雛形に用意されていたMyAppウィジェットをそのまま指定しています。

main.dart
import 'package:flutter/material.dart';
// Riverpodをインポート
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  // runAppの引数にProviderScopeを配置
  runApp(ProviderScope(child: const MyApp()));
}

// 以下は雛形の時点で用意されたMyAppクラスなどそのまま

ステップ4:プロバイダーの作成

Riverpod2.0から導入されたアノテーションによるコード生成を使って、シンプルなプロバイダーを作成します。

まずは全体像を示します。

  1. lib/hello_provider.dartファイルを新規作成
  2. hello_provider.dart内に関数プロバイダーを作成
  3. コード生成のコマンドを実行
  4. lib/hello_provider.g.dartファイルが生成される

「2. hello_provider.dart内に関数プロバイダーを作成」で作成するファイル内容を掲載します。

hello_provider.dart
// 手順2(赤波線の上で cmd + . で補完可能)
// Riverpodをインポート:Ref型のコンパイルエラーを解消
import 'package:flutter_riverpod/flutter_riverpod.dart';
// アノテーションをインポート:@riverpodのコンパイルエラーを解消
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 手順3
// 「riverpodpart」と入力し補完でpart構文を入力
// なお「xxx.g.dart」ファイルはコード生成により用意するため、書いた直後はコンパイルエラーとなる
part 'hello_provider.g.dart';

// 手順1
// 「riverpod」と入力し補完から「Riverpod simple instance」を選択
// 補完で入力された雛形の戻り値型、関数名、戻り値を修正

String hello (Ref ref) {
  return 'hello';
}

「3. コード生成のコマンドを実行」で実行するコマンドは以下です。

dart run build_runner build

「4. lib/hello_provider.g.dartファイルが生成される」でコード生成されたことで、hello_provider.dartのpart構文のコンパイルエラーが解消されます。

ここまで終えたエディタのスクリーンショットです。

ステップ5:プロバイダーの使用

前のステップで作成したプロバイダーを使用し、プロバイダーから取得した値を画面に表示します。

まずは全体像を示します。

  1. lib/hello_box.dartファイルを新規作成
  2. lib/hello_box.dartファイル内にConsumerWidgetを作成
  3. ConsumerWidget内でプロバイダーの値を参照
  4. ConsumerWidgetを画面に表示

「2. lib/hello_box.dartファイル内にConsumerWidgetを作成」と「3. ConsumerWidget内でプロバイダーの値を参照」で作成するファイル内容を掲載します。

hello_box.dart
// 手順3(赤波線の上で cmd + . で補完可能)
// Materialをインポート:Flutterの基本ウィジェットなのでRiverpodとは直接関係しない
import 'package:flutter/material.dart';
// Riverpodをインポート:ConsumerWidgetのコンパイルエラーを解消
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 手順4-2(手順4のref.watchの引数を記述する際に補完で自動的に入力される)
// 自動生成されたプロバイダーをインポート:手順4のref.watchの引数で利用
import 'package:simple_riverpod/hello_provider.dart';

// 手順1:hello_box.dartファイルを作成
// 手順2:「stlessConsumer」と入力し補完でConsumerWidgetを生成
//    →今回はHelloBoxクラスという名前にする
class HelloBox extends ConsumerWidget {
  const HelloBox({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // 手順4-1:helloプロバイダーの値を参照
    //  →引数の「helloProvider」はコード生成で作られたもの
    final hello = ref.watch(helloProvider);

    // 手順5:プロバイダーの値を利用したウィジェットを作成
    return Scaffold(
      body: Center(
        child: Container(
          width: 100,
          height: 100,
          color: Colors.red,
          // Textの引数にプロバイダーの値を利用している
          child: Center(child: Text(hello)),
        ),
      ),
    );
  }
}

「4. ConsumerWidgetを画面に表示」ではlib/main.dartファイル内でHelloBoxウィジェットを配置するよう変更します。ConsumerWidgetの配置は通常のウィジェットと同じように書きます。

main.dart
import 'package:flutter/material.dart';
// Riverpodをインポート
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:simple_riverpod/hello_box.dart';

void main() {
  // runAppの引数にProviderScopeを配置
  runApp(ProviderScope(child: const MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      // ConsumerWidgetは通常のウィジェットと同じように配置
      home: const HelloBox(),
    );
  }
}

ここまで終えたエディタのスクリーンショットです。(hello_box.dartを表示しています)

起動した画面のスクリーンショットです。

おわりに

Riverpodは状態管理ツールであるため、実際の利用シーンではもっと沢山のプロバイダーを用意します。またプロバイダーにも種類があり、いくつかの書き方が存在します。

開発時に最小構成のコードを見るチャンスはあまりないと思いますので、ご参考にしていただければ幸いです。

https://github.com/peter-norio/flutter/tree/main/simple_riverpod

Discussion