diff --git a/lib/main.dart b/lib/main.dart index b6b9b7e2e..d199198f4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,7 +41,7 @@ void main() { //LoggingMiddleware.printer(), ])); - runApp(new InvoiceNinjaApp(store: store)); + runApp(InvoiceNinjaApp(store: store)); } class InvoiceNinjaApp extends StatefulWidget { @@ -50,16 +50,16 @@ class InvoiceNinjaApp extends StatefulWidget { InvoiceNinjaApp({Key key, this.store}) : super(key: key); @override - _InvoiceNinjaAppState createState() => new _InvoiceNinjaAppState(); + _InvoiceNinjaAppState createState() => _InvoiceNinjaAppState(); } class _InvoiceNinjaAppState extends State { @override Widget build(BuildContext context) { - return new StoreProvider( + return StoreProvider( store: widget.store, - child: new MaterialApp( + child: MaterialApp( debugShowCheckedModeBanner: false, localizationsDelegates: [ const AppLocalizationsDelegate(), diff --git a/samples/form.dart b/samples/form_keys.dart similarity index 100% rename from samples/form.dart rename to samples/form_keys.dart diff --git a/samples/form_redux.dart b/samples/form_redux.dart new file mode 100644 index 000000000..4948abb1c --- /dev/null +++ b/samples/form_redux.dart @@ -0,0 +1,323 @@ +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 contacts; +} + +class ContactEntity { + ContactEntity({this.email}); + String email; +} + +// Redux classes +class AppState { + ClientEntity client; + AppState(this.client); + + AppState.init() + : client = ClientEntity( + name: 'Acme Client', + contacts: [ContactEntity(email: 'test@example.com')]); +} + +class UpdateClient { + final String name; + UpdateClient(this.name); +} + +class AddContact {} + +class UpdateContact { + final ContactEntity oldContact; + final ContactEntity newContact; + UpdateContact({this.oldContact, this.newContact}); +} + +class DeleteContact { + final String email; + DeleteContact(this.email); +} + +AppState reducer(AppState state, action) { + // In an 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 DeleteContact) { + return AppState(ClientEntity( + name: state.client.name, + contacts: state.client.contacts.where((contact) => contact.email != action.email).toList(), + )); + } else if (action is UpdateContact) { + return AppState(ClientEntity( + name: state.client.name, + )); + } + + return state; +} + +void main() { + final store = + Store(reducer, initialState: AppState.init(), middleware: [ + LoggingMiddleware.printer(), + ]); + + runApp(MyApp(store: store)); +} + +class MyApp extends StatefulWidget { + final Store store; + + MyApp({Key key, this.store}) : super(key: key); + + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State with SingleTickerProviderStateMixin { + static final GlobalKey _formKey = 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) { + return StoreProvider( + store: widget.store, + child: MaterialApp( + home: Scaffold( + appBar: AppBar( + title: Text('Client Form'), + actions: [ + StoreBuilder( + builder: (BuildContext context, Store store) { + return IconButton( + icon: Icon(Icons.cloud_upload), + onPressed: () { + if (!_formKey.currentState.validate()) { + return; + } + + _formKey.currentState.save(); + + // 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: [ + ClientPage(), + ContactsPage(), + ], + ), + ), + ), + ), + ); + } +} + +class ClientPage extends StatefulWidget { + @override + _ClientPageState createState() => new _ClientPageState(); +} + +class _ClientPageState extends State { + final _nameController = new TextEditingController(); + + @override + void initState() { + super.initState(); + _nameController.addListener(_onChanged); + } + + @override + void dispose() { + _nameController.removeListener(_onChanged); + _nameController.dispose(); + super.dispose(); + } + + _onChanged() { + var name = _nameController.text.trim(); + StoreProvider.of(context).dispatch(UpdateClient(name)); + } + + @override + Widget build(BuildContext context) { + return StoreBuilder(builder: (BuildContext context, Store store) { + _nameController.text = store.state.client.name; + + return FormCard( + children: [ + TextFormField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'Name', + ), + ), + ], + ); + }); + } +} + +class ContactsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return StoreBuilder(builder: (BuildContext context, Store store) { + var contacts = store.state.client.contacts + .map((contact) => ContactForm(contact)) + .toList(); + + return ListView( + children: [] + ..addAll(contacts) + ..add(Padding( + padding: const EdgeInsets.all(12.0), + child: RaisedButton( + elevation: 4.0, + color: Theme.of(context).primaryColor, + textColor: Theme.of(context).secondaryHeaderColor, + child: Text('ADD CONTACT'), + onPressed: () { + store.dispatch(AddContact()); + }, + ), + )), + ); + }); + } +} + +class ContactForm extends StatefulWidget { + ContactForm(this.contact); + final ContactEntity contact; + + @override + _ContactFormState createState() => new _ContactFormState(); +} + +class _ContactFormState extends State { + final _emailController = new TextEditingController(); + + @override + void initState() { + super.initState(); + _emailController.addListener(_onChanged); + } + + @override + void dispose() { + _emailController.removeListener(_onChanged); + _emailController.dispose(); + super.dispose(); + } + + _onChanged() { + var name = _emailController.text.trim(); + //StoreProvider.of(context).dispatch(UpdateClient(name)); + } + + @override + Widget build(BuildContext context) { + var store = StoreProvider.of(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: () => store.dispatch(DeleteContact(widget.contact.email)), + child: Text( + 'Delete', + style: TextStyle( + color: Colors.grey[600], + ), + ), + ), + ) + ], + ), + ], + ); + } +} + +// Helper widget to make the form look a bit nicer +class FormCard extends StatelessWidget { + 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, + ), + ), + ), + ); + } +}