対象者
- 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の項目チェック時にコピペでやっていたけど、改めて理屈が分かり、勉強するいい機会になりました。
参考
- Add Error Shake Effect to TextFields | Flutter
エラー時に入力欄を振動させるための記事です。ただ個人的には、StatefulWidgetのStateの使い方に注目しました。
全ソース
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());
}
}