flutterformsbuttonkeyboardspacing

Flutter Best way to create form with a button on the bottom?


Here is what I'm trying to achieve:

On a smaller device, where all the input fields can't be
displayed on a single page, the form should be like this:

enter image description here

But on a bigger screen, where all the Input fields
can be displayed on one page, I want it to be like this:

enter image description here

(Notice the spacing between the button and the last input
field should be dynamic, so that even on tablets the button
will be on the bottom of the screen)

Please before providing an answer also check how it reacts to the keyboard being opened, I don't want it to make the button move upwards... like this:

enter image description here

Also, of course, I don't want the keyboard to cause overflows, or make the input fields not accessible, I have tried many things, and I haven't found a way to achieve it, even though it seems to be a simple problem.


Solution

  • I am using a LayoutBuilder, SingleChildScrollView and ConstrainedBox to give the min height to the Column.

    To place the button at the bottom when there are enough room, use a Spacer, and using a Spacer inside SingleChildScrollView would also need to warp the Column with an IntrinsicHeight.

    This approach is demonstrated in the documentation of SingleChildScrollView.

    To avoid the keyboard pushing the button, we need to get the viewInsets using MediaQuery.viewInsetsOf(context) and adjust it to give the min height to the Column.

    demo

    class A extends StatefulWidget {
      const A({super.key});
    
      @override
      State<A> createState() => _AState();
    }
    
    class _AState extends State<A> {
      int _number = 5;
    
      @override
      Widget build(BuildContext context) {
        final viewInsets = MediaQuery.viewInsetsOf(context);
        return Scaffold(
          appBar: AppBar(
            title: const Text('Demo'),
          ),
          body: SafeArea(
            child: LayoutBuilder(
              builder: (context, constraints) {
                final minHeight = constraints.maxHeight + viewInsets.bottom;
                return SingleChildScrollView(
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  child: ConstrainedBox(
                    constraints: BoxConstraints(minHeight: minHeight),
                    child: IntrinsicHeight(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          Card(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: [
                                Text('ViewInsets: $viewInsets'),
                                Text('Constraints: $constraints'),
                                Text('minHeight: $minHeight'),
                              ],
                            ),
                          ),
                          for (var i = 0; i < _number; i++)
                            const Card(
                              child: TextField(),
                            ),
                          // This is for demo only, we could simply use a spacer here
                          const Expanded(
                            child: DecoratedBox(
                              decoration: BoxDecoration(
                                border: Border.fromBorderSide(BorderSide(color: Colors.red)),
                              ),
                            ),
                          ),
                          DecoratedBox(
                            decoration: const BoxDecoration(
                              border: Border.fromBorderSide(BorderSide(color: Colors.blue)),
                            ),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.spaceAround,
                              children: [
                                IconButton.filled(
                                  icon: const Icon(Icons.add),
                                  onPressed: () {
                                    setState(() {
                                      _number++;
                                    });
                                  },
                                ),
                                IconButton.filled(
                                  icon: const Icon(Icons.remove),
                                  onPressed: () {
                                    setState(() {
                                      _number--;
                                    });
                                  },
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }