Introduction
Previously, in this article, I described how to deal with RenderFlex overflowed errors.
This time, as a permanent feature, 「Overlay
I would like to use a class to display a new widget layer on top of other widgets.
By the way, currently any message displayed on the app’s home screen is displayed as a Text widget at the top of the screen.
Introduction of overlay_support package
There is a package called overlay_support that allows you to easily create toasts and in-app notifications, so I decided to use it.
https://pub.dev/packages/overlay_support
$ flutter pub add overlay_support
$ flutter pub get
Wrap AppWidget with OverlaySupport
OverlaySupport.global()
To use it in the main part of the app, main.dart
write as follows.
return OverlaySupport.global(child: MaterialApp());
Fixed message display class
This is the class that was originally created to display a Text widget at the top of the screen.
Messages can be closed using the close icon on the right, and a show dialog will open when you click on the message bar. Replace this with the current Overlay_support.
Dart
// lib/widgets/top_message_bar.dart
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';
void showCustomMessageBar({
required BuildContext context,
required String message,
required VoidCallback onClose,
required VoidCallback onTapped,
Duration? duration,
}) {
showOverlayNotification(
(context) {
return GestureDetector(
onTap: () {
onTapped();
OverlaySupportEntry.of(context)?.dismiss();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
margin: const EdgeInsets.fromLTRB(16, 60, 16, 0),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
spreadRadius: 1,
offset: Offset(0, 5),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
message,
style: const TextStyle(
fontSize: 14,
color: Colors.black,
decoration: TextDecoration.none,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
OverlaySupportEntry.of(context)?.dismiss();
onClose();
},
),
],
),
),
);
},
duration: duration,
);
}
Here is the code after the change.
duration
Make the parameter optional so that the caller can set the display period for each notification.
OverlaySupportEntry.of(context)?.dismiss();
is a method that removes the visible overlay widget (in this case the message bar). This allows overlays to be closed programmatically when the user presses the close button or taps the message bar itself.
Dart
// lib/widgets/top_message_bar.dart
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';
void showCustomMessageBar({
required BuildContext context,
required String message,
required VoidCallback onClose,
required VoidCallback onTapped,
Duration? duration, // オプショナルにして、デフォルトはnull(無期限表示)
}) {
showOverlayNotification((context) {
return GestureDetector(
onTap: () {
onTapped();
OverlaySupportEntry.of(context)?.dismiss();
},
child: Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
spreadRadius: 1,
offset: Offset(0, 5),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
message,
style: const TextStyle(fontSize: 14),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
OverlaySupportEntry.of(context)?.dismiss();
onClose();
},
),
],
),
),
);
}, duration: duration); // デフォルトではdurationはnull
}
Calling showCustomMessageBar
_HomeTabState
Add firmware check and message bar display logic to.
If you specify Duration.zero for the duration parameter, the message will be displayed permanently. It can be closed by clicking the close icon.
// lib/screens/home/home_tab.dart
@override
void initState() {
super.initState();
_checkFirmwareUpdate();
}
void _checkFirmwareUpdate() async {
final firmwareUpdate = ref.watch(firmwareUpdateProvider);
firmwareUpdate.whenData((response) {
if (response.message == "NEW_FIRMWARE_AVAILABLE") {
showCustomMessageBar(
context: context,
message: "新しいファームウェアバージョン(${response.newFirmwareVersion})がダウンロード可能です。",
onClose: () {},
onTapped: () {
showDialog(
context: context,
builder: (BuildContext context) {
return FirmwareUpdateDialog(
newVersion: response.newFirmwareVersion ?? "",
onConfirm: () {
final userNotifier = ref.read(userProvider.notifier);
userNotifier.updateUser(User(firmwareVersion: response.newFirmwareVersion));
},
onCancel: () {},
);
},
);
},
duration: Duration.zero;
);
}
});
}
However, when building this, the following error occurs.
visitChildElements()
An error means the method cannot be called because the list of child elements in the widget tree is still being updated during the build.
Overlay is a feature that dynamically adds new widgets (such as dialogs and toast notifications) on top of an independent widget tree. If you manipulate the overlay directly within the build method, it will try to insert the new widget into the widget tree before the build process is complete, so the framework will think that “operations on child elements are occurring during the build.” , causes an error.
To solve this problem, the code that manipulates the overlay must be executed after the build process is complete, and to do so, the code that manipulates the overlay must be wrapped.WidgetsBinding.instance.addPostFrameCallback
WidgetsBinding.instance.addPostFrameCallback((_) {
showOverlay(...); // Overlayを表示する関数
});
What is WidgetsBinding.instance.addPostFrameCallback?
WidgetsBinding.instance.addPostFrameCallback
is a method used within the Flutter framework to schedule a callback (subsequent processing) to be executed after a frame of screen drawing is completed. This callback is executed after the frame has finished drawing, so you can safely display the overlay.
https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addPostFrameCallback.html
However, if you call the function WidgetsBinding.instance.addPostFrameCallback
within showCustomMessageBar
, duplicate notifications will be displayed each time the widget is rebuilt. This is addPostFrameCallback
because it runs after every frame is drawn, so the notification will be triggered again every time the screen updates.
Manage state with StateProvider so that notification is displayed only once
To prevent notifications from appearing twice, you need to use flags that manage the conditions under which notifications are displayed and prevent notifications from appearing again after they have been displayed.
Also, after the user explicitly closes the notification with the “x” icon, the notification message will not be displayed even when transitioning between screens.
Use Riverpod StateProvider
to manage whether a notification is dismissed as an app-wide state.
// ユーザーが通知を閉じたかどうかを追跡するためのプロバイダー
final notificationClosedProvider = StateProvider<bool>((ref) => false);
// 通知が表示されたかどうかを追跡するためのプロバイダー
final notificationShownProvider = StateProvider<bool>((ref) => false);
class HomeTab extends ConsumerStatefulWidget {
const HomeTab({super.key});
@override
_HomeTabState createState() => _HomeTabState();
}
class _HomeTabState extends ConsumerState<HomeTab> {
@override
Widget build(BuildContext context) {
final firmwareUpdate = ref.watch(firmwareUpdateProvider);
final notificationClosed = ref.watch(notificationClosedProvider);
final notificationShown = ref.watch(notificationShownProvider);
// ファームウェア更新があり、まだ通知が閉じられておらず、通知もまだ表示されていない場合に通知を表示
if (firmwareUpdate.asData?.value.message == "NEW_FIRMWARE_AVAILABLE" &&
!notificationClosed &&
!notificationShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!notificationClosed && !notificationShown) {
showCustomMessageBar(
context: context,
message: "新しいファームウェアバージョン(${firmwareUpdate.asData?.value.newFirmwareVersion})がダウンロード可能です。",
onClose: () {
ref.read(notificationClosedProvider.notifier).state = true;
},
onTapped: () {
// ダイアログ表示のロジック
},
);
ref.read(notificationShownProvider.notifier).state = true; // 通知が表示されたことを記録
}
});
}
return Scaffold(
// UIコンポーネント
);
}
}
Operation confirmation
It is called again when the widget is rebuilt, the notification will no longer be redisplayed if it was already displayed or the user dismissed it.
Also, with this modification, we were able to move the message display from the Text widget to the Overlay class, so even if we returned the padding of the Column widget to its original 32px, which originally caused the Overflow error, the error no longer appears.
return DebugContainer(
child: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
Congratulations, congratulations♪
コメント