Home > Software engineering >  Is it possible to have separate BuildContext for two dialogs in Flutter?
Is it possible to have separate BuildContext for two dialogs in Flutter?

Time:02-17

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:

  1. opened dialog 1 - OK
  2. opened dialog 2 - OK
  3. 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.

  • Related