// https://hillelcoren.com/2018/06/12/flutter-complex-forms-with-multiple-tabs-and-relationships/ import 'package:flutter/material.dart'; // Sample entity classes class ClientEntity { ClientEntity({this.name, this.contacts}); String name; List contacts; } class ContactEntity { ContactEntity({this.email}); String email; } void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State with SingleTickerProviderStateMixin { static final GlobalKey _formKey = GlobalKey(); static final GlobalKey _clientKey = GlobalKey(); static final GlobalKey _contactsKey = GlobalKey(); TabController _controller; @override void initState() { super.initState(); _controller = TabController(vsync: this, length: 2); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Create a test client to show initially final ClientEntity _client = ClientEntity( name: 'Acme Client', contacts: [ContactEntity(email: 'test@example.com')]); return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('Client Form'), actions: [ IconButton( icon: Icon(Icons.cloud_upload), onPressed: () { if (!_formKey.currentState.validate()) { return; } _formKey.currentState.save(); final clientState = _clientKey.currentState; final contactsState = _contactsKey.currentState; // If the user never views a tab the state can be null // in which case we'll use the current value final ClientEntity client = ClientEntity( name: clientState?.name ?? _client.name, contacts: contactsState?.getContacts() ?? _client.contacts, ); // Do something with the client... print('Client name: ${client.name}'); }, ) ], bottom: TabBar( controller: _controller, tabs: [ Tab( text: 'Details', ), Tab( text: 'Contacts', ), ], ), ), body: Form( key: _formKey, child: TabBarView( controller: _controller, children: [ ClientPage(client: _client, key: _clientKey), ContactsPage(client: _client, key: _contactsKey), ], ), ), ), ); } } // Display the client's details, currently just their name class ClientPage extends StatefulWidget { const ClientPage({ Key key, @required this.client, }) : super(key: key); final ClientEntity client; @override ClientPageState createState() => ClientPageState(); } class ClientPageState extends State with AutomaticKeepAliveClientMixin { ClientPageState({this.client}); final ClientEntity client; @override bool get wantKeepAlive => true; String name; @override Widget build(BuildContext context) { super.build(context); return FormCard( children: [ TextFormField( initialValue: widget.client.name, onSaved: (value) => name = value.trim(), decoration: InputDecoration( labelText: 'Name', ), ), ], ); } } // Displays the list of contacts with a button to add more class ContactsPage extends StatefulWidget { const ContactsPage({ Key key, @required this.client, }) : super(key: key); final ClientEntity client; @override ContactsPageState createState() => ContactsPageState(); } class ContactsPageState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; List _contacts; List> _contactKeys; @override void initState() { super.initState(); final client = widget.client; _contacts = client.contacts.toList(); _contactKeys = client.contacts .map((contact) => GlobalKey()) .toList(); } List getContacts() { final List contacts = []; _contactKeys.forEach((contactKey) { contacts.add(contactKey.currentState.getContact()); }); return contacts; } void _onAddPressed() { setState(() { _contacts.add(ContactEntity()); _contactKeys.add(GlobalKey()); }); } void _onRemovePressed(GlobalKey key) { setState(() { final index = _contactKeys.indexOf(key); _contactKeys.removeAt(index); _contacts.removeAt(index); }); } @override Widget build(BuildContext context) { super.build(context); final List items = []; for (var i = 0; i < _contacts.length; i++) { final contact = _contacts[i]; final contactKey = _contactKeys[i]; items.add(ContactForm( contact: contact, key: contactKey, onRemovePressed: (key) => _onRemovePressed(key), )); } items.add(Padding( padding: const EdgeInsets.all(14.0), child: RaisedButton( elevation: 4.0, textColor: Theme.of(context).secondaryHeaderColor, child: Text('ADD CONTACT'), onPressed: _onAddPressed, ), )); return ListView( children: items, ); } } // Displays an individual contact class ContactForm extends StatefulWidget { const ContactForm({ Key key, @required this.contact, @required this.onRemovePressed, }) : super(key: key); final ContactEntity contact; final Function(GlobalKey) onRemovePressed; @override ContactFormState createState() => ContactFormState(); } class ContactFormState extends State { String _email; ContactEntity getContact() { return ContactEntity(email: _email); } @override Widget build(BuildContext context) { return FormCard( children: [ TextFormField( initialValue: widget.contact.email, onSaved: (value) => _email = value.trim(), decoration: InputDecoration( labelText: 'Email', ), keyboardType: TextInputType.emailAddress, ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.only(top: 12.0), child: FlatButton( onPressed: () => widget.onRemovePressed(widget.key), child: Text( 'Delete', style: TextStyle( color: Colors.grey[600], ), ), ), ) ], ), ], ); } } // Helper widget to make the form look a bit nicer class FormCard extends StatelessWidget { const FormCard({this.children}); final List children; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 12.0, top: 12.0, right: 12.0), child: Card( elevation: 2.0, child: Padding( padding: const EdgeInsets.only( left: 12.0, right: 12.0, top: 12.0, bottom: 18.0), child: Column( children: children, ), ), ), ); } }