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.

No files opened Select a file to edit

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 {
static final _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.

No files opened Select a file to edit

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.

No files opened Select a file to edit

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.

Viewing and editing

0 / 60 points

Write an application that consists of two screens and a controller used for sharing state. The controller and the two screens should be as follows.

  • ContentController should have a reactive variable called “content”. When the application is started, the value for the reactive value should be “Hello”.
  • HomeScreen should show a text to the user that has the value of the reactive variable “content” in the ContentController. The screen should also have a button with the text “Edit”. Clicking the button “Edit” navigates the user to a screen called EditScreen.
  • EditScreen should have a text field where the user can type in a new value for the reactive variable “content” that is stored in the ContentController. Use a FormBuilderTextField for entering text. The screen should also have a button with the text “Save”. Clicking the button “Save” sets the value of the reactive variable “content” in the ContentController to the value that the user has typed in the text field and navigates the user back to the HomeScreen.

As a concrete example, when the application starts, the user is shown the text “Hello” and a button with the text “Edit”. When the user presses the button “Edit”, the user is moved to the editing screen. After typing in the text “World” and pressing the button “Save”, the user is moved back to the starting screen, where the text shown is now “World”.

Note, do not use named routes in this exercise. That is, use Get.to and Get.back for navigation.

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.

No files opened Select a file to edit

Persisted viewing and editing

0 / 20 points

Take your solution to the earlier “Viewing and editing” exercise, and modify it so that the value of the reactive variable “content” is persisted between application restarts. That is, when the user closes the application and opens it again, the value of the reactive variable “content” should be the same as when the application was closed. To achieve this, use Hive. Use the name “storage” for the Hive’s box, and use the key “content” to concretely store the value of the reactive variable “content”.

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..

No files opened Select a file to edit

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.

No files opened Select a file to edit

Grateful about

0 / 80 points

Create an application that allows typing in things that the user has been grateful about. The application should have two screens, HomeScreen and AddEntryScreen. The screens should be as follows.

  • HomeScreen should show the user the most recently added item that the user has been grateful about (if there are no entries, show the text “No entries yet”) and total count of items that the user has been grateful about (using the text “Total entries: {count}", where {count} corresponds to the number of entries). The screen should also have an elevated button with the text “Add entry”. Clicking the button “Add entry” navigates the user to a screen called AddEntryScreen.
  • AddEntryScreen should have a text field where the user can type in a new item that the user has been grateful about. The screen should also have a button with the text “Save”. Clicking the button “Save” adds the item that the user has typed in the text field to the list of items that the user has been grateful about and navigates the user back to the HomeScreen. Use a FormBuilderTextField for entering the text.

In addition, the application should have the following classes:

  • GratefulService is responsible for interacting with the Hive storage. Store the entries to Hive. Use the name “storage” for the Hive’s box, and use the key “entries” to concretely store the list of items that the user has been grateful about.
  • GratefulController links the GratefulService with the two screens.

Use inversion of control and dependency injection in the application. For finding the last added item, use the last item in the list of entries, e.g. list[size of list - 1].