LoginSignup
12
9

More than 1 year has passed since last update.

【Flutter】Riverpodを用いてカウンタアプリを書き換える

Last updated at Posted at 2022-02-26

はじめに

Providerの基本的な使い方を、備忘録としてまとめようと思います。
基本riverpod公式ドキュメントから、必要と思われる情報を抜き出して記述していますので、さらに詳しく知りたい方はこちらをご覧ください。
https://riverpod.dev/ja/docs/getting_started

また、consumerWidgetの書き換えに関しては、さくしんさんのudemy講座を参考にさせていただきました。
riverpodの書き換えだけでなく、テスト駆動開発や、MVVMの考え方などにも深く言及されているのでおすすめです。
https://www.udemy.com/course/riverpod/

Providerの種類

まずProviderの種類から。以下の7種類のproviderがあるが、初めのうちはstateProviderを使うことが多い。FutureProvider、StreamProviderに関してはfirebaseを使用する際によく使う。

  • Provider…定数
  • StateProvider…変数
  • ScopedProvider…出力
  • StateNotifierProvider…メソッドのついたprovider
  • FutureProvider…外部からデータを取ってきて、後で参照する時に使用
  • StreamProvider…外部からデータを引っ張ってくる時に使用

Providerを使うメリット

  • 値にアクセスしやすい。グローバルに定義されるので、バケツリレーみたいにページ間で値をやりとりしなくて済む
  • 画面更新の最適化ができる。更新範囲を決めることができるので、描画にかかる時間を減らすことができる。
  • いくつかの値を組み合わせて、別の値として定義しやすい。

Providerの始め方

とりあえずグローバル変数として定義して、いろんなファイルから呼び出すのがproviderの主な使い方。
どのproviderも「ref」というオブジェクトを引数にして受け取る。一般的に、refはウィジェットからプロバイダに渡される。

final myProvider = Provider((ref) => return MyValue());

refの使い道

refには以下の3種類の使い方がある。

  • ref.watch
  • ref.listen
  • ref.read

しかし、そのうちref.readは公式では推奨されていない。

ref.read はリアクティブではないため、可能な限り使用を避けてください。
watch や listen の使用では問題が生じる場合の回避策として存在しています。 ほとんどの場面では watch や listen の使用、特に watch の使用がベターなはずです。

ref.watch

プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。

final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // `ref` を使ってプロバイダを監視する
    final counter = ref.watch(counterProvider);

    return Text('$counter');
  }
}

ただし、onTap、onPressedなど、非同期な場面で使用してはならない。

watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。

ref.listen

プロバイダの値を監視し、値が変化するたび、関数を呼び出す(主に、画面遷移するとき、ダイアログを表示するときなど)。ref.listenには2つの引数が必要。

  • 第1引数にプロバイダ
  • 第2引数に状態が変化した際に実行する関数
final anotherProvider = Provider((ref) {
  ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
    print('The counter changed $newCount');
  });
});

ref.watchと同様、非同期な場面で用いてはならない。

listen メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。

ref.read

プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、値を取得する場合に使用できる。公式では非推奨。

providerを使う際のウィジェット

通常のウィジェットでは ref を使用することができないため、ウィジェットをprovider用に書き換える必要がある。

StatelessWidgetを書き換える

StatelessWidget = ConsumerWidget と考えてもよい。
buildメソッドに第2パラメータ(WidgetRef ref)が存在する以外の違いはない。

StatefulWidgetを書き換える

StatefulWidget + State = ConsumerStatefulWidget + ConsumerState と考えてもよい。

実際に書き換えてみる

flutterで初期生成されるカウンタアプリをprovider用に書き換える。

デフォルト

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}


StatefulWidgetとStateをまとめてConsumerWidgetに変更

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

final countProvider = StateProvider<int>((ref) => 0);

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  int _counter = 0;
  void _incrementCounter() {
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
             ref.watch(countProvider).toString(),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.watch(countProvider.state).state++;
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

変更点

  • MyHomePageをConsumerWidgetに変更
  • Widget buildにWidgetRefの追加
  • MyAppをproviderScopeで囲む
  • カウンタを保存する変数_counterを、countProviderとして新たに定義
  • カウンタの表示をcountProviderから取得
  • onPressedでcountProvider.stateの値を書き換える

しかしこれだと、countProviderの値が変わるたびに、ウィジェットが更新されてしまう。
描画効率の面からもこれは避けたい。そこで、ウィジェットをstatelessWidgetに戻し、プロバイダのある箇所をConsumerで囲むことにする。これにより、画面全体ではなく、変更のあったConsumerの部分だけが再描画される。

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

final countProvider = StateProvider<int>((ref) => 0);

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Consumer (
              builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
                  Text(
               ref.watch(countProvider).toString(),
                style: Theme.of(context).textTheme.headline4,
              ),
    ),
          ],
        ),
      ),
      floatingActionButton: Consumer(
        builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
            FloatingActionButton(
          onPressed: () {
            ref.watch(countProvider.state).state++;
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

変更点

  • MyHomePageをstatelessWidgetに変更
  • Widget buildからWidgetRefの削除
  • 各プロバイダをConsumerで囲む

これでも十分だが、プロバイダのある場所をいちいちConsumerをで囲むのは面倒なのでConsumerStatefulWidgetを使用する。これにより、ウィジェット内で、Consumerで囲まずにrefを使用することができる。

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

final countProvider = StateProvider<int>((ref) => 0);

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ), Text(
               ref.watch(countProvider).toString(),
                style: Theme.of(context).textTheme.headline4,
              ),

          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: () {
            ref.watch(countProvider.state).state++;
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
    );
  }
}

変更点

  • MyHomePageをConsumerStatefulWidgetに変更
  • StateをConsumerStateに変更
  • Consumerの削除

以上、カウンタアプリをConsumerWidget、ConsumerStatefulWidgetの2種類を用いて書き換えることに成功した。

終わりに

自分も初心者なので、誤り等あれば指摘していただけるとありがたいです。
最後まで読んでいただきありがとうございました。

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9