Flutter の状態管理:setState / Provider / Riverpod をコードで見比べる

Flutter の状態管理:setState / Provider / Riverpod をコードで見比べる

March 26, 2022

サンプルコードのウィジェットの階層構造 #

App
 └ Layer1
    └ Layer2
      ├ Layer3A
      └ Layer3B

setState #

import 'package:flutter/material.dart';

void main() => runApp(const App());

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Layer1());
  }
}

class Layer1 extends StatefulWidget {
  const Layer1({Key? key}) : super(key: key);
  @override
  State<Layer1> createState() => _Layer1State();
}

class _Layer1State extends State<Layer1> {
  int _count = 0;
  void _increment() => setState(() => _count++);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Current count is $_count')),
      body: Layer2(count: _count, increment: _increment),
    );
  }
}

class Layer2 extends StatelessWidget {
  const Layer2({Key? key, required this.count, required this.increment})
      : super(key: key);

  final int count;
  final void Function() increment;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('I am a counter.'),
        Layer3A(count: count),
        Layer3B(increment: increment),
      ],
    );
  }
}

class Layer3A extends StatelessWidget {
  const Layer3A({Key? key, required this.count}) : super(key: key);

  final int count;

  @override
  Widget build(BuildContext context) {
    final String res = count % 2 == 0 ? 'Even' : 'Odd';
    return Text('Count is $res');
  }
}

class Layer3B extends StatelessWidget {
  const Layer3B({Key? key, required this.increment}) : super(key: key);

  final void Function() increment;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: increment,
      child: const Text('INCREMENT'),
    );
  }
}

Prop Drilling(バケツリレー)になっています。

Provider #

(以下のコードは provider のバージョン 6 系です)

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

void main() => runApp(const App());

class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => Counter(),
      child: const MaterialApp(home: Layer1()),
    );
  }
}

class Layer1 extends StatelessWidget {
  const Layer1({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text('Current count is ${context.watch<Counter>().count}')),
      body: const Layer2(),
    );
  }
}

class Layer2 extends StatelessWidget {
  const Layer2({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Column(
      children: const [
        Text('I am a counter.'),
        Layer3A(),
        Layer3B(),
      ],
    );
  }
}

class Layer3A extends StatelessWidget {
  const Layer3A({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final String res = context.watch<Counter>().count % 2 == 0 ? 'Even' : 'Odd';
    return Text('Count is $res');
  }
}

class Layer3B extends StatelessWidget {
  const Layer3B({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: context.read<Counter>().increment,
      child: const Text('INCREMENT'),
    );
  }
}

ChangeNotifierProvider で囲われている配下で Counter にアクセスできます。これによりプロップスの受け渡しがなくなりました。

Provider から値を取得する部分は Consumer を使ってより簡潔に記載することもできます。

See also: #

Riverpod #

(以下のコードは flutter_riverpod のバージョン 1 系です)

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

void main() => runApp(const ProviderScope(child: App()));

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

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Layer1());
  }
}

class Layer1 extends ConsumerWidget {
  const Layer1({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar:
          AppBar(title: Text('Current count is ${ref.watch(counterProvider)}')),
      body: const Layer2(),
    );
  }
}

class Layer2 extends StatelessWidget {
  const Layer2({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Column(
      children: const [
        Text('I am a counter.'),
        Layer3A(),
        Layer3B(),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final String res = ref.watch(counterProvider) % 2 == 0 ? 'Even' : 'Odd';
    return Text('Count is $res');
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return TextButton(
      onPressed: () {
        ref.read(counterProvider.notifier).update((state) => state + 1);
      },
      child: const Text('INCREMENT'),
    );
  }
}

ステートを参照するウィジェットは StatelessWidget を継承するのではなく ConsumerWidget を継承するようになりました。

ProviderScope の配下の ConsumerWidget は ref で counterProvider にアクセスできます。

See also: #

React 的に捉えると #

  • Flutter: setState = React: useState
  • Flutter: Provider = React: useContext
  • Flutter: Riverpod = React: Recoil

に似ている気がします。