Search code examples
flutterdartflutter-cupertinocupertino-widgets

CupertinoActionSheetAction onPressed to trigger a loading indicator followed by checkmark with a delay


I'm using a CupertinoModalPopup with a CupertinoActionSheet widget to display a set of options for the user to choose from.

I intend to show a circular loading indicator followed by a checkmark(tick) to give the user the impression that he has clicked the option and it is saved after popping out using Navingator.pop(context). First , I'm attempting to do this with a delay of 100milliseconds using Future.delayed after clicking onPressed using only a Widget that displays a circular loading indicator for now. But it isn't working.

Here is my code to better explain what I'm doing. Only relevant code is published.

@override
Widget build(BuildContext context) {
return CupertinoActionSheet(
        title: KNText(
          style: TextStyle(color: primaryTextColor),
          'Video sound settings',
        ),
        message: KNText(
          style: TextStyle(color: primaryTextColor),
          'Please choose from an option below :',
        ),
        actions: <Widget>[
          Container(
              color: primaryColor,
              child: CupertinoActionSheetAction(
                onPressed: ({int index = 0}) async {
                  await Future.delayed(const Duration(milliseconds: 100), () {
                    setState(() {
                      VideosSoundSetting newType =
                          allVideosSoundSettings[index];
                      widget.onTypeChanged(newType);
                      _setEnabledInProgress(true);

                      Navigator.pop(context);
                    });
                    setState(() {
                      _enabledInProgress = false;
                    });
                  });
                },

A circular loading indicator(delay : 100 milliseconds) followed by a checkmark tick(delay:50 milliseconds) will appear for a brief time after clicking on either option to give the user an impression that its a success. Currently , only circular loading indicator is configured below.

                child: Row(
                  children: [
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 153.0),
                      child: KNText(
                        style: TextStyle(color: primaryTextColor),
                        'Enabled',
                      ),
                    ),
                    _enabledInProgress
                        ? const CircularProgressIndicator()
                        : Container()
                  ],
                ),
              )),
          Container(
              color: primaryColor,
              child: CupertinoActionSheetAction(

// A circular loading indicator(delay : 100 milliseconds) followed by a checkmark tick(delay:50 milliseconds) will appear for a brief time after clicking on either option to give the user an impression that its a success. Currently , only circular loading indicator is configured below.

                onPressed: ({int index = 0}) async {
                  await Future.delayed(const Duration(milliseconds: 100), () {
                    setState(() {
                      VideosSoundSetting newType =
                          allVideosSoundSettings[index];
                      widget.onTypeChanged(newType);
                      _setEnabledInProgress(true);

                      Navigator.pop(context);
                    });
                    setState(() {
                      _enabledInProgress = false;
                    });
                  });
                },
                child: Row(
                  children: [
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 150.0),
                      child: KNText(
                        style: TextStyle(color: primaryTextColor),
                        'Disabled',
                      ),
                    ),
                    _enabledInProgress
                        ? const CircularProgressIndicator()
                        : Container()
                  ],
                ),
              )),
        ],
        cancelButton: Container(
          color: primaryColor,
          child: CupertinoActionSheetAction(
            isDefaultAction: true,
            onPressed: () {
              Navigator.pop(context);
            },
            child: const Row(children: [
              Padding(
                padding: EdgeInsets.symmetric(horizontal: 155.0),
                child: KNText(
                  style: TextStyle(
                      color: Color.fromRGBO(255, 0, 0, 1.0),
                      fontWeight: FontWeight.w700),
                  'Cancel',
                ),
              ),
            ]),
          ),
        ));
   }     
    void _setEnabledInProgress(bool enabledInProgress) {
    _enabledInProgress = enabledInProgress;
  }      

What is working :

The options and selection does work with a delay in milliseconds as specified

What doesn't :

The circular progress indicator at the very end of 'Enabled' and Disabled actions doesn't appear. After fixing the circular progress indicator with a delayed duration, I intend to call a checkmark tick widget after which the checkmark should appear and popout from the widget itself,.

If I could be pointed in the right direction of how best to achieve the above goal of

  1. Loading indicator with a delay of 100 milliseconds.
  2. Checkmark Tick appearing immediately after the loading indicator in the same place and popping out after 50 milliseconds

EDIT as on 29-04-2024

Based on the guidance received from @anqit in the comments below, I did manage to somewhat make some progress. have updated the code to reflect the changes.


