flutterdartflutter-textformfieldflutter-textinputfield

Not able to remove focus from input field


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),
                        )
                      ],
                    )),
              ),
            ],
          ),
        ),
      ),
    );
  }
}


Solution

  • 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,
        );
      }
    }