skip to Main Content

I have an Alert Dialog with a TextField with autofocus in it so when Alert Dialog is open the keyboard opens automatically. But if I press back button on Android only the keyboard closes and I need to press the Back button again in order to close the Dialog as well.
I have the desired behaviour in OnSubmitted callback of the TextField where I just use Navigator.pop(context). But I assume this works as I just close the alertDialog and the keyboard closes with it as there is no Text to fill anymore. But when I press Back button I have no way to track it in the TextField or Alert Dialog so I can’t use Navigator.pop(context).
I tried using PopScope on the Alert Dialog, Scaffold and even MaterialApp – it doesn’t register the 1st back button press, only the 2nd one. I’ve also tried using KeyboardListener in order to find the BackButton action press but I could make it work.

I assume this is due to the fact that closing the keyboard doesn’t change the focus of the TextField and can’t be caught using PopScope or other methods.
Here is how I’ve managed to make it work but I think this is way too long and this relays on the keyboard animation. If it is longer – this won’t work.

Here I make sure the window only closes once as it tries to call Navigator.pop(context) twice and it just closes the whole app page itself and makes it black.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:todo_app/utils/global_utils.dart' as globals;

class BasicWindow extends StatefulWidget {
  const BasicWindow({super.key});

  @override
  State<BasicWindow> createState() => _BasicWindowState();
}

class _BasicWindowState extends State<BasicWindow> {
  @override
  Widget build(BuildContext context) {
    //make sure the build is finished 
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      if (globals.mobile) {
        Future.delayed(const Duration(milliseconds: 400), () {
          bool noKeyboardShown =
              MediaQuery.of(context).viewInsets.bottom == 0.0;
          if (noKeyboardShown & !globals.dialogClosed) {
            globals.dialogClosed = true;
            Navigator.pop(context);
            Future.delayed(const Duration(milliseconds: 600), () {
              globals.dialogClosed = false;
            });
          }
        });
      }
    });

    return AlertDialog(
      content: TextField(
        //focusNode: _textFocus,
        onSubmitted: (value) {
          Navigator.pop(context);
        },
        
      ),
    );
  }
}

2

Answers


  1. Chosen as BEST ANSWER

    The problem was that PopScope only calls OnPopInvoked when the focus was changed due the the back button press. And if you have a TextField focused and keyboard open and press the back button it only hides the keyboard and textField still has the focus. So PopScope doesn't register this as a pop and this is why you have to press back button again in order to dismiss the dialog (or any other Widget that has a TextField as a child).

    I tried using keyboardVisibility, onBackPressed (it involved too much change to the route and the Home page as you need to use MaterialApp.route with it) but the problem was still there. In the question post I've included my previous solution that relied on the Future delay in order to check if the keyboard was open or not. And it did work with the back button but it interfered with the built in dialog behaviour and gave me errors when I tapped outside the dialog and it tried to close. Also I had to wait some time after the keyboard was closed and it looked laggy and weird.

    Then I tried using MediaQuery bottom value without the Future delay. But the issue was that it was == 0 when the keyboard only started to open and when it was closed as well. This check resulted in the dialog being closed as soon as the keyboard started to open.

    Also I've noticed that this value changed while the dialog is moved and rebuild as a result of a keyboard opening and closing. So I've made a simple check of the bottom value. If it is increasing - the keyboard is opening, and if it is decreasing the keyboard is closing and when it hits 0 the keyboard is fully closed. This method is quick compared to the previous one from the post, it doesn't interfere with the built in dialog behaviour and it is simple and doesn't change the app route structure. Here is the code:

    class _TaskInputState extends State<TaskInput> {
      double currentWindowPosition = 0.0;
      double previousWindowPosition = 0.0;
    @override
      Widget build(BuildContext context) {
        currentWindowPosition = MediaQuery.of(context).viewInsets.bottom;
        //opening of the keyboard 
        if (previousWindowPosition < currentWindowPosition) {
          print("OPENING");
        }
        //closing
        if (previousWindowPosition > currentWindowPosition) {
        //keyboard is closed
          if (currentWindowPosition == 0.0) {
            Navigator.pop(context);
          }
        }
        previousWindowPosition = currentWindowPosition;
        return AlertDialog(content: TextField)...the rest of the code
    

  2. You must use WillPopScope for this behavior.
    Refactor your code like this:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:todo_app/utils/global_utils.dart' as globals;
    
    class BasicWindow extends StatefulWidget {
      const BasicWindow({super.key});
    
      @override
      State<BasicWindow> createState() => _BasicWindowState();
    }
    
    class _BasicWindowState extends State<BasicWindow> {
      @override
      Widget build(BuildContext context) {
        return WillPopScope(
          onWillPop: () async {
            // Close the keyboard before popping the dialog
            FocusScope.of(context).unfocus();
            
            // Close the dialog
            if (!globals.dialogClosed) {
              globals.dialogClosed = true;
              Navigator.pop(context);
              // Reset the dialogClosed flag after a delay to prevent re-triggering
              Future.delayed(const Duration(milliseconds: 600), () {
                globals.dialogClosed = false;
              });
            }
    
            // Return true to allow the back action (closing the dialog)
            return false;
          },
          child: AlertDialog(
            content: TextField(
              autofocus: true,
              onSubmitted: (value) {
                Navigator.pop(context);
              },
            ),
          ),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search