skip to Main Content

I’m having a bit of a problem with setting _selectedTime in initState in Flutter. I have some variables which used to be set with late

  late DateTime _selectedDate;
  late DateTime _selectedTime;
  late DateTime _currentMonth;
  Duration _duration = const Duration(hours: 1);

and in initState I read the values from arguments and set:

@override
  void initState() {
    super.initState();

    // Initialize to the current date
    _selectedDate = DateTime.now();
    _selectedTime = DateTime.now();
    _currentMonth = DateTime(_selectedDate.year, _selectedDate.month);

    // Read arguments and set the duration
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final Map<String, dynamic> arguments =
          ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>;
      if (arguments.containsKey('reservation')) {
        final Reservation reservation =
            Reservation.fromJson(arguments['reservation']);
        setState(() {
          _selectedDate = reservation.dateFrom;
          _currentMonth = DateTime(_selectedDate.year, _selectedDate.month);
          _selectedTime = _selectedDate;

          print(_selectedTime);
          final difference =
              reservation.dateTo.difference(reservation.dateFrom);

          final minutes = difference.inMinutes;
          _duration = Duration(minutes: minutes.toInt());
        });
      }
    });
  }

It works just fine with _selectedDate and it shows correct value in calendar widget, however, for _selectedTime it still uses DateTime.now() value in TimePickerSpinner. My TimePickerSpinner is as follows:

  TimePickerSpinner(
    time: _selectedTime,
    is24HourMode: true,
    normalTextStyle: GoogleFonts.inter(
      fontSize: 21,
      color: Colors.white,
    ),
    highlightedTextStyle: GoogleFonts.inter(
      fontSize: 21,
      color: Colors.white,
    ),
    spacing: 10,
    itemHeight: 40,
    isForce2Digits: true,
    alignment: Alignment.center,
    onTimeChange: (time) {
      setState(() {
        _selectedTime = time;
      });
    },
  ),

Data I expect for _selectedTime is 2024-08-12 14:45:44.000Z, but I get 2024-08-11 14:03:44.000Z which is DateTime.now(), but when I print that value I get correct value, but I guess something is wrong with showing it in widget.

It is a bit unclear for me why specifically _selectedTime is not set correctly, but _selectedDate works just fine. Just in case someone asks, I send the data with:

onTap: () {
    Get.toNamed(Routes.stationsBookingScreenEndpoint,
        arguments: {'reservation': reservation.toJson()});
},

so there is no issue with the data that is sent with arguments.

2

Answers


  1. Chosen as BEST ANSWER

    I guess I solved the issue myself, a bit silly approach, but if it works it works. I introduced new boolean:

       bool _isInitialized = false;
    

    and inside the initState I set the values and also this new boolean:

         setState(() {
          _selectedDate = reservation.dateFrom;
          _currentMonth = DateTime(_selectedDate.year, _selectedDate.month);
          _selectedTime = reservation.dateFrom; // Set the correct time
    
          final difference =
              reservation.dateTo.difference(reservation.dateFrom);
    
          final minutes = difference.inMinutes;
          _duration = Duration(minutes: minutes.toInt());
          _isInitialized = true; // Mark initialization as done
        });
    

    and inside the return I check if the boolean is false, so

      @override
      Widget build(BuildContext context) {
        if (!_isInitialized) {
          return Center(child: CircularProgressIndicator());
        }
        return ...;
    

    Unfortunately using key did not really solve the issue, thus had to try this .


  2. I believe your code is fine. Problem is in TimePickerSpinner widget. If I’m not wrong, this widget is provided by time_picker_spinner package, so I will refer to its source code in my answer.

    To start with, TimePickerSpinner is stateful widget, meaning it has State object. Our main interest here is how this widget manages widget’s parameters update regarding its State.

    Turns out, it implemented in not best way 🙂 Here is source code, we will breakdown what happens step-by-step.

    1. TimePickerSpinner takes time parameter here. Pay attention to its nullability.
    2. This parameter, time used in initState of State object here.
    3. After initState(), there is no other places where State could react to change in time of it’s widget.

    Because of that, here is what happens:

    1. Initial build. It happens in first frame, so TimePickerSpinner provided with DateTime.now() value of time, since your addPostFrameCallback have not been called yet.
    2. It builds it’s state in initState() here with provided _selectedTime, which is DateTime.now() at the moment.
    3. For simplicity, lets not dive to deep, so first frame is over, all tree is built
    4. Callback added in your addPostFrameCallback invokes. It calculate all properties and calls setState()
    5. Key moment here. You expect TimePickerSpinner to update with new _selectedTime. But it will never happen, because State object takes widget’s time parameter only in first build. After first build, it maintains its own inner state and initState() will not be called again.

    What to do:

    I do not think (at least, for now) that you have many options here. If it were your code, I would suggest to add didUpdateWidget lifecycle callback of state, to reflect update in widget parameters. It may look like this:

    @override
    void didUpdateWidget(TimePickerSpinner oldWidget) {
      super.didUpdateWidget(oldWidget);
      if (widget.time != oldWidget.time && widget.time != null) {
        setState(() {
          currentTime = widget.time;
        });
      }
    }
    

    But since it’s 3rd party package, I do not think it is possible since _TimePickerSpinnerState is private. Maybe best way is to create a fork, modify state and use it. Or, at least, provide issue so package maintainer could fix it for you.

    Good luck!

    UPDATED

    Approach with changing Key (as suggested in comments by @Jilmar Xa) should also work because changing key will completely eliminate whole widget with its state from the tree and replace with new instance. But it has performance implications, of course, so this approach should be used carefully

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search