Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
359 lines (313 sloc) 8.84 KB
// https://hillelcoren.com/2018/06/18/flutter-using-redux-to-manage-complex-forms-with-multiple-tabs-and-relationships/
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:redux_logging/redux_logging.dart';
// Sample entity classes
class ClientEntity {
ClientEntity({this.name, this.contacts});
String name;
List<ContactEntity> contacts;
@override
String toString() {
return (name ?? '') + ': ' + contacts.join(', ');
}
}
class ContactEntity {
ContactEntity({this.email});
String email;
@override
String toString() {
return email ?? '';
}
}
// Redux classes
class AppState {
ClientEntity client;
AppState(this.client);
AppState.init()
: client = ClientEntity(
name: 'Acme Client',
contacts: [ContactEntity(email: 'test@example.com')]);
@override
String toString() {
return client.toString();
}
}
class UpdateClient {
final String name;
UpdateClient(this.name);
}
class AddContact {}
class UpdateContact {
final int index;
final String email;
UpdateContact({this.index, this.email});
}
class DeleteContact {
final int index;
DeleteContact(this.index);
}
AppState reducer(AppState state, dynamic action) {
// In an actual app you'd most like want to
// use built_value to rebuild the state
if (action is UpdateClient) {
return AppState(ClientEntity(
name: action.name,
contacts: state.client.contacts,
));
} else if (action is AddContact) {
return AppState(ClientEntity(
name: state.client.name,
contacts: []
..addAll(state.client.contacts)
..add(ContactEntity())));
} else if (action is UpdateContact) {
return AppState(ClientEntity(
name: state.client.name,
contacts: []
..addAll(state.client.contacts)
..removeAt(action.index)
..insert(action.index, ContactEntity(email: action.email))));
} else if (action is DeleteContact) {
return AppState(ClientEntity(
name: state.client.name,
contacts: []
..addAll(state.client.contacts)
..removeAt(action.index)));
}
return state;
}
void main() {
final store =
Store<AppState>(reducer, initialState: AppState.init(), middleware: [
LoggingMiddleware<dynamic>.printer(),
]);
runApp(MyApp(store: store));
}
class MyApp extends StatefulWidget {
final Store<AppState> store;
const MyApp({Key key, this.store}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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) {
return StoreProvider<AppState>(
store: widget.store,
child: MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Client Form'),
actions: <Widget>[
StoreBuilder(
builder: (BuildContext context, Store<AppState> store) {
return IconButton(
icon: Icon(Icons.cloud_upload),
onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
// Do something with the client...
print('Client name: ' + store.state.client.name);
},
);
}),
],
bottom: TabBar(
controller: _controller,
tabs: [
Tab(
text: 'Details',
),
Tab(
text: 'Contacts',
),
],
),
),
body: Form(
key: _formKey,
child: TabBarView(
controller: _controller,
children: <Widget>[
ClientPage(),
ContactsPage(),
],
),
),
),
),
);
}
}
class ClientPage extends StatefulWidget {
@override
_ClientPageState createState() => new _ClientPageState();
}
class _ClientPageState extends State<ClientPage> {
final _nameController = new TextEditingController();
@override
void didChangeDependencies() {
final store = StoreProvider.of<AppState>(context);
_nameController.removeListener(_onChanged);
_nameController.text = store.state.client.name;
_nameController.addListener(_onChanged);
super.didChangeDependencies();
}
@override
void dispose() {
_nameController.removeListener(_onChanged);
_nameController.dispose();
super.dispose();
}
void _onChanged() {
final name = _nameController.text.trim();
final store = StoreProvider.of<AppState>(context);
if (name != store.state.client.name) {
store.dispatch(UpdateClient(name));
}
}
@override
Widget build(BuildContext context) {
return StoreBuilder(builder: (BuildContext context, Store<AppState> store) {
return FormCard(
children: <Widget>[
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Name',
),
),
],
);
});
}
}
class ContactsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreBuilder(builder: (BuildContext context, Store<AppState> store) {
final client = store.state.client;
final contacts = client.contacts.map((contact) => ContactForm(
contact: contact,
//key: Key('__contact_${contact.id}__'),
index: store.state.client.contacts.indexOf(contact)));
return ListView(
children: []
..addAll(contacts)
..add(Padding(
padding: const EdgeInsets.all(14.0),
child: RaisedButton(
elevation: 4.0,
textColor: Theme.of(context).secondaryHeaderColor,
child: Text('ADD CONTACT'),
onPressed: () {
store.dispatch(AddContact());
},
),
)),
);
});
}
}
class ContactForm extends StatefulWidget {
const ContactForm({Key key, @required this.contact, @required this.index})
: super(key: key);
final int index;
final ContactEntity contact;
@override
_ContactFormState createState() => new _ContactFormState();
}
class _ContactFormState extends State<ContactForm> {
final _emailController = new TextEditingController();
@override
void didChangeDependencies() {
_emailController.removeListener(_onChanged);
_emailController.text = widget.contact.email;
_emailController.addListener(_onChanged);
super.didChangeDependencies();
}
@override
void dispose() {
_emailController.removeListener(_onChanged);
_emailController.dispose();
super.dispose();
}
void _onChanged() {
final store = StoreProvider.of<AppState>(context);
final email = _emailController.text.trim();
if (email != widget.contact.email) {
store.dispatch(UpdateContact(email: email, index: widget.index));
}
}
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<AppState>(context);
return FormCard(
children: <Widget>[
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
),
keyboardType: TextInputType.emailAddress,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: FlatButton(
onPressed: () => store.dispatch(DeleteContact(widget.index)),
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({
Key key,
@required this.children,
}) : super(key: key);
final List<Widget> 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,
),
),
),
);
}
}