【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. 【Widget of the Week】#3 Wrap

  2. flutter

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

  3. flutter

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

  4. 【Widget of the Week】#11 SliverAppBa…

  5. flutter

    【Flutter】KotlinでNative側のCounterを実装し…

  6. flutter

    【Flutter】画面が開く前にローディングを表示

最近の記事

  1. flutter
  2. VBA

制作実績一覧

  1. Checkeys