【Flutter】状態管理(setState,Provider,Riverpod)

flutter

1. 概要

上記記事ではSqliteを使い、データを保存しながら状態を管理する内容を書きました。今回はStatelessWidgetを使った場合の状態管理について書きます。

※今回は下記3つの例で、すべて同じ結果になるように構成してます。

  • setState()
    • StatefulWidget
      • build()を呼び出して状態を変化させる
  • Provider
    • StatelessWidget
      • アプリ起動中に状態が変化しない
      • ChangeNotifierProvider / ChangeNotifier
  • Riverpod
    • StatelessWidget / ConsumerWidget
      • アプリ起動中に状態が変化しない
      • ProviderScope / StateProvider

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. setState

2-1. プロジェクトを作成

2-2. ソースコード

  • main.dart
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';
import 'package:stateful_demo/views/home.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

  • views/home.dart
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';

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;

  @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,
            ),
            ElevatedButton(onPressed: plus, child: const Text('+')),
            ElevatedButton(onPressed: minus, child: const Text('-')),
          ],
        ),
      ),
    );
  }

  void plus() {
    setState(() {
      _counter++;
    });
    print('plus=$_counter');
  }

  void minus() {
    setState(() {
      _counter--;
    });
    print('minus=$_counter');
  }
}

2-3. The Stateful Widget Lifecycle

2-4. 結果

2-5. 解説

状態が変更される対象を「setState()」内に書く事で再び「build()」が呼ばれ、再描画されるため下記が一致してます。

  • ログの数値
  • 画面の数値

3. Provider

3-1. プロジェクトを作成

3-2. Dependencyのインストール

flutter pub add provider
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  provider: ^6.0.3

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

3-3. ソースコード

  • main.dart
import 'package:flutter/material.dart';
import 'package:provider_demo/views/parent.dart';

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

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

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

  • views/parent.dart
import 'package:flutter/material.dart';
import 'package:provider_demo/models/counter.dart';
import 'package:provider/provider.dart';
import 'package:provider_demo/views/child.dart';

class ParentWidget extends StatelessWidget {
  final data = CounterData();

  ParentWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider Demo'),
      ),
      body: ChangeNotifierProvider<CounterData>.value(
        value: data,
        child: const ChildWidget(),
      ),
    );
  }
}

  • views/child.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_demo/models/counter.dart';

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

  @override
  Widget build(BuildContext context) {
    final CounterData data = context.watch<CounterData>();
    return Scaffold(
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${data.counter}',
                style: Theme.of(context).textTheme.headline4,
              ),
              ElevatedButton(
                  onPressed: () {
                    data.plus();
                  },
                  child: const Text('+')),
              ElevatedButton(
                  onPressed: () {
                    data.minus();
                  },
                  child: const Text('-')),
            ]),
      ),
    );
  }
}

  • models/counter.dart
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';

class CounterData extends ChangeNotifier {
  int counter = 0;

  void plus() {
    counter++;
    print('plus=$counter');
    notifyListeners();
  }

  void minus() {
    counter--;
    print('minus=$counter');
    notifyListeners();
  }
}

3-4. 結果

3-5. 解説

状態管理する所で「ChangeNotifierProvider」を使い、状態管理の対象となるものを渡します。

状態管理の対象となるものは「ChangeNotifier」を継承し、状態の変更があった際に「notifyListeners()」を使い、監視元に知らせます。

状態管理の対象を受け取り、操作します。

  • context.watch<T>():値の取得とその変化を監視
  • context.read<T>():値の取得のみ(監視はしない)

4. Riverpod

4-1. プロジェクトを作成

4-2. Dependencyのインストール

flutter pub add flutter_riverpod
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  flutter_riverpod: ^1.0.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

4-3. ソースコード

  • main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_demo/views/home.dart';

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

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

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

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

  • views/home.dart
// ignore_for_file: avoid_print

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

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod example')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('You have pushed the button this many times:'),
          Center(
              child: Text(
            '${ref.watch(counterProvider.state).state}',
            style: Theme.of(context).textTheme.headline4,
          )),
          ElevatedButton(onPressed: () => plus(ref), child: const Text('+')),
          ElevatedButton(onPressed: () => minus(ref), child: const Text('-')),
        ],
      ),
    );
  }

  void plus(WidgetRef ref) {
    ref.read(counterProvider.state).state++;
    print('plus=${ref.read(counterProvider.state).state}');
  }

  void minus(WidgetRef ref) {
    ref.read(counterProvider.state).state--;
    print('minus=${ref.read(counterProvider.state).state}');
  }
}

4-4. 結果

4-5. 解説

「ProviderScope」を使い、状態管理の対象を指定します。

状態を管理するためのプロバイダとして「StateProvider」を使います。

プロバイダを利用するためには「ref」を取得する必要があるので「ConsumerWidget」を継承します。

  • プロバイダの利用方法 | Riverpod
    • ref.watch():プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。
    • ref.read():プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、その時点での値を取得する場合に使用できる。

5. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. flutter

    【Flutter】アプリ名やアイコンの変更とローンチスクリーンの表示

  2. flutter

    【Flutter】GoogleMapを使用したアプリの開発について P…

  3. flutter

    【Flutter】GoogleMapを使用したアプリの開発について P…

  4. flutter

    【Flutter】Flutter実装メモ  enumの拡張

  5. flutter

    【Flutter】Pankoに「どこでもあみだくじ」機能を追加

  6. flutter

    【Flutter】コード自動生成とGraphQLClientを使ってG…

最近の記事

  1. AWS
  2. flutter

制作実績一覧

  1. Checkeys