1

I have four textfields, a title field, a details field, a date field, and a time field. Both the date and time fields are wrapped within a gesture detector, and onTap calls a pickDateAndTime method. The problem is that when I click on the date field and try to manually change the time through the input rather than the dial way, the focus goes to the title field and when I am still on the time picker and type something in the time picker, the title field gets changed with the new input. The weird part is that this error just appeared out of nowhere, and there are no errors reported in the console.

class TodoScreen extends StatefulWidget {
  final int? todoIndex;
  final int? arrayIndex;

  const TodoScreen({Key? key, this.todoIndex, this.arrayIndex})
      : super(key: key);

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final ArrayController arrayController = Get.find();
  final AuthController authController = Get.find();
  final String uid = Get.find<AuthController>().user!.uid;
  late TextEditingController _dateController;
  late TextEditingController _timeController;
  late TextEditingController titleEditingController;
  late TextEditingController detailEditingController;

  late String _setTime, _setDate;
  late String _hour, _minute, _time;
  late String dateTime;
  late bool done;

  @override
  void initState() {
    super.initState();
    String title = '';
    String detail = '';
    String date = '';
    String? time = '';

    if (widget.todoIndex != null) {
      title = arrayController
              .arrays[widget.arrayIndex!].todos![widget.todoIndex!].title ??
          '';
      detail = arrayController
              .arrays[widget.arrayIndex!].todos![widget.todoIndex!].details ??
          '';
      date = arrayController
          .arrays[widget.arrayIndex!].todos![widget.todoIndex!].date!;
      time = arrayController
          .arrays[widget.arrayIndex!].todos![widget.todoIndex!].time;
    }

    _dateController = TextEditingController(text: date);
    _timeController = TextEditingController(text: time);
    titleEditingController = TextEditingController(text: title);
    detailEditingController = TextEditingController(text: detail);
    done = (widget.todoIndex == null)
        ? false
        : arrayController
            .arrays[widget.arrayIndex!].todos![widget.todoIndex!].done!;
  }

  DateTime selectedDate = DateTime.now();
  TimeOfDay selectedTime = TimeOfDay(
      hour: (TimeOfDay.now().minute > 55)
          ? TimeOfDay.now().hour + 1
          : TimeOfDay.now().hour,
      minute: (TimeOfDay.now().minute > 55) ? 0 : TimeOfDay.now().minute + 5);

  Future<DateTime?> _selectDate() => showDatePicker(
      builder: (context, child) {
        return datePickerTheme(child);
      },
      initialEntryMode: DatePickerEntryMode.calendarOnly,
      context: context,
      initialDate: selectedDate,
      initialDatePickerMode: DatePickerMode.day,
      firstDate: DateTime.now(),
      lastDate: DateTime(DateTime.now().year + 5));

  Future<TimeOfDay?> _selectTime() => showTimePicker(
      builder: (context, child) {
        return timePickerTheme(child);
      },
      context: context,
      initialTime: selectedTime,
      initialEntryMode: TimePickerEntryMode.input);

  Future _pickDateTime() async {
    DateTime? date = await _selectDate();
    if (date == null) return;
    if (date != null) {
      selectedDate = date;
      _dateController.text = DateFormat("MM/dd/yyyy").format(selectedDate);
    }
    TimeOfDay? time = await _selectTime();
    if (time == null) {
      _timeController.text = formatDate(
          DateTime(
              DateTime.now().year,
              DateTime.now().day,
              DateTime.now().month,
              DateTime.now().hour,
              DateTime.now().minute + 5),
          [hh, ':', nn, " ", am]).toString();
    }
    if (time != null) {
      selectedTime = time;
      _hour = selectedTime.hour.toString();
      _minute = selectedTime.minute.toString();
      _time = '$_hour : $_minute';
      _timeController.text = _time;
      _timeController.text = formatDate(
          DateTime(2019, 08, 1, selectedTime.hour, selectedTime.minute),
          [hh, ':', nn, " ", am]).toString();
    }
  }

  @override
  Widget build(BuildContext context) {
    bool visible =
        (_dateController.text.isEmpty && _timeController.text.isEmpty)
            ? false
            : true;

    final formKey = GlobalKey<FormState>();

    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: Text((widget.todoIndex == null) ? 'New Task' : 'Edit Task',
            style: menuTextStyle),
        leadingWidth: (MediaQuery.of(context).size.width < 768) ? 90.0 : 100.0,
        leading: Center(
          child: Padding(
            padding: (MediaQuery.of(context).size.width < 768)
                ? const EdgeInsets.only(left: 0)
                : const EdgeInsets.only(left: 21.0),
            child: TextButton(
              style: const ButtonStyle(
                splashFactory: NoSplash.splashFactory,
              ),
              onPressed: () {
                Get.back();
              },
              child: Text(
                "Cancel",
                style: paragraphPrimary,
              ),
            ),
          ),
        ),
        centerTitle: true,
        actions: [
          Center(
            child: Padding(
              padding: (MediaQuery.of(context).size.width < 768)
                  ? const EdgeInsets.only(left: 0)
                  : const EdgeInsets.only(right: 21.0),
              child: TextButton(
                style: const ButtonStyle(
                  splashFactory: NoSplash.splashFactory,
                ),
                onPressed: () async {
                },
                child: Text((widget.todoIndex == null) ? 'Add' : 'Update',
                    style: paragraphPrimary),
              ),
            ),
          )
        ],
      ),
      body: SafeArea(
        child: Container(
          width: double.infinity,
          padding: (MediaQuery.of(context).size.width < 768)
              ? const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0)
              : const EdgeInsets.symmetric(horizontal: 35.0, vertical: 15.0),
          child: Column(
            children: [
              Form(
                key: formKey,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextFormField(
                        validator: Validator.titleValidator,
                        controller: titleEditingController,
                        autofocus: true, // problem here
                        autocorrect: false,
                        cursorColor: Colors.grey,
                        maxLines: 1,
                        maxLength: 25,
                        textInputAction: TextInputAction.next,
                        decoration: InputDecoration(
                            counterStyle: counterTextStyle,
                            hintStyle: hintTextStyle,
                            hintText: "Title",
                            border: InputBorder.none),
                        style: todoScreenStyle),
                    primaryDivider,
                    TextField(
                        controller: detailEditingController,
                        maxLines: null,
                        autocorrect: false,
                        cursorColor: Colors.grey,
                        textInputAction: TextInputAction.done,
                        decoration: InputDecoration(
                            counterStyle: counterTextStyle,
                            hintStyle: hintTextStyle,
                            hintText: "Notes",
                            border: InputBorder.none),
                        style: todoScreenDetailsStyle),
                  ],
                ),
              ),
              Visibility(
                visible: (widget.todoIndex != null) ? true : false,
                child: GestureDetector(
                  onTap: () {},
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        "Completed",
                        style: todoScreenStyle,
                      ),
                      Transform.scale(
                        scale: 1.3,
                        child: Theme(
                            data: ThemeData(
                                unselectedWidgetColor: const Color.fromARGB(
                                    255, 187, 187, 187)),
                            child: Checkbox(
                                shape: const CircleBorder(),
                                checkColor: Colors.white,
                                activeColor: primaryColor,
                                value: done,
                                side: Theme.of(context).checkboxTheme.side,
                                onChanged: (value) {
                                  setState(() {
                                    done = value!;
                                  });
                                })),
                      )
                    ],
                  ),
                ),
              ),
              GestureDetector(
                onTap: () async {
                  await _pickDateTime();
                  setState(() {
                    visible = true;
                  });
                },
                child: Column(
                  children: [
                    Row(
                      children: [
                        Flexible(
                          child: TextField(
                            enabled: false,
                            controller: _dateController,
                            onChanged: (String val) {
                              _setDate = val;
                            },
                            decoration: InputDecoration(
                                hintText: "Date",
                                hintStyle: hintTextStyle,
                                border: InputBorder.none),
                            style: todoScreenStyle,
                          ),
                        ),
                        visible
                            ? IconButton(
                                onPressed: () {
                                  _dateController.clear();
                                  _timeController.clear();
                                  setState(() {});
                                },
                                icon: const Icon(
                                  Icons.close,
                                  color: Colors.white,
                                ))
                            : Container()
                      ],
                    ),
                    primaryDivider,
                    TextField(
                      onChanged: (String val) {
                        _setTime = val;
                      },
                      enabled: false,
                      controller: _timeController,
                      decoration: InputDecoration(
                          hintText: "Time",
                          hintStyle: hintTextStyle,
                          border: InputBorder.none),
                      style: todoScreenStyle,
                    )
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Should I open an issue on Github, as I had not made any changes to the code for it behave this way and also because there were no errors in the console

Here is the full code on Github

Update

Here is a reproducible example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TodoScreen(),
    );
  }
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({Key? key}) : super(key: key);

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  late TextEditingController _dateController;
  late TextEditingController _timeController;
  late TextEditingController titleEditingController;
  late TextEditingController detailEditingController;

  late String _setTime, _setDate;
  late String _hour, _minute, _time;
  late String dateTime;

  @override
  void initState() {
    super.initState();
    String title = '';
    String detail = '';
    String date = '';
    String? time = '';

    _dateController = TextEditingController(text: date);
    _timeController = TextEditingController(text: time);
    titleEditingController = TextEditingController(text: title);
    detailEditingController = TextEditingController(text: detail);
  }

  @override
  void dispose() {
    super.dispose();
    titleEditingController.dispose();
    detailEditingController.dispose();
    _timeController.dispose();
    _dateController.dispose();
  }

  Theme timePickerTheme(child) => Theme(
        data: ThemeData.dark().copyWith(
          timePickerTheme: TimePickerThemeData(
            backgroundColor: const Color.fromARGB(255, 70, 70, 70),
            dayPeriodTextColor: Colors.green,
            hourMinuteTextColor: MaterialStateColor.resolveWith((states) =>
                states.contains(MaterialState.selected)
                    ? Colors.white
                    : Colors.white),
            dialHandColor: Colors.green,
            helpTextStyle: TextStyle(
                fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green),
            dialTextColor: MaterialStateColor.resolveWith((states) =>
                states.contains(MaterialState.selected)
                    ? Colors.white
                    : Colors.white),
            entryModeIconColor: Colors.green,
          ),
          textButtonTheme: TextButtonThemeData(
            style: ButtonStyle(
                foregroundColor:
                    MaterialStateColor.resolveWith((states) => Colors.green)),
          ),
        ),
        child: child!,
      );

  Theme datePickerTheme(child) => Theme(
        data: ThemeData.dark().copyWith(
            colorScheme: ColorScheme.dark(
          surface: Colors.green,
          secondary: Colors.green,
          onPrimary: Colors.white,
          onSurface: Colors.white,
          primary: Colors.green,
        )),
        child: child!,
      );

  DateTime selectedDate = DateTime.now();
  TimeOfDay selectedTime = TimeOfDay(
      hour: (TimeOfDay.now().minute > 55)
          ? TimeOfDay.now().hour + 1
          : TimeOfDay.now().hour,
      minute: (TimeOfDay.now().minute > 55) ? 0 : TimeOfDay.now().minute + 5);

  Future<DateTime?> _selectDate() => showDatePicker(
      builder: (context, child) {
        return datePickerTheme(child);
      },
      initialEntryMode: DatePickerEntryMode.calendarOnly,
      context: context,
      initialDate: selectedDate,
      initialDatePickerMode: DatePickerMode.day,
      firstDate: DateTime.now(),
      lastDate: DateTime(DateTime.now().year + 5));

  Future<TimeOfDay?> _selectTime() => showTimePicker(
      builder: (context, child) {
        return timePickerTheme(child);
      },
      context: context,
      initialTime: selectedTime,
      initialEntryMode: TimePickerEntryMode.input);

  Future _pickDateTime() async {
    DateTime? date = await _selectDate();
    if (date == null) return;
    if (date != null) {
      selectedDate = date;
      _dateController.text = selectedDate.toString();
    }
    TimeOfDay? time = await _selectTime();
    if (time != null) {
      selectedTime = time;
      _hour = selectedTime.hour.toString();
      _minute = selectedTime.minute.toString();
      _time = '$_hour : $_minute';
      _timeController.text = _time;
      _timeController.text =
          DateTime(2019, 08, 1, selectedTime.hour, selectedTime.minute)
              .toString();
    }
  }

  @override
  Widget build(BuildContext context) {
    bool visible =
        (_dateController.text.isEmpty && _timeController.text.isEmpty)
            ? false
            : true;

    final formKey = GlobalKey<FormState>();

    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        centerTitle: true,
      ),
      body: SafeArea(
        child: Container(
          width: double.infinity,
          padding: (MediaQuery.of(context).size.width < 768)
              ? const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0)
              : const EdgeInsets.symmetric(horizontal: 35.0, vertical: 15.0),
          child: Column(
            children: [
              Container(
                  decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(14.0)),
                  padding: const EdgeInsets.symmetric(
                      horizontal: 24.0, vertical: 15.0),
                  child: Form(
                    key: formKey,
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        TextFormField(
                          controller: titleEditingController,
                          autofocus: true,
                          autocorrect: false,
                          cursorColor: Colors.grey,
                          maxLines: 1,
                          maxLength: 25,
                          textInputAction: TextInputAction.next,
                          decoration: InputDecoration(
                              hintText: "Title", border: InputBorder.none),
                        ),
                        Divider(color: Colors.black),
                        TextField(
                          controller: detailEditingController,
                          maxLines: null,
                          autocorrect: false,
                          cursorColor: Colors.grey,
                          textInputAction: TextInputAction.done,
                          decoration: InputDecoration(
                              hintText: "Notes", border: InputBorder.none),
                        ),
                      ],
                    ),
                  )),
              GestureDetector(
                onTap: () async {
                  await _pickDateTime();
                  setState(() {
                    visible = true;
                  });
                },
                child: Container(
                    margin: const EdgeInsets.only(top: 20.0),
                    width: double.infinity,
                    padding: const EdgeInsets.symmetric(
                        horizontal: 24.0, vertical: 15.0),
                    decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(14.0)),
                    child: Column(
                      children: [
                        Row(
                          children: [
                            Flexible(
                              child: TextField(
                                enabled: false,
                                controller: _dateController,
                                onChanged: (String val) {
                                  _setDate = val;
                                },
                                decoration: InputDecoration(
                                    hintText: "Date", border: InputBorder.none),
                              ),
                            ),
                            visible
                                ? IconButton(
                                    onPressed: () {
                                      _dateController.clear();
                                      _timeController.clear();
                                      setState(() {});
                                    },
                                    icon: const Icon(
                                      Icons.close,
                                      color: Colors.white,
                                    ))
                                : Container()
                          ],
                        ),
                        Divider(
                          color: Colors.blue,
                        ),
                        TextField(
                          onChanged: (String val) {
                            _setTime = val;
                          },
                          enabled: false,
                          controller: _timeController,
                          decoration: InputDecoration(
                              hintText: "Enter", border: InputBorder.none),
                        )
                      ],
                    )),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Rohith Nambiar
  • 2,957
  • 4
  • 17
  • 37

4 Answers4

1

In your main.dart file, you should return something like this:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  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 GestureDetector(
      onTap: () {
        // This allows closing keyboard when tapping outside of a text field
        FocusScopeNode currentFocus = FocusScope.of(context);

        if (!currentFocus.hasPrimaryFocus &&
            currentFocus.focusedChild != null) {
          FocusManager.instance.primaryFocus!.unfocus();
        }
      },
      child: // your app's entry point,
    );
  }
}
Eric Aig
  • 972
  • 1
  • 13
  • 18
  • It removes the focus from the title textfield but now I am not able to focus on the time picker input – Rohith Nambiar Nov 23 '22 at 10:29
  • Ok, but looking at your code, you have `enabled: false,` both on date and time TextFields. You should either remove it or set it to `true` to allow them to accept focus as well as being able to manually modify their values. Although, this might hinder the `onTap` of `GestureDetector` that you have wrapped around them. The play here, might be to use an IconButton that executes the `_pickDateTime`. The code above is global/app wide. Which means it works for any TextField and by tapping outside it, it unfocuses whatever TextField with focus. – Eric Aig Nov 23 '22 at 12:25
  • I don't think the problem is related to the `GestureDetector` at all. Even after removing that whole widget, I am not able to dismiss the keyboard – Rohith Nambiar Nov 23 '22 at 13:55
  • Ok, but did you remove the `enabled: false` or by setting `enabled: true` from the date & time TextField widgets? – Eric Aig Nov 23 '22 at 13:57
  • Yes when I did that, on clicking on the title textfield the focus goes to date textfield – Rohith Nambiar Nov 23 '22 at 13:59
  • And tapping outside the textfield doesn't close the Keyboard (Assuming you still have `main.dart` as I indicated)? – Eric Aig Nov 23 '22 at 14:02
  • It does not close the keyboard – Rohith Nambiar Nov 23 '22 at 14:06
  • The weird thing is that the code worked fine until today morning – Rohith Nambiar Nov 23 '22 at 15:35
  • I feel your pain. You probably updated Flutter or something? I've rechecked your code again and I can't find anything strange. If you want, I can help a bit more if you can provide a minimum runnable app that I can debug. – Eric Aig Nov 24 '22 at 07:23
  • Ok, will update the question later with a reproducible example – Rohith Nambiar Nov 24 '22 at 12:47
  • I have updated the question with a runnable app – Rohith Nambiar Nov 24 '22 at 15:50
  • Unfortunately that did not work, it still behaves the same way – Rohith Nambiar Nov 24 '22 at 17:51
  • Sorry I thought you opened the first pull request, it was opened by someone else – Rohith Nambiar Nov 25 '22 at 09:26
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249884/discussion-between-eric-aig-and-rohith-nambiar). – Eric Aig Nov 25 '22 at 09:32
0

Add a focusNode in your textField:

FocusNode focusNode = FocusNode();

TextField(
    focusNode: focusNode,
);

And then in the gesture detector, add that following code to unselect the textfield input:

FocusScope.of(context).requestFocus(FocusNode());
Texv
  • 1,225
  • 10
  • 14
0

simply wrap your Scaffold widget GestureDetector and add FocusScope.of(context).requestFocus(FocusNode()); it will automatically unfocused text field when you click anywhere on your screen

GestureDetector(
        onTap: () {
          FocusScope.of(context).requestFocus(FocusNode());
        },
     child: Scaffold()
)
Mashood .H
  • 926
  • 6
  • 16
  • Did not work, even when I am not on the time picker, I am not able to close the keyboard when the focus is on the title textfield – Rohith Nambiar Nov 23 '22 at 05:00
0

You can use below code to remove focus in gesture detector event

 FocusScopeNode currentFocus = FocusScope.of(context);
    if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
      currentFocus.unfocus();
    }
niceumang
  • 1,347
  • 1
  • 8
  • 21
  • Did not work, even when I am not on the time picker, I am not able to close the keyboard when the focus is on the title textfield – Rohith Nambiar Nov 23 '22 at 05:00