Patterns, Data, and Input

Storing Form Data


Learning Objectives

  • You know how to store form data in the application state.
  • You know how to persist the stored form data.
  • You know of the possibility to share application state between widgets.

In the previous chapter, we learned about validating form data. At the end, we created an application that submits an email address through a form. In this chapter, we modify the application so that the form data is stored in the application state. As a starting point, we use the following application.

Run the program to see the output

State and form data

We previously learned about managing application state using GetX. To store form data in the application state, we can use GetX and its reactive state management. To get started, we import the GetX package and wrap the application in a GetMaterialApp widget.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:get/get.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: Scaffold(
        body: FormWidget(),
      ),
    );
  }
}

// ...

Then, we create a controller class that manages the data from the form. As we are collecting emails, which are strings, we can store the emails as a list of strings in the controller class. The controller class provides methods for adding an email to the list and for retrieving the size of the list. The class is shown below.

class EmailController {
  final emails = <String>[].obs;

  void add(String email) {
    emails.add(email);
  }

  get size => emails.length;
}

Now, to use the class in the application, we need to create an instance of the class and add it to the application — for this, we use Get.lazyPut.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:get/get.dart';

void main() {
  Get.lazyPut<EmailController>(() => EmailController());
  runApp(MyApp());
}

// ...

Next, we need to modify the FormWidget class to use the controller class to store the form data. Let’s add an instance of the EmailController as a property to the class, using Get.find. In addition, we modify the _submit method to save and validate the form data, and add the email through the controller if the email is valid. Finally, if the email is valid, we can clear the form. This would be done as follows.

// ...
class FormWidget extends StatelessWidget {
  final static _formKey = GlobalKey<FormBuilderState>();
  final emailController = Get.find<EmailController>();

  _submit() {
    if (_formKey.currentState!.saveAndValidate()) {
      emailController.add(_formKey.currentState!.value['email']);
      _formKey.currentState?.reset();
    }
  }

// ...

The above implementation assumes that the name of the email field is email. If the name of the email field is different, the name should be changed accordingly, or otherwise, the key that is used to access the email from the form data should be changed. With the changes, the form data is now stored in the application state. The full application looks as follows.

Run the program to see the output

Showing the emails

To test whether the form data is actually stored in the application, we can add a widget that uses the email controller to list the emails. Let’s call the widget EmailViewWidget, and implement it so that it uses Obx to listen to changes in the email service and shows the list of emails if there are emails, while otherwise showing the text “No emails”. The widget is shown below.

class EmailViewWidget extends StatelessWidget {
  final emailController = Get.find<EmailController>();

  @override
  Widget build(BuildContext context) {
    return Obx(
      () => emailController.size == 0
          ? Text('No emails')
          : Column(
              children: emailController.emails
                  .map(
                    (email) => Text(email),
                  )
                  .toList(),
            ),
    );
  }
}

The map function is used to convert the list of emails to a list of text widgets that show the emails. The list of text widgets is then shown in a column. To show the EmailViewWidget in the application, we can modify the application to show the widget below the form. We can create a column of the FormWidget and the EmailViewWidget and add the column to the body of the scaffold, as shown below.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            FormWidget(),
            EmailViewWidget(),
          ],
        ),
      ),
    );
  }
}

With the changes, the application now shows the list of emails below the form. The list of emails is updated when the form data is added to the service class. The full application looks as follows.

Run the program to see the output

In the above example, the email controller is used in two widgets, EmailViewWidget and FormWidget. As we use dependency injection, it is created only once, and it is shared between the widgets.

Loading Exercise...

Persisting emails

If we would wish that the emails are persisted between application restarts, we can use Hive for storing the emails on the device. This would involve adding the library and importing it to the application, modifying the main function to initialize Hive and to create a storage, and modifying the EmailController to read the emails from the storage when the controller is initialized and to save the emails to the storage when new emails are added.

The changed main function would be as follows.

// other imports
import 'package:hive_ce_flutter/hive_flutter.dart';

Future<void> main() async {
  await Hive.initFlutter();
  await Hive.openBox("storage");
  Get.lazyPut<EmailController>(() => EmailController());
  runApp(MyApp());
}

And the rewritten version of the EmailController with the storage logic would be as follows. Note that below, we use the RxList class from GetX to store the emails; otherwise, the type inference from Dart seems to break when trying to display the list of existing emails.

// imports

class EmailController {
  final storage = Hive.box("storage");

  RxList emails;

  EmailController() : emails = [].obs {
    emails.value = storage.get('emails') ?? [];
  }

  void add(String email) {
    emails.add(email);
    storage.put('emails', emails);
  }

  get size => emails.length;
}

Now, the application would store the emails between restarts. The full application that persists emails is shown below.

Run the program to see the output

Loading Exercise...

Separate service class

Let’s again modify the application to create a separate service class for managing the emails and for interacting with the data storage. A first version of the service class would provide functionality for reading the emails from the storage when the service is initialized and for saving the emails to the storage when new emails are added.

The service class — EmailService — is shown below.

// imports

class EmailService {
  final storage = Hive.box("storage");

  get emails => storage.get('emails') ?? [];

  void addEmail(String email) {
    storage.put('emails', emails..add(email));
  }
}

The two dots above in emails..add(email) is a shorthand for creating a new list with the email added to the existing list of emails, retrieved using get emails which translates into storage.read('emails') ?? [].

To use the service class in the application, we need to add the service to the application using Get.lazyPut and then modify the EmailController to use the service class for storing the emails. The modified main function would be as follows.

// imports

Future<void> main() async {
  await Hive.initFlutter();
  await Hive.openBox("storage");
  Get.lazyPut<EmailService>(() => EmailService());
  Get.lazyPut<EmailController>(() => EmailController());
  runApp(MyApp());
}

And the modified EmailController would be as follows.

class EmailController {
  final emailService = Get.find<EmailService>();

  RxList emails;

  EmailController() : emails = [].obs {
    emails.value = emailService.emails;
  }

  void add(String email) {
    emailService.addEmail(email);
    emails.add(email);
  }

  int get size => emails.length;
}

Try out the application below. As you notice, something is a bit off..

Run the program to see the output

When we add an email to the application, the email is added twice. This is not something that we want.

The reason for this is that although we seem to be adding the email to both the list of emails in the EmailController and the to the storage through the EmailService, we are actually adding the email to the same list. The reason for this is that the method get emails => storage.read('emails') ?? [] in the EmailService class returns a reference to the list of emails, and not a copy of the list of emails.

This means that when we add an email to the list of emails in the EmailController and then add the email to the list of emails in the EmailService, we are actually adding the email to the same list as the one returned by the EmailService class.

One way to resolve the issue is to create a copy of the list of emails in the EmailService class when the emails are read from the storage. This would ensure that the list of emails in the EmailService class is separate from the list of emails in the EmailController class. The modified EmailService class is shown below.

class EmailService {
  final storage = Hive.box("storage");

  get emails => storage.containsKey('emails') ? [...storage.get('emails')] : [];

  void addEmail(String email) {
    storage.put('emails', emails..add(email));
  }
}

Now, the method get emails first checks whether the storage has data for the key emails and then returns a copy of the list of emails if there is data, or an empty list if there is no data. With the changes, the application now works as expected. The full application that persists emails and shows the emails is shown below.

Run the program to see the output

Loading Exercise...