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

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.