Flutter: ウィジェットのリビルドのタイミングを整理する - プロップス & Riverpod 関連

Flutter: ウィジェットのリビルドのタイミングを整理する - プロップス & Riverpod 関連

September 10, 2022

プロップスと Riverpod に関して、ウィジェットがリビルドされるタイミングを整理しました。

TL;DR #

ウィジェットがリビルドされるタイミングは以下のどれかに該当したとき。

プロップス関連 #

  • 親からプロップスを受け取っていて、親がリビルドされたとき。
    • プロップスが変化したかは関係ない。

Riverpod 関連 #

  • そのウィジェットが Riverpod の ref.watch(stateProvider) している State が更新されたとき。
    • ref.watch(stateProvider.notifier) を参照している場合は State が更新されてもリビルドされない。

逆に、次の場合はリビルドされない #

  • 親からプロップスを受け取っていなければ、親がリビルドされても自分はリビルドされない。

検証1 #

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

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

/*
  State
*/

final myCounterStateProvider1 = StateProvider<int>((ref) => 0);
final myCounterStateProvider2 = StateProvider<int>((ref) => 0);

/*
  Widget
*/

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

  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: const [
            MyChildWidget1(),
            MyChildWidget2(),
            MyButtonWidget(),
          ],
        ),
      ),
    );
  }
}

class MyChildWidget1 extends ConsumerWidget {
  const MyChildWidget1({super.key});

  @override
  Widget build(context, ref) {
    ref.watch(myCounterStateProvider1);
    print('BUILD: MyChildWidget1');

    return Container();
  }
}

class MyChildWidget2 extends ConsumerWidget {
  const MyChildWidget2({super.key});

  @override
  Widget build(context, ref) {
    ref.watch(myCounterStateProvider2);
    print('BUILD: MyChildWidget2');

    return Container();
  }
}

class MyButtonWidget extends ConsumerWidget {
  const MyButtonWidget({super.key});

  @override
  Widget build(context, ref) {
    final myCounterStateNotifier1 = ref.watch(myCounterStateProvider1.notifier);
    final myCounterStateNotifier2 = ref.watch(myCounterStateProvider2.notifier);
    print('BUILD: MyButtonWidget');

    return Column(
      children: [
        ElevatedButton(
          onPressed: () => myCounterStateNotifier1.state++,
          child: const Text('myCounterState1 ++'),
        ),
        ElevatedButton(
          onPressed: () => myCounterStateNotifier2.state++,
          child: const Text('myCounterState2 ++'),
        ),
      ],
    );
  }
}

コンソールの出力 #

# 1. 初回画面表示
BUILD: MyApp
BUILD: MyChildWidget1
BUILD: MyChildWidget2
BUILD: MyButtonWidget

# 2. myCounterState1 ++ ボタンを押す
BUILD: MyChildWidget1

# 3. myCounterState1 ++ ボタンを押す
BUILD: MyChildWidget1

# 4. myCounterState2 ++ ボタンを押す
BUILD: MyChildWidget2

# 5. myCounterState2 ++ ボタンを押す
BUILD: MyChildWidget2

ここから分かること #

親からプロップスを受け取っていない場合の挙動は次の通りとなる。

  • ref.watch(stateProvider) している State が更新されたらリビルドされる
  • ref.watch(stateProvider.notifier) している State が更新されてもリビルドされない

検証2 #

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

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

/*
  State
*/

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

/*
  Widget
*/

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

  @override
  Widget build(context, ref) {
    final myCounterState = ref.watch(myCounterStateProvider);
    print('BUILD: MyApp');

    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            MyChildWidget(myCounterState),
            const MyButtonWidget(),
          ],
        ),
      ),
    );
  }
}

class MyChildWidget extends StatelessWidget {
  const MyChildWidget(this.myCounterState, {super.key});

  final int myCounterState;

  @override
  Widget build(context) {
    print('BUILD: MyChildWidget');

    return MyGrandChildWidget(myCounterState);
  }
}

class MyGrandChildWidget extends StatelessWidget {
  const MyGrandChildWidget(this.myCounterState, {super.key});

  final int myCounterState;

  @override
  Widget build(context) {
    print('BUILD: MyGrandChildWidget');

    return Container();
  }
}

class MyButtonWidget extends ConsumerWidget {
  const MyButtonWidget({super.key});

  @override
  Widget build(context, ref) {
    final myCounterStateNotifier = ref.watch(myCounterStateProvider.notifier);
    print('BUILD: MyButtonWidget');

    return Column(
      children: [
        ElevatedButton(
          onPressed: () => myCounterStateNotifier.state++,
          child: const Text('myCounterState ++'),
        ),
      ],
    );
  }
}

コンソールの出力 #

# 1. 初回画面表示
BUILD: MyApp
BUILD: MyChildWidget
BUILD: MyGrandChildWidget
BUILD: MyButtonWidget

# 2. myCounterState ++ ボタンを押す
BUILD: MyApp
BUILD: MyChildWidget
BUILD: MyGrandChildWidget

# 3. myCounterState ++ ボタンを押す
BUILD: MyApp
BUILD: MyChildWidget
BUILD: MyGrandChildWidget

ここから分かること #

親がリビルドされてプロップスが変化した場合の挙動は次の通りとなる。

  • リビルドされる

検証3 #

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

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

/*
  State
*/

final myCounterStateProvider1 = StateProvider<int>((ref) => 0);
final myCounterStateProvider2 = StateProvider<int>((ref) => 0);

/*
  Widget
*/

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

  @override
  Widget build(context, ref) {
    final myCounterState1 = ref.watch(myCounterStateProvider1);
    final myCounterState2 = ref.watch(myCounterStateProvider2);
    print('BUILD: MyApp');

    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            MyChildWidget1(myCounterState1),
            MyChildWidget2(myCounterState2),
            const MyButtonWidget(),
          ],
        ),
      ),
    );
  }
}

class MyChildWidget1 extends StatelessWidget {
  const MyChildWidget1(this.myCounterState1, {super.key});

  final int myCounterState1;

  @override
  Widget build(context) {
    print('BUILD: MyChildWidget1');

    return Container();
  }
}

class MyChildWidget2 extends StatelessWidget {
  const MyChildWidget2(this.myCounterState2, {super.key});

  final int myCounterState2;

  @override
  Widget build(context) {
    print('BUILD: MyChildWidget2');

    return Container();
  }
}

class MyButtonWidget extends ConsumerWidget {
  const MyButtonWidget({super.key});

  @override
  Widget build(context, ref) {
    final myCounterStateNotifier = ref.watch(myCounterStateProvider1.notifier);
    print('BUILD: MyButtonWidget');

    return Column(
      children: [
        ElevatedButton(
          onPressed: () => myCounterStateNotifier.state++,
          child: const Text('myCounterState ++'),
        ),
      ],
    );
  }
}

コンソールの出力 #

# 1. 初回画面表示
BUILD: MyApp
BUILD: MyChildWidget1
BUILD: MyChildWidget2
BUILD: MyButtonWidget

# 2. myCounterState1 ++ ボタンを押す
BUILD: MyApp
BUILD: MyChildWidget1
BUILD: MyChildWidget2

# 3. myCounterState1 ++ ボタンを押す
BUILD: MyApp
BUILD: MyChildWidget1
BUILD: MyChildWidget2

ここから分かること #

親がリビルドされてプロップスが変化しなかった場合の挙動は次の通りとなる。

  • リビルドされる

MyChildWidget2 が受け取っているプロップス myCounterState2 は変化していないが、親がリビルドされれば MyChildWidget2 もリビルドされている。