Solution

  • Summary:
    The StatefulBuilder widgets may not be required, as the BuildContext may come from your custom widget. The ElevatedButton serves as an example of launching the CupertinoActionSheet, which can be substituted with whatever method you are using. Note that I didn't make use of custom widgets such as KNText. The two parts to your question to my understanding are how to show loading, and how to show a checkmark after selection and loading. Note I set the delay for loading to 1000ms, so you can see the loading longer in the example.

    Loading Summary
    The loading CircularProgressIndicator is shown by roughly following the below steps. ref _showLoading function in the code below.

    1. call showDialog
    2. have showDialog return a CircularProgressIndicator
    3. run Future.delayed (in production a network call?)
    4. call Navigator.pop to close the loading dialog

    Checkmark Summary:
    The checkmark can be shown in many ways, in this case I made use of Visibility to hide and show the checkmarks based on the values in the Map named selected in the code below. The values in selected are updated when the buttons in the modal are pressed (after loading is done). The rough process is as follows

    1. enable or disable button pressed
    2. show loading
    3. loading done
    4. update values in selected based on which button was pressed
    5. rebuild, because setState was called. When building the checkmarks are shown based on the values in selected.

      Button:
      Button

      Modal:
      Modal

      Loading:
      Loading

      Check changed after loading
      changed after loading

    Code:

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      // used to indicate which option is selected
      Map<String, bool> selected = {
        "enabled": true,
        "disabled": false,
      };
    
      // shows the loading indicator
      Future<void> _showLoading({
        required BuildContext context,
      }) async {
        // show dialog overlay with just a progress indicator
        showDialog(
          barrierDismissible: false,
          context: context,
          builder: (context) => const Center(
            child: CircularProgressIndicator(
              color: Colors.red,
            ),
          ),
        );
    
        // run delay - assume swapping for some kind of network call
        // on delay done pop loading dialog
        await Future.delayed(
          const Duration(
            milliseconds: 1000,
          ),
          () {
            Navigator.pop(
              context,
            );
          },
        );
      }
    
      runApp(
        MaterialApp(
          home: Scaffold(
            body: StatefulBuilder(
              builder: (context, setState) => Center(
                // button to trigger modal
                child: ElevatedButton(
                  child: const Text(
                    "Click to test",
                  ),
                  onPressed: () => showCupertinoModalPopup(
                    context: context,
                    builder: (BuildContext modalContext) {
                      /*
                        Rough example begins
                      */
                      return StatefulBuilder(
                        builder: (context, setState) => CupertinoActionSheet(
                          title: const Text(
                            "Video sound settings",
                          ),
                          message: const Text(
                            "Please choose from an option below :",
                          ),
                          actions: [
                            CupertinoActionSheetAction(
                              onPressed: ({
                                int index = 0,
                              }) async {
                                // show loading, and wait till function done
                                await _showLoading(
                                  context: context,
                                );
    
                                // tell flutter to build, check will be shown based on values set on next build
                                setState(
                                  () {
                                    selected["enabled"] = true;
                                    selected["disabled"] = false;
                                  },
                                );
                              },
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  const Text(
                                    "Enabled",
                                  ),
                                  Visibility(
                                    visible: selected["enabled"]!,
                                    child: const Icon(
                                      Icons.check,
                                    ),
                                  ),
                                ],
                              ),
                            ),
                            CupertinoActionSheetAction(
                              onPressed: ({
                                int index = 0,
                              }) async {
                                await _showLoading(
                                  context: context,
                                );
    
                                // opposite values
                                setState(
                                  () {
                                    selected["enabled"] = false;
                                    selected["disabled"] = true;
                                  },
                                );
                              },
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  const Text(
                                    "Disabled",
                                  ),
                                  Visibility(
                                    visible: selected["disabled"]!,
                                    child: const Icon(
                                      Icons.check,
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                          cancelButton: CupertinoActionSheetAction(
                            isDefaultAction: true,
                            onPressed: () {
                              Navigator.pop(
                                context,
                              );
                            },
                            child: const Text(
                              "Cancel",
                            ),
                          ),
                        ),
                      );
                    },
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }
    

    Updated code

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    
    void main() {
      // used to indicate which option is selected
      Map<String, bool> selected = {
        "enabled": true,
        "disabled": false,
      };
    
      // if true show loading
      bool loadingFlag = false;
    
      runApp(
        MaterialApp(
          home: Scaffold(
            body: StatefulBuilder(
              builder: (context, setState) => Center(
                // button to trigger modal
                child: ElevatedButton(
                  child: const Text(
                    "Click to test",
                  ),
                  onPressed: () => showCupertinoModalPopup(
                    context: context,
                    builder: (BuildContext modalContext) {
                      /*
                        Rough example begins
                      */
                      return StatefulBuilder(
                        builder: (context, setState) => CupertinoActionSheet(
                          title: const Text(
                            "Video sound settings",
                          ),
                          message: const Text(
                            "Please choose from an option below :",
                          ),
                          actions: [
                            CupertinoActionSheetAction(
                              onPressed: ({
                                int index = 0,
                              }) async {
    
                                // update flags and set loading flag, then build
                                setState(
                                  () {
                                    selected["enabled"] = true;
                                    selected["disabled"] = false;
                                    loadingFlag = true;
                                  },
                                );
    
                                // wait for delay, change loading flag, then build
                                await Future.delayed(
                                  const Duration(
                                    milliseconds: 1000,
                                  ),
                                  () {
                                    setState(
                                      () {
                                        loadingFlag = false;
                                      },
                                    );
                                  },
                                );
                              },
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  const Text(
                                    "Enabled",
                                  ),
                                  Visibility(
                                    visible: selected["enabled"]!,
                                    // check loading flag to determine if should show loading or check
                                    child: loadingFlag
                                        ? const CircularProgressIndicator()
                                        : const Icon(
                                            Icons.check,
                                          ),
                                  ),
                                ],
                              ),
                            ),
                            CupertinoActionSheetAction(
                              onPressed: ({
                                int index = 0,
                              }) async {
                                // opposite enable disable
                                setState(
                                  () {
                                    selected["enabled"] = false;
                                    selected["disabled"] = true;
                                    loadingFlag = true;
                                  },
                                );
    
                                await Future.delayed(
                                  const Duration(
                                    milliseconds: 1000,
                                  ),
                                  () {
                                    setState(
                                      () {
                                        loadingFlag = false;
                                      },
                                    );
                                  },
                                );
                              },
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  const Text(
                                    "Disabled",
                                  ),
                                  Visibility(
                                    visible: selected["disabled"]!,
                                    child: loadingFlag
                                        ? const CircularProgressIndicator()
                                        : const Icon(
                                            Icons.check,
                                          ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                          cancelButton: CupertinoActionSheetAction(
                            isDefaultAction: true,
                            onPressed: () {
                              Navigator.pop(
                                context,
                              );
                            },
                            child: const Text(
                              "Cancel",
                            ),
                          ),
                        ),
                      );
                    },
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }