mytooyo / board_datetime_picker

Picker to select date and time for Flutter. It is both a calendar and a picker, offering a variety of options as a package.
BSD 3-Clause "New" or "Revised" License
6 stars 6 forks source link

New feature - textfield date picker widget #21

Closed matteoberla closed 2 months ago

matteoberla commented 3 months ago

Hi, i've been developing with your package and it works perfecty for mobile device. I'm now developing a web platform and your package is still working perfecty but the UX is very slow as the user can't select a date by compiling a textfield and maybe autocompile it even if all the info are not provided (like the year, minutes, seconds)

I've looked for a package that already handles all these things but i cannot find it, i've also noticed that flutter provides a date input field (https://api.flutter.dev/flutter/material/InputDatePickerFormField-class.html) but i think it's not what i was expecting, so 've tried to develop a widget that autocompile and accepts valid date/hours formats only.

I think it can be done better as i've faced many problems and i was wondering if you could take a look and maybe consider to add something similar to your package.

https://github.com/mytooyo/board_datetime_picker/assets/69200334/ed28964a-2bf4-44b2-9686-6a94e3db26b3

-For now it handles dd/mm/yyyy and HH:MM:ss, the autocompiling feature expects to receive at least dd/MM or HH, and the remaining data is automatically generated. -I was not able to set fixed length of the fields and automatically jump between the different parts of the dates so the user is forced to insert valid formats, but for now returning null if it's not valid is enough. -only digits are accepted and the date divider is always replaced by an "/" or ":" (i can input dots, slash, columns...)

I think it would be useful to add this type of widget as in many websites the date input is writable by the user using the keyboard and it's faster. it may become useful in some cases also for the mobile world as someone could prefere to let the user input the data.

Let me know what do you think about it.

P.S. i'm sorry to ask you for the second time, but i've noticed that the support of the intl package is now forced to 0.19 as i've asked last time but i've encoutered many versioning problems with other packages that expects the 0.18 and the developers are not going to update it for me. is it possible to support both 0.18 and 0.19 for you? Thanks

mytooyo commented 3 months ago

@matteoberla

Thank you for the feature suggestion.

As an implementation image, is this correct?

For autocompletion, it would be better to reflect autocompletion in the TextField at the timing when the focus is lost, rather than during input. However, the callback at the time of TextField change detection assumes the value after autocompletion for UX.

We will respond to the package version as we think it is possible.

matteoberla commented 3 months ago

First of all thanks for the response.

-When a custom TextField is focused, the Picker is displayed from below (focus is maintained). Not mandatory but very cool feature if it's possible.

-When the value of the TextField is changed, the Picker will also change in conjunction with the TextField. this would be perfect

-Return null for dates that are not valid null value of course and also show a red border or something else to the user.

-Allow specifying input format (e.g., dd/mm) yes, i think that yyyy-MM-dd, yyyy/MM/dd, dd/MM/yyyy (the divider could change based on the locale i think, in italy we always use / but idk if it's used worldwide). I've encoutered the case where the user insert 2/3 and shoud become 02/03 automatically. Also, add a customizable hint label as here in italy to indicate dd/mm/yyyy i would use gg/mm/aaaa or maybe let it empty, depends on cases.

Only numbers and delimiters ("/", ":") can be entered I think that the user could also insert "," "-" "." ";" but all of these should be replaced with the correct divider.

For autocompletion, make it after textfield focus lost is perfect so the user sees that the input was accepted.

mytooyo commented 3 months ago

OK, I understand the overview of the feature. I have a feeling it will be a substantial feature and will take some time. Please wait until we finish implementing the new features!

matteoberla commented 3 months ago

Yes I understand that it's a big feature and i'll wait for your news.

Thank you very much.

mytooyo commented 2 months ago

@matteoberla

Although it is not yet complete, I believe it will work for the functions you have in mind, so I would be happy if you could try it out and give us your feedback! As for the seconds, only hours and minutes are supported this time, as Picker itself does not support seconds.

board_datetime_picker:
  git:
    url: https://github.com/mytooyo/board_datetime_picker
    ref: textfield
SizedBox(
  width: 160,
  child: BoardDateTimeInputField(
    pickerType: DateTimePickerType.datetime,
    options: const BoardDateTimeOptions(
      languages: BoardPickerLanguages.it(),
      pickerFormat: PickerFormat.dmy,
    ),
    textStyle: Theme.of(context).textTheme.bodyMedium,
    onChanged: (date) {
      print('onchanged: $date');
    },
  ),
);

The parameters are identical to the options and types of the existing BoardDateTimeBuilder and showBoardDateTimePicker. The input string allowed conforms to the pickerType. In addition, the necessary items can be specified in the TextFormField of the Flutter standard, and the Hint section can also be specified with decoration. I am still considering how to generate error messages, but for now I am using the validator feature of the TextFormField standard.

matteoberla commented 2 months ago

Thank you very much.

I've just tested it and here are some considerations. -I would need an "on submit" function so when the user exit from the field I can verify if the inserted date/time is valid and make some others operations on it. In my version i'm currently using a Focus widget wrapped around the textfield and i'm executing a function when the focus is changed

onFocusChange: (hasFocus) {
          if (onFocusChanged != null && hasFocus == false) {
            onFocusChanged!();
          }
        },

-autocomplete the year in PickerFormat.dmy, i'm now writing 02/04 and then pressing enter, even if 02/04 is a valid date (as i'm expecting the widget to autocomplete the year to 2024) the date is not returned at the moment. I saw that pressing another "/" the year is autocompleted but i think that for the user won't be that intuitive, if you add the "on submit" you could try to execute the autocompletion at that time.

-I've the italian keyboard so in order to input ":" i've to press Maiusc + "." but at the moment the textfield is adding an "/" and is selecting the whole text. so i'm not getting how to use it manually as the time is not retrieved by the onChanged (using the picker works fine)

-replace "." "," with "/" or ":" based on the picker format. Using the italian keyboard is much faster to input ex. 02.04 than 02/04 or 10.15 than 10:15 (same reason as previous point).

-Error on input, Can you add a parameter in the widget to hide this error string and maintain the red border only?

Seems to be a very good looking starting point, thank you.

mytooyo commented 2 months ago

@matteoberla

Thanks for trying! We have received feedback and made some corrections. I believe we have fixed the feedback areas that we received with these corrections.

Error Handling Methods

BoardDateTimeInputField(
  validators: BoardDateTimeInputFieldValidators(
    // If true, a message is displayed at the bottom as in TextFormField
    showMessage: false,
    // If handling for each error is required, please do it here
    // If showMessage is false, the return value can be null

    // - Called when the format is invalid.
    onIllegalFormat: () => null,
    // - Called when outside the specified date and time range.
    onOutOfRange: () => null,
    // - Called when required checks are needed.
    onRequired: () => null,
  ),
)

I am pushing to the same branch so you can try again!

matteoberla commented 2 months ago

I'm trying now but i'm facing some issues,

i can't input two digits as the textfield is always selecting all text and overwriting my input. maybe the focus node is not working fine?

I can't find the OnFocusChanged callback and when pressing enter on time text field the time is not retrieved.

Thank you for the explaination about the error handling!

mytooyo commented 2 months ago

I'm trying now but i'm facing some issues,

i can't input two digits as the textfield is always selecting all text and overwriting my input. maybe the focus node is not working fine?

Thank you for the explaination about the error handling!

I was checking on macOS, so I didn't see the problem, but it seems to occur with Chrome. Please wait a little while while we fix it.

mytooyo commented 2 months ago

I'm trying now but i'm facing some issues, i can't input two digits as the textfield is always selecting all text and overwriting my input. maybe the focus node is not working fine? Thank you for the explaination about the error handling!

I was checking on macOS, so I didn't see the problem, but it seems to occur with Chrome. Please wait a little while while we fix it.

@matteoberla Fixed cursor position not to be incorrect in Chrome as well. Please check back when you have a moment, as it has been reflected.

Please wait a moment for OnFocusChanged.

matteoberla commented 2 months ago

DateTimePickerType.date seems to be fixed and perfecty working. I still can't retrieve the value of the time inserted using the widget with DateTimePickerType.time.

A note about OnFocusChanged: if you can, please pass the date as parameter so i can catch the current value when the user exits from the field and i can also catch null values (not valid dates) and then make some other operations by myself.

matteoberla commented 2 months ago

I'm also seeing an error in the console when closing the page where the widget is placed, i'm leaving here the exception

The following assertion was thrown while finalizing the widget tree:
Looking up a deactivated widget's ancestor is unsafe.

At this point the state of the widget's element tree is no longer stable.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

...
packages/board_datetime_picker/src/board_datetime_input_field.dart 387:16         dispose

it seems to be referring to line 387 of the package: focusNodeDebounce?.cancel();

mytooyo commented 2 months ago

@matteoberla

For time, I have prepared an onResult method, similar to what is supported below. The onChange method also returns the selected time on the day. https://github.com/mytooyo/board_datetime_picker/issues/15

The implementation of OnFocusChanged is called when the text field gets focus and when the picker is hidden because it is out of focus.

Other errors during screen transitions have also been corrected.

matteoberla commented 2 months ago

Isn't it possible to expose that onFocusChangedfunction or execute a custom function when it is called? I'm going to explain why.

-I need to be able to catch when the user cancel the whole input so i can set that field to null. -I need to know if the date inserted is not valid so i can bring back the initial data value in the textfield otherwise i don't have a callback function for when the user ends the input of the data.

Dispose Error is fixed!

By using onResult i get a new value each time I'm compiling the textform, so i don't get only the final value, but instead all the values between. It would be very useful to have just the final value, for this i was asking for a callback function called when the onFocusChanged occurs.

Let me know if it makes sense and if it's possible.

mytooyo commented 2 months ago

Would it be better to say that onFocusChanged is fine as it is now, but provide a way to change the value of a text field from the outside? For example, make the value updatable in textcontroller

Am I correct in understanding that it would be nice to have a way to notify the final value with onCompeted, rather than always receiving a callback? In that case, onFocusChanged and onCompleted would be callback functions with the same meaning, so I think one of them should be unified.

matteoberla commented 2 months ago

Sorry it was my bad, i didn't see that you already implemented onFocusChange, it's fine the way you implemented it but i've just tested it and there's something wrong.

My code at the moment has two BoardDateTimeInputField one with date and one with time, my onFocusChanged is like this

onFocusChange: (hasFocus, finalDate) {
    if (!hasFocus) {
      print("ora: $finalDate");
    }
  },

the function is called both in time and date textfields so each time i exit from a field both functions are called, but instead i expect only the field that was focused (but now not anymore) to be called and to retrieve its value.

Hope i explained correcly.

matteoberla commented 2 months ago

I've found a solution for this but i need your help.

-I've removed the call to onFocusChange -I've wrapped the TextFormField in a Focus Widget and added

onFocusChange: (hasFocus){
            checkFormat(textController.text, complete: true);
            widget.onFocusChange?.call(hasFocus, selectedDate, textController.text);
        },

It works fine but the only case where the selectedDate is not updated is when i insert an incomplete date (ex. 03 or 03/ ) in fact the validation doesn't return an error and so the selectedDate is not updated to null.

I've also returned the text so i can check if the user input was a date or if it was an empty string.

Let me know if it could work.

mytooyo commented 2 months ago

Since it needs to work with the Picker, there are some parts of it that will not work if I just wrap it in a FocusWidget. Are you saying that the onFocusChange call in the focusScopeListener is unnecessary?

Can you tell me again what functionality you want as a feature? (What callbacks would be nice to have)

matteoberla commented 2 months ago

yes the call in focusScopeListener is unnecessary as when it is called from one BoardDateTimeInputField it is called from all the textfields in the code, so for example, if i have 10 textfields all onFocusChange are fired and it's not what i wanted, instead by using the focus widget only the textfield's onFocusChange i'm compiling is fired.

But if you tell me that there could be some problems with the picker we need to find another solution.

Basically i want to compile the textfield, once finished (so i exit from that field) i want only the onFocusChange of that field to be called.

My code at the moment is:

Padding(
  padding: const EdgeInsets.all(8.0),
  child: SizedBox(
    width: 160,
    child: BoardDateTimeInputField(
      pickerType: DateTimePickerType.date,
      onFocusChange: (hasFocus, finalDate, stringInput) {
        if (!hasFocus) {
          print("data: $finalDate, $stringInput");
        }
      },
      options: const BoardDateTimeOptions(
        languages: BoardPickerLanguages.it(),
        startDayOfWeek: DateTime.monday,
        pickerFormat: PickerFormat.dmy,
        textColor: darkTextColor,
        activeColor: mainColor,
        activeTextColor: lightTextColor,
        showDateButton: false,
        pickerSubTitles: BoardDateTimeItemTitles(
            year: "Anno",
            month: "Mese",
            day: "Giorno",
            hour: "Ora",
            minute: "Minuto"),
      ),
      onChanged: (date) {},
    ),
  ),
  ),
  Padding(
  padding: const EdgeInsets.all(8.0),
  child: SizedBox(
    width: 160,
    child: BoardDateTimeInputField(
      pickerType: DateTimePickerType.time,
      delimiter: ":",
      onFocusChange: (hasFocus, finalDate, stringInput) {
        if (!hasFocus) {
          print("ora: $finalDate, $stringInput");
        }
      },
      options: const BoardDateTimeOptions(
        languages: BoardPickerLanguages.it(),
        startDayOfWeek: DateTime.monday,
        pickerFormat: PickerFormat.dmy,
        textColor: darkTextColor,
        activeColor: mainColor,
        activeTextColor: lightTextColor,
        showDateButton: false,
        pickerSubTitles: BoardDateTimeItemTitles(
            year: "Anno",
            month: "Mese",
            day: "Giorno",
            hour: "Ora",
            minute: "Minuto"),
      ),
      onChanged: (date) {},
    ),
  ),
  ),

when i exit from the first textfield both onFocusChange are called so in the console i see:

data: ......
ora: ......

but in this case i expect to only receive:

data: ......

This happens because the focus listeners of both fields are fired even if the focus state doesn't change from true to false. At the moment it is fired even if the focus state was false and it's still false in the second text field.

Maybe you can keep the focusScopeListener but fire it only when the focus state of the widget change from true to false or false to true? But i'm not sure if it can be the solution

mytooyo commented 2 months ago

@matteoberla I see.. Thank you very much! I recognized the problem. I will look for a way to fix it while working with Picker.

matteoberla commented 2 months ago

Thank you, then there's this part

It works fine but the only case where the selectedDate is not updated is when i insert an incomplete date (ex. 03 or 03/ ) in fact the validation doesn't return an error and so the selectedDate is not updated to null.

I would expect the selectedDate to be set to null in that case

mytooyo commented 2 months ago

@matteoberla

I would expect the selectedDate to be set to null in that case

I'm trying to return null, is that a problem?

I modified it to be able to control focus on a per-field basis. I also added a String type to the onFocusChanged parameter.

I also added a BoardDateTimeTextController to allow external users to update values in text fields.

final textController = BoardDateTimeTextController();
BoardDateTimeInputField(
  controller: textController,
  ...
),

textController.setText('01.01.2024');
// or
textController.setDate(DateTime.now());

Still need to sort out the source code and comments.

matteoberla commented 2 months ago

I'm trying to return null, is that a problem?

Null is fine if the date inserted is not completed and so not valid

Rest of edits seems perfect, thank you!

mytooyo commented 2 months ago

Thank you for your cooperation! I will release the next version after I have organized the source code and verified that it works.

matteoberla commented 2 months ago

i've seen that if i insert a valid date ex. 03/04/2024 and then i insert ex. 03/ the retrieved value is not updated to null, but instead the old value is returned. same when i delete the whole text. same when the date format is not valid.

mytooyo commented 2 months ago

Thanks for letting me know! I guess the error handling wasn't working properly. It's fixed now.

matteoberla commented 2 months ago

Hi, it seems to be partially fixed.

input: 10/10/2024 console: data: 2024-10-10 00:00:00.000, 10/10/2024

input: "" console: data: null, "" (All correct)

input: 10/10/2024 console: data: 2024-10-10 00:00:00.000, 10/10/2024

input: 2000 console: data: 2024-10-10 00:00:00.000, 2000 (Here there's a problem as the date is not valid but it's not returned null)

mytooyo commented 2 months ago

Hi, it seems to be partially fixed.

input: 10/10/2024 console: data: 2024-10-10 00:00:00.000, 10/10/2024

input: "" console: data: null, "" (All correct)

input: 10/10/2024 console: data: 2024-10-10 00:00:00.000, 10/10/2024

input: 2000 console: data: 2024-10-10 00:00:00.000, 2000 (Here there's a problem as the date is not valid but it's not returned null)

Thank you. I have detected some problems while checking the operation and have corrected them. I've already fixed that issue as well.

matteoberla commented 2 months ago

Now it's working perfecty.

Thank you very much for your support, what you have done it's amazing, and you are always very kind.

Thank you very much, i'll start implementing it in my final code.

mytooyo commented 2 months ago

Thank you too for the great feature suggestions and tremendous support! Thanks to you, I have successfully implemented the feature. I will release it in v1.6.0, so please incorporate the official version!

matteoberla commented 2 months ago

I'm trying to set the initial value, i've never tried before but i'm getting this: The following LateError was thrown building TypedInputDatetimeWidget: LateInitializationError: Field 'minimumDate' has not been initialized.

Even if i specify minimumDate the error is the same.

I've also tried as a workaround to call setText when the widget is initialized but text is not set, if i call setText from the press of a button is set correctly.

Am i doing something wrong?

mytooyo commented 2 months ago

I'm sorry. The initial values were processed in the wrong order. Fixed in v1.6.1.

matteoberla commented 2 months ago

Also found this

The following assertion was thrown while dispatching notifications for ValueNotifier<DateTime>:
setState() or markNeedsBuild() called during build.

This ItemWidget widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: ItemWidget-[LabeledGlobalKey<ItemWidgetState>#abde3]
  dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#e2532]]
  state: ItemWidgetState#97631(ticker inactive)
The widget which was currently being built when the offending call was made was: MediaQuery
  MediaQueryData(size: Size(1814.0, 1284.0), devicePixelRatio: 1.0, textScaler: no scaling, platformBrightness: Brightness.light, padding: EdgeInsets.zero, viewPadding: EdgeInsets.zero, viewInsets: EdgeInsets.zero, systemGestureInsets: EdgeInsets.zero, alwaysUse24HourFormat: false, accessibleNavigation: false, highContrast: false, onOffSwitchLabels: false, disableAnimations: false, invertColors: false, boldText: false, navigationMode: traditional, gestureSettings: DeviceGestureSettings(touchSlop: null), displayFeatures: [])
When the exception was thrown, this was the stack: 
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 297:3                        throw_
packages/flutter/src/widgets/framework.dart 5042:9                                                 <fn>
packages/flutter/src/widgets/framework.dart 5053:14                                                markNeedsBuild
packages/flutter/src/widgets/framework.dart 1223:5                                                 setState
packages/board_datetime_picker/src/ui/parts/item.dart 147:5                                        updateState
packages/board_datetime_picker/src/options/board_item_option.dart 255:28                           updateState
packages/board_datetime_picker/src/options/board_item_option.dart 218:5                            updateList
packages/board_datetime_picker/src/board_datetime_builder.dart 460:14                              notify
packages/flutter/src/foundation/change_notifier.dart 433:24                                        notifyListeners
packages/flutter/src/foundation/change_notifier.dart 555:5                                         set value
packages/board_datetime_picker/src/board_datetime_builder.dart 576:5                               changeDateTime
packages/board_datetime_picker/src/board_datetime_builder.dart 40:23                               changeDate
packages/board_datetime_picker/src/board_datetime_input_field.dart 935:27                          checkFormat
packages/board_datetime_picker/src/board_datetime_input_field.dart 504:7                           <fn>
packages/flutter/src/foundation/change_notifier.dart 433:24                                        notifyListeners
packages/flutter/src/foundation/change_notifier.dart 555:5                                         set value
packages/board_datetime_picker/src/board_datetime_input_field.dart 59:5                            setText

And it takes me to setText, i'm using provider and i think that when notifyListeners() is called the problem occurs

mytooyo commented 2 months ago

I could not reproduce it in my environment. Do you have a sample using Provider? I will not have time after tomorrow to investigate that issue. Please wait a few minutes.

matteoberla commented 2 months ago

i can't show you a sample as i'm working on a quite complex project. What i'm doing is using as initial value a value from provider, i'm updating that value calling notifylisteners and so the widget rebuild, when it rebuilds the setText is called and i think that at this moment the error occurs, i'm tring to investigate too

matteoberla commented 2 months ago

I've noticed also that if the textfield has something in it, if i call setText text is not overwritted

UPDATE I've noticed something strange -load the page -call setText by pressing a button which call controller.setText("12:00"); succesfully compiled text -delete all text manually -calling again settext this time nothing happens

if i exit from the page and thus rebuild the widget the first time the value is set correctly, if i repeat the process same thing happens

mytooyo commented 2 months ago

I've noticed also that if the textfield has something in it, if i call setText text is not overwritted

UPDATE

I've noticed something strange

-load the page

-call setText by pressing a button which call controller.setText("12:00"); succesfully compiled text

-delete all text manually

-calling again settext this time nothing happens

if i exit from the page and thus rebuild the widget the first time the value is set correctly, if i repeat the process same thing happens

Please try using the v1.6.2 branch for this bug. It may be resolved.

matteoberla commented 2 months ago

Ok, i don't have much time so i'll probably test it tomorrow and let you know, i'll update you if i'll find something new

matteoberla commented 2 months ago

Trying to reproduce the error, i've found this:

======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for ValueNotifier<dynamic>:
A TextEditingController was used after being disposed.

Once you have called dispose() on a TextEditingController, it can no longer be used.
When the exception was thrown, this was the stack: 
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 297:3  throw_
packages/flutter/src/foundation/change_notifier.dart 179:9                   <fn>
packages/flutter/src/foundation/change_notifier.dart 185:14                  debugAssertNotDisposed
packages/flutter/src/foundation/change_notifier.dart 412:27                  notifyListeners
packages/flutter/src/foundation/change_notifier.dart 555:5                   set value
packages/flutter/src/widgets/editable_text.dart 240:11                       set value
packages/flutter/src/widgets/editable_text.dart 225:5                        set text
packages/board_datetime_picker/src/board_datetime_input_field.dart 924:5     checkFormat
packages/board_datetime_picker/src/board_datetime_input_field.dart 512:5     [_controllerListener]
packages/flutter/src/foundation/change_notifier.dart 433:24                  notifyListeners
packages/flutter/src/foundation/change_notifier.dart 555:5                   set value
packages/board_datetime_picker/src/board_datetime_input_field.dart 941:38    checkFormat
packages/board_datetime_picker/src/board_datetime_input_field.dart 302:9     [_focusListener]
packages/flutter/src/foundation/change_notifier.dart 433:24                  notifyListeners
packages/flutter/src/widgets/focus_manager.dart 1089:5                       [_notify]
packages/flutter/src/widgets/focus_manager.dart 1794:11                      applyFocusChangesIfNeeded
dart-sdk/lib/async/zone.dart 1391:47                                         _rootRun
dart-sdk/lib/async/zone.dart 1301:19                                         run
dart-sdk/lib/async/zone.dart 1209:7                                          runGuarded
dart-sdk/lib/async/zone.dart 1249:23                                         <fn>
dart-sdk/lib/async/zone.dart 1399:13                                         _rootRun
dart-sdk/lib/async/zone.dart 1301:19                                         run
dart-sdk/lib/async/zone.dart 1209:7                                          runGuarded
dart-sdk/lib/async/zone.dart 1249:23                                         callback
dart-sdk/lib/async/schedule_microtask.dart 40:11                             _microtaskLoop
dart-sdk/lib/async/schedule_microtask.dart 49:5                              _startMicrotaskLoop
dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 181:7           <fn>
The ValueNotifier<dynamic> sending notification was: ValueNotifier<dynamic>#8789a(12:03)
====================================================================================================

This happens if you have declared the BoardDateTimeTextController inside the provider instead of declaring it before build method (declaring it before the build method doesn't return this error). It seems that the BoardDateTimeTextController even when the widget rebuilds, still has an association to the TextEditingController you are using inside the widget and while trying to change the value of the field the error occurs.

I think that yesterday's error was based on the same situation even if it wasn't the same.

Why am i declaring BoardDateTimeTextController inside provider? Actually i'm used to declare an object inside provider, and this object has the actual text value and a textController associated to that specific text. I always do like this with textcontrollers because notifyListeners() rebuild widget and sometimes it can happens that the textcontroller lose it's focus because the controller it's regenerated and so the user has to tap again on the field in order to continue compiling.

matteoberla commented 2 months ago

Please try using the v1.6.2 branch for this bug. It may be resolved.

Successfully solved, thank you.

mytooyo commented 2 months ago

This happens if you have declared the BoardDateTimeTextController inside the provider instead of declaring it before build method (declaring it before the build method doesn't return this error). It seems that the BoardDateTimeTextController even when the widget rebuilds, still has an association to the TextEditingController you are using inside the widget and while trying to change the value of the field the error occurs.

Thank you. Please wait a moment while I investigate.

mytooyo commented 2 months ago

@matteoberla I do not know how to reproduce it. I have tried everything on my end and cannot reproduce it. I think there are various ways to implement Provider, so please let me know how to reproduce it. I can't seem to get it to reproduce with my possible implementation...

import 'package:board_datetime_picker/board_datetime_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ProviderData extends ChangeNotifier {
  DateTime date;

  final textController = BoardDateTimeTextController();

  ProviderData(this.date);

  void setDate(DateTime d) {
    date = d;
    notifyListeners();
  }
}

class ProviderParentWidget extends StatelessWidget {
  const ProviderParentWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ProviderData>(
      create: (context) => ProviderData(DateTime.now()),
      builder: (context, child) {
        final data = Provider.of<ProviderData>(context);

        return Scaffold(
          body: Align(
            alignment: Alignment.topCenter,
            child: BoardDateTimeInputField(
              controller: data.textController,
              pickerType: DateTimePickerType.datetime,
              options: const BoardDateTimeOptions(
                languages: BoardPickerLanguages.en(),
              ),
              initialDate: data.date,
              maximumDate: DateTime(2040),
              minimumDate: DateTime(1900, 1, 1),
              textStyle: Theme.of(context).textTheme.bodyMedium,
              onChanged: (date) {},
              onFocusChange: (val, date, text) {
                if (date != null) {
                  data.setDate(date);
                }
              },
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              data.textController.setDate(DateTime.now());
            },
          ),
        );
      },
    );
  }
}
matteoberla commented 2 months ago

Hi, I’ll try on Monday as I’m not in my office and let you know, i’ll try to modify your code to replicate the error case. Thank you for now.

Il giorno sab 6 apr 2024 alle 13:26 mytooyo @.***> ha scritto:

@matteoberla https://github.com/matteoberla I do not know how to reproduce it. I have tried everything on my end and cannot reproduce it. I think there are various ways to implement Provider, so please let me know how to reproduce it. I can't seem to get it to reproduce with my possible implementation...

import 'package:board_datetime_picker/board_datetime_picker.dart';import 'package:flutter/material.dart';import 'package:provider/provider.dart'; class ProviderData extends ChangeNotifier { DateTime date;

final textController = BoardDateTimeTextController();

ProviderData(this.date);

void setDate(DateTime d) { date = d; notifyListeners(); } } class ProviderParentWidget extends StatelessWidget { const ProviderParentWidget({super.key});

@override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => ProviderData(DateTime.now()), builder: (context, child) { final data = Provider.of(context);

    return Scaffold(
      body: Align(
        alignment: Alignment.topCenter,
        child: BoardDateTimeInputField(
          controller: data.textController,
          pickerType: DateTimePickerType.datetime,
          options: const BoardDateTimeOptions(
            languages: BoardPickerLanguages.en(),
          ),
          initialDate: data.date,
          maximumDate: DateTime(2040),
          minimumDate: DateTime(1900, 1, 1),
          textStyle: Theme.of(context).textTheme.bodyMedium,
          onChanged: (date) {},
          onFocusChange: (val, date, text) {
            if (date != null) {
              data.setDate(date);
            }
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          data.textController.setDate(DateTime.now());
        },
      ),
    );
  },
);

} }

— Reply to this email directly, view it on GitHub https://github.com/mytooyo/board_datetime_picker/issues/21#issuecomment-2041053550, or unsubscribe https://github.com/notifications/unsubscribe-auth/AQP6TTV22B7YUJQDDEU7L6LY37LVZAVCNFSM6AAAAABFGXJX36VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANBRGA2TGNJVGA . You are receiving this because you were mentioned.Message ID: @.***>

matteoberla commented 2 months ago

an error with this code:

import 'package:board_datetime_picker/board_datetime_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ProviderData extends ChangeNotifier {
  DateTime date = DateTime.now();

  final textController = BoardDateTimeTextController();

  void setDate(DateTime d) {
    date = d;
    notifyListeners();
  }
}

