How to build a Flutter Form for managing questions and their answers
Tags: flutter, app, form, question
Published: 2022-01-19 20:07:10

## What we will be building ...
A one-screen [flutter](https://flutter.dev) app containing a form. The form can be used to create/update a _question_ and its options (and the correct option too). This interface is for setting questions that could be used elsewhere like an online quiz. The whole _question_ data can then be saved to a database like [Firebase Firestore](https://firebase.google.com/products/firestore). The screen looks like this:

![UI of Question Form built with Flutter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ppeezy8g4mf8rlqp28e5.png)

## Prerequisites
* Have basic understanding of flutter and probably programming as a whole.
* Have [installed flutter](https://docs.flutter.dev/get-started/install) and its working.

## Steps
### 1. Setup
Initialize the flutter project by running the following command in your terminal

```shell
flutter create questions
```

### 2. Coding
a. Open the project in your favorite editor.
b. Delete the contents of `lib/main.dart` file, paste the following code, and save the file.

{% gist https://gist.github.com/obumnwabude/0f4a6d742c3daf74261ad0125d9bd321 %}

### 3. Run
Run the code with the following command 

```shell
flutter run
```
The app should run in your attached device and it should show something similar to the above UI.

## More on the Question Form
### [Iteration](https://en.wikipedia.org/wiki/Iteration)/[Looping](https://en.wikipedia.org/wiki/Control_flow#Loops)

It is good practice to make code short or DRY (Don't Repeat Yourself). The code for the "correct option radio buttons" and the "TextFormFields for the option values" are the same. For these two sections of [`Widget`](https://api.flutter.dev/flutter/widgets/Widget-class.html)s, we are looping over the list of options

```dart
const options = ['A', 'B', 'C', 'D'];
``` 

to return each necessary Widget.

The [`Radio`](https://api.flutter.dev/flutter/material/Radio-class.html), [`Text`](https://api.flutter.dev/flutter/widgets/Text-class.html) (for each option), and spacer (`const SizedBox(width: 16)`) are been repeated together, reason why we had to call the [`expand`](https://api.flutter.dev/flutter/dart-core/Iterable/expand.html) method on [`Iterable`](https://api.flutter.dev/flutter/dart-core/Iterable-class.html) to _expand the list of lists_ been returned. Same is applicable for the [`TextFormField`](https://api.flutter.dev/flutter/material/TextFormField-class.html) for each option's value and its own spacer (`const SizedBox(height: 32)`) as they are been returned together too in the [`map` function](https://api.flutter.dev/flutter/dart-core/Iterable/map.html).

```dart
  \\ ... ,
  const Text('Correct Option'),
  Row(
      children: options
          .map((option) => [
                Radio<String>(
                  value: option,
                  groupValue: _question['correct'] as String,
                  onChanged: (v) => setState(() => _question['correct'] = v!),
                ),
                Text(option),
                const SizedBox(width: 16)
              ])
          .expand((w) => w)
          .toList()),
  const SizedBox(height: 32),
  ...options
      .asMap()
      .entries
      .map((entry) => [
            TextFormField(
              controller: _optionCtrls[entry.key],
              decoration: InputDecoration(labelText: 'Option ${entry.value}*'),
              validator: (v) =>
                  v!.isEmpty ? 'Please fill in Option ${entry.value}' : null,
            ),
            const SizedBox(height: 32),
          ])
      .expand((w) => w),
  \\ ...
```


### Validation
[Form validation in flutter](https://docs.flutter.dev/cookbook/forms/validation) is achieved by providing a validator function as an argument to the `validator` named parameter of the [`TextFormField`](https://api.flutter.dev/flutter/material/TextFormField-class.html). The [validator function](https://api.flutter.dev/flutter/widgets/FormField/validator.html) should return `null` if the input is valid, else it should return an error message if the input is invalid (or empty in our case).

```dart
validator: (v) => v!.isEmpty ? 'Please fill in the Question' : null
```


![Flutter Question Form with error messages under TextFormFields](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6yicetk9vvpsoysw527c.png)


### Feedback to user with [SnackBar](https://api.flutter.dev/flutter/material/SnackBar-class.html)

The submit button of forms are usually disabled if the form is invalid (has errors) in some apps. However, the user might not know why the button is disabled and that will be poor user experience. 

Good user experience entails giving feedback to user when they take actions on your app. That's why those error messages on the Form show up when the user leaves the fields empty. Instead of disabling the submit button, we instead check for the form's validity when the button is pressed.

If the form is valid, we collect the _question_ data and show the snackbar with success message. If on the other hand the form is invalid, we show the snackbar with the error message. 

```dart
ElevatedButton(
  onPressed: () {
    if (_formKey.currentState!.validate()) {
      _question['value'] = _questionCtrl.text;
      _question['options'] = _optionCtrls.asMap().entries.map(
        (entry) {
          return {
            'index': options[entry.key],
            'value': entry.value.text,
          };
        },
      );
      if (kDebugMode) {
        print(_question);
      }
      showSnackbar(
        success: true,
        text: 'Question updated successfully.',
      );
    } else {
      showSnackbar(
        success: false,
        text: 'Please fill all the required fields.',
      );
    }
  },
  child: const Text('Update'),
)
```


![Error Snackbar in Flutter Question Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/24sc3263h07k8k5c3xpl.png)

### Customizing the SnackBar
[Displaying a SnackBar](https://docs.flutter.dev/cookbook/design/snackbars) is easy with Flutter. By default, the snackbar pops in the bottom with full screen with. But we can customize the snackbar to appear at the top of the screen, in order to quickly catch the user's attention.  We can also make the snackbar float by giving it some left and right margins. 

Notice how we used a function: `showSnackbar`, that takes an [`IconData`](https://api.flutter.dev/flutter/widgets/IconData-class.html) (to be displayed in the snackbar), text to be displayed, and a _success_ `bool`, that is used to determine the color of the displayed icon.

```dart
void showSnackbar({required bool success, required String text}) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      margin: EdgeInsets.only(
        bottom: MediaQuery.of(context).size.height - 96,
        right: 16,
        left: 16,
      ),
      behavior: SnackBarBehavior.floating,
      content: Row(children: [
        Icon(
          success ? Icons.gpp_good : Icons.error,
          color: success ? Colors.greenAccent : Colors.redAccent,
        ),
        const SizedBox(width: 8),
        Text(text),
      ]),
    ),
  );
}
```


![Success Snackbar in Flutter Question Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ao0m3eby5isr8smxe52h.png)

## Final Thoughts
Flutter is a good framework for mobile development. Developer experience with flutter is smooth and the lines of code are shorter compared to other frameworks. 

Forms are an important part of many apps as they help in collecting data. In our flutter app, we used a form to collect data about a question been set/created. This Flutter Question Form may not do much because the data is not been submitted anywhere. But then, we have successfully built a UI that can serve for setting questions that could be saved to some database.

## Up Next
We will use this form to build an app that can edit many questions to be set for an online quiz.