Reputation: 20617
Steps to reproduce:
Copy paste the below code in DartPad.dev/flutter
Hit run
Click the Do Api Call
button
you should see two popups, one below and one above
After 5 seconds, the one below is desired to close not the one above, instead, the one above closes
How to close the one below and leave the one above open ?
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CloseSpecificDialog(),
),
),
);
}
}
class CloseSpecificDialog extends StatefulWidget {
@override
_CloseSpecificDialogState createState() => _CloseSpecificDialogState();
}
class _CloseSpecificDialogState extends State<CloseSpecificDialog> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Do API call'),
onPressed: () async {
showDialogBelow();
showDialogAbove();
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
},
)),
);
}
void showDialogBelow() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
width: 350.0,
height: 150.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am below (you should not see this after 5 sec)'),
],
),
),
),
);
});
}
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
/// This should close the dialog below not the one above
void closeDialogBelowNotAbove() {
Navigator.of(context).pop();
}
}
Upvotes: 0
Views: 1972
Reputation: 2644
I had a similar requirement for my applications and had to spend quite some time to figure out the approach.
First I will tell you what advice I've got/read online which did not work for me:
BuildContext
of each dialog from builder function when calling showDialog
Navigator.pop(context, rootNavigator: true)
removeRoute
method on Navigator
None of these worked. #1 and #2 are a no-go because pop
method can only remove the latest route/dialog on the navigation stack, so you can't really remove dialog that is placed below other dialog.
#3 was something I was hoping would work but ultimately it did not work for me. I tried creating enclosing Navigator
for specific widget where I'm displaying the dialogs but pushing dialog as new route caused dialog being treated as page.
This is not a perfect solution but Overlay widget is actually used internally by other Flutter widgets, including Navigator
. It allows you to control what gets placed in which order so it also means you can decide which element on overlay to remove!
My approach was to create a StatefulWidget
which would contain a Stack
. This stack would render whatever else passed to it and also Overlay
widget. This widget would also hold references to OverlayEntry
which are basically identifiers for dialogs themselves.
I'd use GlobalKey
to reference the Overlay
's state and then insert and remove dialogs (OverlayEntry
) as I wished.
There is a disadvantage to this though:
You can find interactive example on this dartpad or you can see the code here:
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<OverlayState> _overlay = GlobalKey<OverlayState>();
OverlayEntry? _dialog1;
OverlayEntry? _dialog2;
@override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
_openDialog1();
debugPrint('Opened dialog 1. Dialog should read: "Dialog 1"');
Timer(const Duration(seconds: 2), () {
_openDialog2();
debugPrint('Opened dialog 2. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 3), () {
_closeDialog1();
debugPrint('Closed dialog 1. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 5), () {
_closeDialog2();
debugPrint('Closed dialog 2. You should not see any dialog at all.');
});
});
});
});
}
@override
void dispose() {
_closeDialog1();
_closeDialog2();
super.dispose();
}
Future<void> _openDialog1() async {
_dialog1 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 1', timeout: false, onClose: _closeDialog1));
setState(() {
_overlay.currentState?.insert(_dialog1!);
});
}
Future<void> _openDialog2() async {
_dialog2 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 2', timeout: false, onClose: _closeDialog2));
setState(() {
_overlay.currentState?.insert(_dialog2!);
});
}
Future<void> _closeDialog1() async {
setState(() {
_dialog1?.remove();
_dialog1 = null;
});
}
Future<void> _closeDialog2() async {
setState(() {
_dialog2?.remove();
_dialog2 = null;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: <Widget>[
Align(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
TextButton(onPressed: _openDialog1, child: const Text('Open 1')),
TextButton(onPressed: _openDialog2, child: const Text('Open 2')),
])),
Align(
alignment: Alignment.bottomCenter,
child: Text(
'Opened 1? ${_dialog1 != null}\nOpened 2? ${_dialog2 != null}'),
),
Overlay(key: _overlay),
],
),
);
}
}
class CustomDialog extends StatefulWidget {
const CustomDialog({
Key? key,
required this.timeout,
required this.title,
required this.onClose,
}) : super(key: key);
final String id;
final bool timeout;
final String title;
final void Function() onClose;
@override
createState() => _CustomDialogState();
}
class _CustomDialogState extends State<CustomDialog>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;
Duration? _elapsed;
final Duration _closeIn = const Duration(seconds: 5);
late final Timer? _timer;
@override
void initState() {
super.initState();
_timer = widget.timeout ? Timer(_closeIn, widget.onClose) : null;
_ticker = createTicker((elapsed) {
setState(() {
_elapsed = elapsed;
});
});
_ticker.start();
}
@override
void dispose() {
_ticker.dispose();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(children: [
GestureDetector(
onTap: widget.onClose,
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height)),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AlertDialog(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(widget.title),
content: SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text([
'${_elapsed?.inMilliseconds ?? 0.0}',
if (widget.timeout) ' / ${_closeIn.inMilliseconds}',
].join('')))),
actions: [
TextButton(
onPressed: widget.onClose, child: const Text('Close'))
],
)),
]));
}
}
In my example you can see that when the app runs, it will start up Timer
which will fire other timers. This only demonstrates that you are able to close/open specific dialogs programatically. Feel free to comment out initState
method if you don't want this.
1: Since this solution does not use Navigator
at all, you can't use WillPopScope
to detect back button press. It's a shame, it'd be great if Flutter had a way to attach listener to back button press.
2: showDialog
method does lot for you and you basically have to re-implement what it does within your own code.
Upvotes: 2
Reputation: 3305
Popping will remove route which is added the latest, and showDialog
just pushes a new route with dialogue
you can directly use the Dialog
widgets in a Stack and manage the state using a boolean variable To Achieve same the effect,
class _MyHomePageState extends State<MyHomePage> {
bool showBelow = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await Future.delayed(Duration(seconds: 5));
setState(() {
showBelow = false;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
if(showBelow) AlertDialog(
title: Text('Below..'),
content: Text('Beyond'),
),
AlertDialog(
title: Text('Above..'),
),
],
),
);
}
}
Upvotes: 0
Reputation: 34270
Remove
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
Add Future.delayed
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
Future.delayed(Duration(seconds: 5), () {
closeDialogBelowNotAbove();
});
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
Note: Navigator.pop() method always pop above alert/widget available on the screen, as it works with BuildContext
which widget currently has.
Upvotes: -1