【Flutter】StatefulWidgetのStateのメソッドを実行する

対象者

  • FlutterでStatefulWidgetのイベントを、WidgetTreeの上のWidgetから呼び出したい人

はじめに

FlutterでWidgetを組み合わせて、GUIのレイアウトを作成します。それで、実際に動作させようとしたとき「あれ、このWidgetの中身にどうやってアクセスするんだっけ」となるときがあります。親のWidgetから子のWidgetにコールバックを渡す、状態管理を使う、などの手段はありますが、今回は「親Widgetから子StatefulWidgetのメソッドを実行する」方法を紹介します。

Widgetが震える処理については、別記事に書きます。

実施するソース

ShakeWidgetというStatefulWidgetを作成しました。対応するStateのShakeWidgetStateには、shake()というメソッドが定義されています。

final _keyShakeWidget = GlobalKey<ShakeWidgetState>();

ShakeWidget(
  key: _keyShakeWidget,
  child: FilledButton(
    onPressed: () => _keyShakeWidget.currentState?.shake(),
    child: const Text('震える'),
  ),
),

ポイントは以下の3点。

  • final _keyShakeWidget = GlobalKey<ShakeWidgetState>();
    キーをStateのジェネリックで宣言する。
    通常Stateクラスはアンダーバーをつけてプライベートクラスにしますが、今回は外から参照するためにアンダーバーをつけちゃダメですね。

  • key: _keyShakeWidget,
    宣言したキーを対応するStatefulWidgetに設定する

  • onPressed: () => _keyShakeWidget.currentState?.shake(),
    設定したキーのcurrentStateから実施したいメソッドを呼び出します。

まとめ

StatefulWidgetのStateのメソッドにアクセスする方法を紹介しました。
StatefulWidgetのStateのこういう使い方は、Formの項目チェック時にコピペでやっていたけど、改めて理屈が分かり、勉強するいい機会になりました。

参考

全ソース

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const MyHomePage(title: '押すと震えるボタン'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final _keyShakeWidget = GlobalKey<ShakeWidgetState>();
  final _keyFastShakeWidget = GlobalKey<ShakeWidgetState>();
  final _keySlowShakeWidget = GlobalKey<ShakeWidgetState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            ShakeWidget(
              key: _keyShakeWidget,
              child: FilledButton(
                onPressed: () => _keyShakeWidget.currentState?.shake(),
                child: const Text('震える'),
              ),
            ),
            ShakeWidget(
              key: _keyFastShakeWidget,
              count: 5,
              offset: 10,
              duration: const Duration(milliseconds: 300),
              child: FilledButton(
                onPressed: () => _keyFastShakeWidget.currentState?.shake(),
                child: const Text('激しく震える'),
              ),
            ),
            ShakeWidget(
              key: _keySlowShakeWidget,
              count: 2,
              offset: 50,
              duration: const Duration(milliseconds: 3000),
              child: FilledButton(
                onPressed: () => _keySlowShakeWidget.currentState?.shake(),
                child: const Text('ゆっくり震える'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ShakeWidget extends StatefulWidget {
  const ShakeWidget({
    Key? key,
    required this.child,
    this.offset = 10,
    this.count = 3,
    this.duration = const Duration(milliseconds: 300),
    this.noStop = false,
  }) : super(key: key);

  final Widget child;
  final double offset;
  final int count;
  final Duration duration;
  final bool noStop;

  @override
  ShakeWidgetState createState() => ShakeWidgetState();
}

class ShakeWidgetState extends State<ShakeWidget>
    with SingleTickerProviderStateMixin {
  late final _animationController =
      AnimationController(vsync: this, duration: widget.duration);

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      child: widget.child,
      builder: (context, child) {
        final sineValue =
            sin(widget.count * 2 * pi * _animationController.value);
        return Transform.translate(
          offset: Offset(sineValue * widget.offset, 0),
          child: child,
        );
      },
    );
  }

  void shake() {
    _animationController
        .forward()
        .whenComplete(() => _animationController.reset());
  }
}