Search code examples
flutterflutter-animation

How to Implement a Sign-Up/Login Pop-up with Progressive Disclosure in Flutter?


Question: I'm developing a mobile app using Flutter, and I want to implement a sign-up/login flow where users are prompted to sign up or log in when they perform certain actions, such as making a purchase or interacting with certain features.

The desired flow is as follows: When the user performs an action that requires authentication, a pop-up card should appear, prompting them to sign up or log in. Initially, the card should only request basic information like name and phone number. However, when the user clicks on the arrow at the bottom right corner of the card, it should expand to reveal additional fields like email and password.

I'm relatively new to Flutter and UI design, so I'm not sure how to approach this. Can someone provide guidance on how to implement this sign-up/login pop-up with progressive disclosure in Flutter? Specifically, I'm looking for recommendations on:

The best Flutter widgets or libraries to use for creating the pop-up card. How to implement the progressive disclosure feature, where additional fields are revealed upon user interaction. Any sample code or tutorials that demonstrate similar implementations. Any help or pointers in the right direction would be greatly appreciated!

I'm basically trying to create this:

to lead to this:

Here's what I expected to happen:

When the user interacts with the feature, the pop-up card should appear. Initially, the pop-up card should only display fields for the user's name and phone number. Upon clicking the arrow button, the card should expand to reveal additional fields for email and password.