class ProviderParentWidget extends StatelessWidget {
  const ProviderParentWidget({super.key});

  @override
  Widget build(BuildContext context) {
    var providerData = Provider.of<ProviderData>(context, listen: true);
    return Scaffold(
      body: Align(
        alignment: Alignment.topCenter,
        child: BoardDateTimeInputField(
          controller: providerData.textController,
          pickerType: DateTimePickerType.datetime,
          options: const BoardDateTimeOptions(
            languages: BoardPickerLanguages.en(),
          ),
          initialDate: providerData.date,
          maximumDate: DateTime(2040),
          minimumDate: DateTime(1900, 1, 1),
          textStyle: Theme.of(context).textTheme.bodyMedium,
          onChanged: (date) {},
          onFocusChange: (val, date, text) {
            if (date != null) {
              providerData.setDate(date);
            }
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          providerData.textController.setDate(DateTime.now());
        },
      ),
    );
  }
}

doing this you have to wrap your MaterialApp with a MultiProvider widget and declare

ChangeNotifierProvider(
  create: (_) => ProviderData(),
),

You only get the error if: -start from another page -go inside this page and change the date -go back -return inside and try to change the date

error is:

The following assertion was thrown while dispatching notifications for ValueNotifier<dynamic>:
A TextEditingController was used after being disposed.

Once you have called dispose() on a TextEditingController, it can no longer be used.

I'll try to reproduce the other error i got last week and let you know

matteoberla commented 2 months ago

In this case the error occurs after i change a character (in my case i've deleted "0" from "12:00" and inserted "1" so it becomes "12:01") and then onFocusChange is fired.

Same changes needed as previous example

import 'package:board_datetime_picker/board_datetime_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ProviderData extends ChangeNotifier {
  DateTime start = DateTime(2024, 10, 11, 12, 0);

  DateTime datetime = DateTime(2024, 10, 11, 12, 0);
  BoardDateTimeTextController controller = BoardDateTimeTextController();

  updateDateTime(DateTime date) {
    start = date;
    //controller.setText(start.toString());
    controller.setText("${start.hour}:${start.minute}");
    datetime = start;
    //print(datetime);
    notifyListeners();
  }
}

class ProviderParentWidget extends StatelessWidget {
  const ProviderParentWidget({super.key});

  @override
  Widget build(BuildContext context) {
    var providerData = Provider.of<ProviderData>(context, listen: true);
    return Scaffold(
      body: Align(
        alignment: Alignment.topCenter,
        child: BoardDateTimeInputField(
          controller: providerData.controller,
          pickerType: DateTimePickerType.time,
          options: const BoardDateTimeOptions(
            languages: BoardPickerLanguages.en(),
          ),
          initialDate: providerData.datetime,
          maximumDate: DateTime(2040),
          minimumDate: DateTime(1900, 1, 1),
          textStyle: Theme.of(context).textTheme.bodyMedium,
          onChanged: (date) {},
          onFocusChange: (val, date, text) {
            if (date != null) {
              providerData.updateDateTime(date);
            }
          },
        ),
      ),
    );
  }
}

The error is printed as many times as i exit and enter from this page, so it seems that the textcontroller still has references to this widget and the boardController tries to update it.

error:

======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for ValueNotifier<dynamic>:
A TextEditingController was used after being disposed.

Once you have called dispose() on a TextEditingController, it can no longer be used.
matteoberla commented 2 months ago

for the other error i was facing i'll try to explain you what i was doing and the solution i've found.

I've a table with lots of rows, each row has a start time and an end time, both of them are BoardDateTimeInputField and thus the user can edit them. If start>end or end<start i'm reversing them and so both notifyListeners(), widget rebuilding and setText() happens basically at the same moment, when reversing the time picker is showed but onFocusChange() is making it disappearing, while setText() (or maybe onChanged) is tring to update the picker to the value inserted, setState is called but the widget is rebuilding so doesn't exists anymore and then the error occurs (I'm not sure but this is the only explaination i've found.

I've seen that by wrapping item.dart line 145, void updateState(Map<int, int> newMap, int newIndex) like so:

void updateState(Map<int, int> newMap, int newIndex) {
    if (!mounted) return;
    WidgetsBinding.instance.addPostFrameCallback((_){
      setState(() {
        map = newMap;
        if (selectedIndex != newIndex) {
          selectedIndex = newIndex;
          scrollController.animateToItem(
            selectedIndex,
            duration: duration,
            curve: Curves.easeIn,
          );
        }
      });
    });
  }

error doesn't occur anymore, hope it's a correct solution

matteoberla commented 2 months ago

UPDATE on textController error

seems to be fixed by adding widget.controller?._notifier.removeListener(_controllerListener); inside dispose()

I'm now investigating about onFocusChange that seems to be called 2 times when i exit from the field. _onFocused(); is called two times only if i press outside the field, while if i press enter when focused on the field works fine.

matteoberla commented 2 months ago

hi, I'm not getting why at line 301 of board_datetime_input_field.dart onFocusChanged is called only if the text is not empty

if (textController.text.isNotEmpty) {
        checkFormat(textController.text, complete: true);

        // If the focus is out of focus, but the focus has moved to another InputField
        final pf = FocusManager.instance.primaryFocus;
        if (pf is BoardDateTimeInputFocusNode) {
          closePicker();
          widget.onFocusChange?.call(false, selectedDate, textController.text);
        }
      }

I think it should be called in any case

matteoberla commented 2 months ago

hi, i've noticed that with showPicker = false the onFocusChanged callback is never called, while if set to true it's called two times

mytooyo commented 2 months ago

@matteoberla Hi, Sorry for the delay and thanks for doing the research. I have already confirmed that the error is fixed by calling _notifier.removeListener. Also, the issue of onFocusChange not being called when the picker is not displayed has been fixed.

However, I have not encountered an event where onFocusChange is called twice.