I want to control how I close specific dialogs in Flutter. I know if I call Navigator.of(context).pop()
it will close the latest dialog.
However my situation is that I can have two dialogs opened at the same time in different order (a -> b
or b -> a
) and I want to explicitly close one of them.
I know that showDialog
builder method provides a BuildContext
that I can reference and do Navigator.of(storedDialogContext).pop()
but that actually doesn't really help since this context shares same navigation stack.
Update: Vandan has provided useful answer. One solution is to use Overlay widget but it has its downsides, see this answer
My example is on dartpad.dev, example code:
import 'dart:async';
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> {
Completer<BuildContext>? _dialog1Completer;
Completer<BuildContext>? _dialog2Completer;
bool _opened1 = false;
bool _opened2 = false;
@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.');
});
});
});
});
}
Future<void> _openDialog1() async {
setState(() {
_opened1 = true;
});
_dialog1Completer = Completer<BuildContext>();
await showDialog(
barrierDismissible: false,
context: context,
routeSettings: const RouteSettings(name: 'dialog1'),
builder: (dialogContext) {
if (_dialog1Completer?.isCompleted == false) {
_dialog1Completer?.complete(dialogContext);
}
return CustomDialog(title: 'Dialog 1', timeout: false, onClose: _closeDialog1);
});
}
Future<void> _openDialog2() async {
setState(() {
_opened2 = true;
});
_dialog2Completer = Completer<BuildContext>();
await showDialog(
barrierDismissible: false,
context: context,
routeSettings: const RouteSettings(name: 'dialog1'),
builder: (dialogContext) {
if (_dialog2Completer?.isCompleted == false) {
_dialog2Completer?.complete(dialogContext);
}
return CustomDialog(title: 'Dialog 2', timeout: false, onClose: _closeDialog2);
});
}
Future<void> _closeDialog1() async {
final ctx = await _dialog1Completer?.future;
if (ctx == null) {
debugPrint('Could not closed dialog 1, no context.');
return;
}
Navigator.of(ctx, rootNavigator: true).pop();
setState(() {
_dialog1Completer = null;
_opened1 = false;
});
}
Future<void> _closeDialog2() async {
final ctx = await _dialog2Completer?.future;
if (ctx == null) {
debugPrint('Could not closed dialog 2, no context.');
return;
}
Navigator.of(ctx, rootNavigator: true).pop();
setState(() {
_dialog2Completer = null;
_opened2 = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
TextButton(onPressed: _openDialog1, child: const Text('Open 1')),
TextButton(onPressed: _openDialog2, child: const Text('Open 2')),
const Spacer(),
Align(
alignment: Alignment.bottomCenter,
child: Text('Opened 1? $_opened1\nOpened 2? $_opened2'),
),
],
),
),
);
}
}
class CustomDialog extends StatefulWidget {
const CustomDialog({
Key? key,
required this.timeout,
required this.title,
required this.onClose,
}) : super(key: key);
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 AlertDialog(
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'))
],
);
}
}
If you were to run this code and observe console you can see steps being printed, on step #3 you can observe unwanted behaviour:
- opened dialog 1 - OK
- opened dialog 2 - OK
- closed dialog 1 - not OK
I think I understand the problem, Navigator.of(dialogContext, rootNavigator: true)
searches for nearest navigator and then calls .pop()
method on it, removing the latest route/dialog on its stack.
I would need to remove specific dialog.
What would be the solution here? Multiple Navigator
objects?
CodePudding user response:
I highly suggest that in this case you use Overlay
in Flutter. Overlays are rendered independently of widgets on the screen and have their own lifetimes. They appear when you ask them to and you can control when and which one of them should disappear at which time.