Solution

  • You don't need any packages to handle this UI, you can simply use some Flutter components to create it, that's by using : showDialog , StateFullBuilder, Dialog and AnimatedContainer Widgets.

    First, you will need to create these two properties in the branch:

     late double _containerHeigt;
     bool _isLargHeiht = false;
    

    then in the build method(in my case I build ad extension screenHeight you can give it a value directly):

    
      @override
      Widget build(BuildContext context) {
        _containerHeigt = context.screenHeight * .3;
        return Scaffold(
          body: Center(
            child: SizedBox(
              width: context.screenWidth * .8,
              height: context.screenHeight * .08,
              child: MaterialButton(
                onPressed: () {
                  showDialog(
                    context: context,
                    barrierDismissible: false,
                    builder: (context) {
                      return StatefulBuilder(
                        builder: (context, StateSetter setState) {
                          return Dialog(
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(20),
                            ),
                            child: AnimatedContainer(
                              padding: const EdgeInsets.all(10),
                              duration: const Duration(seconds: 2),
                              height: _containerHeigt,
                              decoration: BoxDecoration(
                                color: const Color(0xFFDDDDDD),
                                borderRadius: BorderRadius.circular(20),
                              ),
                              child: const Stack(
                                children: [
                                  Column(
                                    children: <Widget>[],
                                  ),
                                ],
                              ),
                            ),
                          );
                        },
                      );
                    },
                  );
                },
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                color: Colors.orange,
                child: const Text("Show Log in"),
              ),
            ),
          ),
        );
      }
    

    choose the widget you want to Tap on it to show the Dialog (in this Example its MaterialButton)

    Then when user press on the button showDialog method called, and it's return a StatefulBuilder which is very important to change the state in the showDialog method as the setState of StatefulWidget don't work with it,

    After that you will give the height of the AnimatedContainer the property _containerHeigt

    then create a method which will change the _containerHeigt value according to the _isLargHeiht value, like this :

    void _changeContainerHeight({required StateSetter setState}) {
        setState(
          () {
            _isLargHeiht = !_isLargHeiht;
            if (_isLargHeiht) {
              _containerHeigt = context.screenHeight * .6;
            } else {
              _containerHeigt = context.screenHeight * .3;
            }
          },
        );
      }
    

    then create the button that will call this method, in the above UI it will be a child of Stack like this:

    Positioned(
        bottom: 20,
        right: 0,
        child: GestureDetector(
          onTap: () {
            _changeContainerHeight(setState: setState);
          },
          child: const CircleAvatar(
            backgroundColor: Colors.black,
            child: Icon(
              Icons.arrow_downward_rounded,
              color: Colors.white,
            ),
          ),
        ),
      )
    

    finally you can create you custom UI in :

     Column(
       children: <Widget>[],
     ),
    

    which is the first child of the Stack Widget

    Here is the Full UI Example :

    class AnimationLogInWidget extends StatefulWidget {
      const AnimationLogInWidget({super.key});
    
      @override
      State<AnimationLogInWidget> createState() => _AnimationLogInWidgetState();
    }
    
    class _AnimationLogInWidgetState extends State<AnimationLogInWidget> {
      late double _containerHeigt;
      bool _isLargHeiht = false;
    
      void _changeContainerHeight({required StateSetter setState}) {
        setState(
          () {
            _isLargHeiht = !_isLargHeiht;
            if (_isLargHeiht) {
              _containerHeigt = context.screenHeight * .6;
            } else {
              _containerHeigt = context.screenHeight * .3;
            }
          },
        );
      }
    
      @override
      Widget build(BuildContext context) {
        _containerHeigt = context.screenHeight * .3;
        return Scaffold(
          body: Center(
            child: SizedBox(
              width: context.screenWidth * .8,
              height: context.screenHeight * .08,
              child: MaterialButton(
                onPressed: () {
                  showDialog(
                    context: context,
                    barrierDismissible: false,
                    builder: (context) {
                      return StatefulBuilder(
                        builder: (context, StateSetter setState) {
                          return Dialog(
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(20),
                            ),
                            child: AnimatedContainer(
                              padding: const EdgeInsets.all(10),
                              duration: const Duration(seconds: 2),
                              height: _containerHeigt,
                              decoration: BoxDecoration(
                                color: const Color(0xFFDDDDDD),
                                borderRadius: BorderRadius.circular(20),
                              ),
                              child: Stack(
                                children: [
                                  Column(
                                    children: <Widget>[
                                      const Text(
                                        "Create new account",
                                        style: TextStyle(fontSize: 20),
                                      ),
                                      Expanded(
                                        child: SingleChildScrollView(
                                          physics: _isLargHeiht
                                              ? const AlwaysScrollableScrollPhysics()
                                              : const NeverScrollableScrollPhysics(),
                                          child: Column(
                                            children: <Widget>[
                                              const CustomTextFeildWidget(
                                                lable: "Your Name",
                                              ),
                                              const CustomTextFeildWidget(
                                                lable: "Phon number",
                                              ),
                                              const CustomTextFeildWidget(
                                                lable: "Email",
                                              ),
                                              SizedBox(
                                                height: context.screenHeight * .07,
                                                width: context.screenWidth * .6,
                                                child: MaterialButton(
                                                  onPressed: () {},
                                                  color: Colors.black,
                                                  shape: RoundedRectangleBorder(
                                                    borderRadius:
                                                        BorderRadius.circular(20),
                                                  ),
                                                  child: const Text(
                                                    "LOG IN",
                                                    style: TextStyle(
                                                      color: Colors.white,
                                                    ),
                                                  ),
                                                ),
                                              )
                                            ],
                                          ),
                                        ),
                                      ),
                                    ],
                                  ),
                                  Positioned(
                                    bottom: 20,
                                    right: 0,
                                    child: GestureDetector(
                                      onTap: () {
                                        _changeContainerHeight(setState: setState);
                                      },
                                      child: const CircleAvatar(
                                        backgroundColor: Colors.black,
                                        child: Icon(
                                          Icons.arrow_downward_rounded,
                                          color: Colors.white,
                                        ),
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          );
                        },
                      );
                    },
                  );
                },
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                color: Colors.orange,
                child: const Text("Show Log in"),
              ),
            ),
          ),
        );
      }
    }
    
    class CustomTextFeildWidget extends StatelessWidget {
      const CustomTextFeildWidget({
        super.key,
        required this.lable,
      });
    
      final String lable;
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(10.0),
          child: TextField(
            decoration: InputDecoration(
              label: Text(lable),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(15),
                borderSide: const BorderSide(
                  color: Colors.orange,
                ),
              ),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(15),
                borderSide: const BorderSide(
                  color: Colors.orange,
                ),
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(15),
                borderSide: const BorderSide(
                  color: Colors.orange,
                ),
              ),
            ),
          ),
        );
      }
    }
    
    

    You can customize everything with _isLargHeiht and _containerHeigt .

    Hope that help you !!