Flutter: Complex forms with multiple tabs and relationships

Note: we’re no longer using this approach, you can see our updated solution here.

In our app we needed to support editing complex entities with nested relationships, in this post I’ll try provide a high level overview of our solution.

The full code for this post can be found here

Screenshot_1528817346

For this example we’ll use a Client entity which has a name and a list of contacts, each contact has an email address.

class ClientEntity {
 ClientEntity({this.name, this.contacts});
 String name;
 List<ContactEntity> contacts;
}

class ContactEntity {
 ContactEntity({this.email});
 String email;
}

Dividing the UI into multiple tabs is relatively straightforward however as soon as we started testing we realized if a user made a change in one tab and then switched to a different tab without first clicking save their changes would be lost. Not good…

The solution is to have the state class use the AutomaticKeepAliveClientMixin. Adding the mixin is pretty simple, you just need to override the wantKeepAlive method to return true.

class ContactsPageState extends State<ContactsPage>
 with AutomaticKeepAliveClientMixin {

 @override
 bool get wantKeepAlive => true;

For most simple forms you can use keys or text controllers to access the text input values however with arrays/relationships it gets a bit more complicated. The approach we’re using is to have the master page create global keys for the sub-pages to use.

On the contacts page we store two lists for the contacts: the contact models as well as the keys for the contact form state.

List<ContactEntity> _contacts;
List<GlobalKey<ContactFormState>> _contactKeys;

@override
 void initState() {
  super.initState();
  var client = widget.client;
  _contacts = client.contacts.toList();
  _contactKeys = client.contacts
    .map((contact) => GlobalKey<ContactFormState>())
    .toList();
 }

To build the view we loop through each of the contacts/keys. When initially working on this whenever we focused the text input the focus would be lost, this was caused by using anonymous keys which were being recreated every time the view was rebuilt. The solution was to just make sure we reused the same keys when rebuilding the layout.

for (var i = 0; i < _contacts.length; i++) {
   var contact = _contacts[i];
   var contactKey = _contactKeys[i];
   items.add(ContactForm(
     contact: contact,
     key: contactKey,
     onRemovePressed: (key) => _onRemovePressed(key),
   ));
 }

When the user clicks save the master page can use the keys to generate a new client from a combination of the existing client and the changes in the form. We need to check if the state is null in case the user clicks save without first viewing the second tab.

 ClientEntity client = ClientEntity(
   name: clientState?.name ?? _client.name,
   contacts: contactsState?.getContacts() ?? _client.contacts,
 );

There’s one caveat, as of right now I haven’t been able to make this work with more than two tabs. I believe it’s a bug, I’ve opened a GitHub issue for it and hope it will be resolved before our app launches 🙂

Hope you found this post useful, if you have any suggestions to improve the code please let me know.

 

One thought on “Flutter: Complex forms with multiple tabs and relationships”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s