🦁
FlutterのOverlayって難しい
FlutterのOverlay
をご存知ですか?名前の通り、画面の上に重ねて表示することができる機能です。
Overlay.of(context).insert(OverlayEntry(builder: (context) => (ウィジェット)))
で使えます。
import "package:flutter/material.dart";
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: Text("Show notification"),
onPressed: () => Overlay.of(context).insert(
OverlayEntry(
builder: (context) => Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: [
BoxShadow(
offset: Offset.fromDirection(0),
blurRadius: 2,
spreadRadius: 0,
blurStyle: BlurStyle.normal,
),
],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"The Earth exploded",
style: Theme.of(context).textTheme.titleLarge,
),
Text(
"Please move to Mars ASAP.",
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
),
),
),
),
);
}
}
通知のように表示されましたね。では、非常に似た以下のコードを実行してみましょう。
import "package:flutter/material.dart";
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ElevatedButton(
child: Text("Show notification"),
onPressed: () => Overlay.of(context).insert(
OverlayEntry(
builder: (context) => Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: [
BoxShadow(
offset: Offset.fromDirection(0),
blurRadius: 2,
spreadRadius: 0,
blurStyle: BlurStyle.normal,
),
],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"The Earth exploded",
style: Theme.of(context).textTheme.titleLarge,
),
Text(
"Please move to Mars ASAP.",
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
),
),
),
),
),
);
}
}
══╡ EXCEPTION CAUGHT BY GESTURE ╞════════════════════════════════
The following assertion was thrown while handling a gesture:
No Overlay widget found.
Some widgets require an Overlay widget ancestor for correct
operation.
The most common way to add an Overlay to an application is to
include a MaterialApp, CupertinoApp or Navigator widget in the
runApp() call.
The context from which that widget was searching for an overlay
was:
MyApp
When the exception was thrown, this was the stack
Handler: "onTap"
Recognizer:
TapGestureRecognizer#b6745
═════════════════════════════════════════════════════════════════
ボタンを押すと、例外が発生してしまいます。これは、(おそらく)Scaffold
のcontext
が必要なのだと思います。これが少し扱いにくいことがあり、例えばこんなエラーにもなりうります(間違ってたらすみません)。
DartError: Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
うーん、dispose
した後のcontext
を使っていたからこうなるのだと思います。また、複数のページがあるアプリで使うとそのページ内で完結する場合は良いのですが、例えばフォアグラウンドの通知を受信したらオーバーレイを表示するようにするときには、少し面倒ですね。また、ページを跨いだオーバーレイの場合は大変そうです。少しレアなケースな気がしますが、中規模でこのような使い方をするときに困ってしまいます。
私は、この問題に6時間ぐらい悩みました。その結果思ったことは、Overlay
以外を使えるなら、その方が使いやすいことがある、ということです。例えば、今回の場合はSnackBar
の方が使いやすいことに気づきました。カスタマイズ性に欠けますが、Scaffold
の子でないcontext
を使えます。
import "package:flutter/material.dart";
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text("Show SnackBar"),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"The Earth exploded",
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Colors.black),
),
Text(
"Please move to Mars ASAP.",
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.black),
),
],
),
backgroundColor: Theme.of(context).colorScheme.surface,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
);
},
),
),
);
}
}
結論
SnackBar
は最強。
Discussion