Integrate your app
In this section you'll integrate Amplify API with your app, and use the generated data model to create, update, query, and delete BudgetEntry items in your app.
First, replace the contents of your main.dart file with the following UI boilerplate code. Typically, you would break this file up into smaller modules but we've kept it as a single file here just for the tutorial.
1import 'package:flutter/material.dart';2import 'package:go_router/go_router.dart';3
4import 'models/ModelProvider.dart';5
6Future<void> main() async {7 WidgetsFlutterBinding.ensureInitialized();8 await _configureAmplify();9 runApp(const MyApp());10}11
12Future<void> _configureAmplify() async {13 // To be filled in14}15
16class MyApp extends StatelessWidget {17 const MyApp({super.key});18
19 // GoRouter configuration20 static final _router = GoRouter(21 routes: [22 GoRoute(23 path: '/',24 builder: (context, state) => const HomeScreen(),25 ),26 ],27 );28
29 30 Widget build(BuildContext context) {31 return MaterialApp.router(32 routerConfig: _router,33 debugShowCheckedModeBanner: false,34 );35 }36}37
38class LoadingScreen extends StatelessWidget {39 const LoadingScreen({super.key});40
41 42 Widget build(BuildContext context) {43 return const Scaffold(44 body: Center(45 child: CircularProgressIndicator(),46 ),47 );48 }49}50
51class HomeScreen extends StatefulWidget {52 const HomeScreen({super.key});53
54 55 State<HomeScreen> createState() => _HomeScreenState();56}57
58class _HomeScreenState extends State<HomeScreen> {59 var _budgetEntries = <BudgetEntry>[];60
61 62 void initState() {63 super.initState();64 }65
66 Future<void> _refreshBudgetEntries() async {67 // To be filled in68 }69
70 Future<void> _deleteBudgetEntry(BudgetEntry budgetEntry) async {71 // To be filled in72 }73
74 Future<void> _navigateToBudgetEntry({BudgetEntry? budgetEntry}) async {75 // To be filled in76 }77
78 double _calculateTotalBudget(List<BudgetEntry?> items) {79 var totalAmount = 0.0;80 for (final item in items) {81 totalAmount += item?.amount ?? 0;82 }83 return totalAmount;84 }85
86 Widget _buildRow({87 required String title,88 required String description,89 required String amount,90 TextStyle? style,91 }) {92 return Row(93 children: [94 Expanded(95 child: Text(96 title,97 textAlign: TextAlign.center,98 style: style,99 ),100 ),101 Expanded(102 child: Text(103 description,104 textAlign: TextAlign.center,105 style: style,106 ),107 ),108 Expanded(109 child: Text(110 amount,111 textAlign: TextAlign.center,112 style: style,113 ),114 ),115 ],116 );117 }118
119 120 Widget build(BuildContext context) {121 return Scaffold(122 floatingActionButton: FloatingActionButton(123 // Navigate to the page to create new budget entries124 onPressed: _navigateToBudgetEntry,125 child: const Icon(Icons.add),126 ),127 appBar: AppBar(128 title: const Text('Budget Tracker'),129 ),130 body: Center(131 child: Padding(132 padding: const EdgeInsets.only(top: 25),133 child: RefreshIndicator(134 onRefresh: _refreshBudgetEntries,135 child: Column(136 children: [137 if (_budgetEntries.isEmpty)138 const Text('Use the \u002b sign to add new budget entries')139 else140 Row(141 mainAxisAlignment: MainAxisAlignment.center,142 children: [143 // Show total budget from the list of all BudgetEntries144 Text(145 'Total Budget: \$ ${_calculateTotalBudget(_budgetEntries).toStringAsFixed(2)}',146 style: const TextStyle(fontSize: 24),147 )148 ],149 ),150 const SizedBox(height: 30),151 _buildRow(152 title: 'Title',153 description: 'Description',154 amount: 'Amount',155 style: Theme.of(context).textTheme.titleMedium,156 ),157 const Divider(),158 Expanded(159 child: ListView.builder(160 itemCount: _budgetEntries.length,161 itemBuilder: (context, index) {162 final budgetEntry = _budgetEntries[index];163 return Dismissible(164 key: ValueKey(budgetEntry),165 background: const ColoredBox(166 color: Colors.red,167 child: Padding(168 padding: EdgeInsets.only(right: 10),169 child: Align(170 alignment: Alignment.centerRight,171 child: Icon(Icons.delete, color: Colors.white),172 ),173 ),174 ),175 onDismissed: (_) => _deleteBudgetEntry(budgetEntry),176 child: ListTile(177 onTap: () => _navigateToBudgetEntry(178 budgetEntry: budgetEntry,179 ),180 title: _buildRow(181 title: budgetEntry.title,182 description: budgetEntry.description ?? '',183 amount:184 '\$ ${budgetEntry.amount.toStringAsFixed(2)}',185 ),186 ),187 );188 },189 ),190 ),191 ],192 ),193 ),194 ),195 ),196 );197 }198}
Go ahead and run your code now on any mobile, web, or desktop device and you should see an app with some titles and a floating action button but not much else.
1flutter run
Configure Amplify
Let's start by configuring Amplify when the application loads. At the top of the file, find the _configureAmplify
method and replace it with the following code.
1+ import 'package:amplify_api/amplify_api.dart';2+ import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';3+ import 'package:amplify_authenticator/amplify_authenticator.dart';4+ import 'package:amplify_flutter/amplify_flutter.dart';5import 'package:flutter/material.dart';6import 'package:go_router/go_router.dart';7
8+ import 'amplifyconfiguration.dart';9import 'models/ModelProvider.dart';10
11Future<void> main() async {12 WidgetsFlutterBinding.ensureInitialized();13 await _configureAmplify();14 runApp(const MyApp());15}16
17Future<void> _configureAmplify() async {18- // To be filled in19+ try {20+ // Create the API plugin.21+ //22+ // If `ModelProvider.instance` is not available, try running 23+ // `amplify codegen models` from the root of your project.24+ final api = AmplifyAPI(modelProvider: ModelProvider.instance);25+26+ // Create the Auth plugin.27+ final auth = AmplifyAuthCognito();28+29+ // Add the plugins and configure Amplify for your app.30+ await Amplify.addPlugins([api, auth]);31+ await Amplify.configure(amplifyconfig);32+33+ safePrint('Successfully configured');34+ } on Exception catch (e) {35+ safePrint('Error configuring Amplify: $e');36+ }37}
Here, we're adding the API and Authentication plugins to our app and configuring Amplify with the
generated amplifyconfiguration.dart
file.
There's one more step to complete the configuration of Auth and that is to wrap our application in the Amplify Authenticator, which will provide a pre-built authentication flow with less than 5 lines of code.
You can learn more about the Authenticator and how to customize it here.
In the MyApp
class, make the following change to add the Authenticator.
1@override2 Widget build(BuildContext context) {3- return MaterialApp.router(4- routerConfig: _router,5- debugShowCheckedModeBanner: false,6- );7+ return Authenticator(8+ child: MaterialApp.router(9+ routerConfig: _router,10+ debugShowCheckedModeBanner: false,11+ builder: Authenticator.builder(),12+ ),13+ );14 }
If you try running your app again, you should now see a sign-in screen instead of the home screen. The Authenticator guards your application so that only signed-in users can access it.
Try creating a user with the Sign Up
form before continuing on to the next step!
Manipulating data
Creating a Budget Entry
Currently, the +
button doesn't do anything. Let's hook that up to a budget entry screen where we can create
and update budget items.
First, add the following below the _HomeScreenState
class.
1class ManageBudgetEntryScreen extends StatefulWidget {2 const ManageBudgetEntryScreen({3 required this.budgetEntry,4 super.key,5 });6
7 final BudgetEntry? budgetEntry;8
9 10 State<ManageBudgetEntryScreen> createState() =>11 _ManageBudgetEntryScreenState();12}13
14class _ManageBudgetEntryScreenState extends State<ManageBudgetEntryScreen> {15 final _formKey = GlobalKey<FormState>();16 final TextEditingController _titleController = TextEditingController();17 final TextEditingController _descriptionController = TextEditingController();18 final TextEditingController _amountController = TextEditingController();19
20 late final String _titleText;21
22 bool get _isCreate => _budgetEntry == null;23 BudgetEntry? get _budgetEntry => widget.budgetEntry;24
25 26 void initState() {27 super.initState();28
29 final budgetEntry = _budgetEntry;30 if (budgetEntry != null) {31 _titleController.text = budgetEntry.title;32 _descriptionController.text = budgetEntry.description ?? '';33 _amountController.text = budgetEntry.amount.toStringAsFixed(2);34 _titleText = 'Update budget entry';35 } else {36 _titleText = 'Create budget entry';37 }38 }39
40 41 void dispose() {42 _titleController.dispose();43 _descriptionController.dispose();44 _amountController.dispose();45 super.dispose();46 }47
48 Future<void> submitForm() async {49 if (!_formKey.currentState!.validate()) {50 return;51 }52
53 // If the form is valid, submit the data54 final title = _titleController.text;55 final description = _descriptionController.text;56 final amount = double.parse(_amountController.text);57
58 if (_isCreate) {59 // Create a new budget entry60 final newEntry = BudgetEntry(61 title: title,62 description: description.isNotEmpty ? description : null,63 amount: amount,64 );65 final request = ModelMutations.create(newEntry);66 final response = await Amplify.API.mutate(request: request).response;67 safePrint('Create result: $response');68 } else {69 // Update budgetEntry instead70 final updateBudgetEntry = _budgetEntry!.copyWith(71 title: title,72 description: description.isNotEmpty ? description : null,73 amount: amount,74 );75 final request = ModelMutations.update(updateBudgetEntry);76 final response = await Amplify.API.mutate(request: request).response;77 safePrint('Update result: $response');78 }79
80 // Navigate back to homepage after create/update executes81 if (mounted) {82 context.pop();83 }84 }85
86 87 Widget build(BuildContext context) {88 return Scaffold(89 appBar: AppBar(90 title: Text(_titleText),91 ),92 body: Align(93 alignment: Alignment.topCenter,94 child: ConstrainedBox(95 constraints: const BoxConstraints(maxWidth: 800),96 child: Padding(97 padding: const EdgeInsets.all(16),98 child: SingleChildScrollView(99 child: Form(100 key: _formKey,101 child: Column(102 crossAxisAlignment: CrossAxisAlignment.start,103 children: [104 TextFormField(105 controller: _titleController,106 decoration: const InputDecoration(107 labelText: 'Title (required)',108 ),109 validator: (value) {110 if (value == null || value.isEmpty) {111 return 'Please enter a title';112 }113 return null;114 },115 ),116 TextFormField(117 controller: _descriptionController,118 decoration: const InputDecoration(119 labelText: 'Description',120 ),121 ),122 TextFormField(123 controller: _amountController,124 keyboardType: const TextInputType.numberWithOptions(125 signed: false,126 decimal: true,127 ),128 decoration: const InputDecoration(129 labelText: 'Amount (required)',130 ),131 validator: (value) {132 if (value == null || value.isEmpty) {133 return 'Please enter an amount';134 }135 final amount = double.tryParse(value);136 if (amount == null || amount <= 0) {137 return 'Please enter a valid amount';138 }139 return null;140 },141 ),142 const SizedBox(height: 20),143 ElevatedButton(144 onPressed: submitForm,145 child: Text(_titleText),146 ),147 ],148 ),149 ),150 ),151 ),152 ),153 ),154 );155 }156}
Then, add the route to the global GoRouter
.
1static final _router = GoRouter(2 routes: [3 GoRoute(4 path: '/',5 builder: (context, state) => const HomeScreen(),6 ),7+ GoRoute(8+ path: '/manage-budget-entry',9+ name: 'manage',10+ builder: (context, state) => ManageBudgetEntryScreen(11+ budgetEntry: state.extra as BudgetEntry?,12+ ),13+ ),14 ],15 );
Finally, hook up the +
button to navigate to this route when it's pressed.
1Future<void> _navigateToBudgetEntry({BudgetEntry? budgetEntry}) async {2- // To be filled in3+ await context.pushNamed('manage', extra: budgetEntry);4 }
Now, if you click the +
button you should be sent to the new screen.
Try creating a budget entry at this point! You should be able to enter values in the fields, and get errors if a value entered does not match the schema.
When returning to the home screen, though, our budget entry is nowhere to be found! Let's fix that.
Loading remote budget entries
In the _HomeScreenState
class, we have a method called _refreshBudgetEntries
. Let's populate
that to update the state class's _budgetEntries
variable every time we call it.
To that, we'll use Amplify's model helpers which provide a quick way to create CRUD requests for
your data. Here we're using the ModelQueries.list
helper to retrieve all the budget entries
and then passing the GraphQL request to the API category which will automatically transform
the response into a list of BudgetEntry
types.
1Future<void> _refreshBudgetEntries() async {2- // To be filled in3+ try {4+ final request = ModelQueries.list(BudgetEntry.classType);5+ final response = await Amplify.API.query(request: request).response;6+7+ final todos = response.data?.items;8+ if (response.hasErrors) {9+ safePrint('errors: ${response.errors}');10+ return;11+ }12+ setState(() {13+ _budgetEntries = todos!.whereType<BudgetEntry>().toList();14+ });15+ } on ApiException catch (e) {16+ safePrint('Query failed: $e');17+ }18 }
Now we can modify initState
so that the entries are refreshed every time the homepage loads.
1@override2 void initState() {3 super.initState();4+ _refreshBudgetEntries();5 }
And finally, we also want to refresh the entries anytime we return from the create/update screen so that any changes are reflected in the list.
1Future<void> _navigateToBudgetEntry({BudgetEntry? budgetEntry}) async {2 await context.pushNamed('manage', extra: budgetEntry);3+ // Refresh the entries when returning from the4+ // budget entry screen.5+ await _refreshBudgetEntries();6 }
Deleting budget entries
If you notice, right now you can swipe right on a budget entry to dismiss it. Let's modify the code so that it properly deletes the entry when dismissed.
Modify the _deleteBudgetEntry
function as follows. Again, we'll use Amplify model helpers to simplify
the creation of the GraphQL request.
1Future<void> _deleteBudgetEntry(BudgetEntry budgetEntry) async {2- // To be filled in3+ final request = ModelMutations.delete<BudgetEntry>(budgetEntry);4+ final response = await Amplify.API.mutate(request: request).response;5+ safePrint('Delete response: $response');6+ await _refreshBudgetEntries();7 }
If you've made it this far, congratulations! You've built a fully working budget management application 🥳