Now that we have our categories, we can work on the edit form:
This will show us how to implement the following:
- A form into our screen
- Basic validation
- API call to update the category
- Modal forms
Adding Edit Icon
First, we want to add an Icon Button to our List:
return ListTile( title: Text(category.name), trailing: IconButton(onPressed: () => print("Clicked edit"), icon: Icon(Icons.edit)),);
This will add an edit icon to the right side of the list item:
Adding Modal Form
To add the form, we should first prepare our Widget to show the form:
// ...class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; final _formKey = GlobalKey<FormState>(); late Category selectedCategory; final categoryNameController = TextEditingController(); // ...}
This will add a form key, a selected category, and a controller for the category name. Let's dive into why we need these:
- Form Key: This key will be used to validate the form. We will use it to check if the form is valid before submitting it. This is a Flutter best practice.
- Selected Category: This will store the category we are editing and be used to update it.
- Category Name Controller: This will control the text field in the form. We will use this to set the initial value and get the text field's value. This is a Flutter best practice.
Okay, now let's add the form to our IconButton
. For this, we will use:
-
showModalBottomSheet
: This will show a modal bottom sheet with the form. -
Form
: This will wrap our form fields and maps to our_formKey
. -
TextFormField
: This will show the category name field. -
ElevatedButton
: This will be used to submit the form.
Let's add them to our IconButton
trailing property:
return ListTile( title: Text(category.name), trailing: IconButton(onPressed: () => print("Clicked edit"), icon: Icon(Icons.edit)), trailing: IconButton( onPressed: () { selectedCategory = category; categoryNameController.text = category.name; showModalBottomSheet( context: context, builder: (BuildContext context) { return Container( padding: EdgeInsets.all(20), child: Form( key: _formKey, child: Column( children: [ Text('Edit Category'), TextFormField( decoration: InputDecoration( labelText: 'Category Name', ), controller: categoryNameController, validator: (String? value) { if (value == null || value.isEmpty) { return 'Please enter category name'; } return null; }, ), Padding( padding: EdgeInsets.only(top: 20), child: ElevatedButton( onPressed: () { saveCategory(); }, child: Text('Update'), ), ), ], ), ), ); }); }, icon: Icon(Icons.edit)),);});
Since we currently have only one Input field - let's look at it in detail:
TextFormField( decoration: InputDecoration( // this is used to add a label to the input field labelText: 'Category Name', ), controller: categoryNameController, // this uses the controller to control the input field (like setting the initial value, getting the value) validator: (String? value) { // this is used to validate the input field if (value == null || value.isEmpty) { return 'Please enter category name'; } return null; // if there are no errors - return null }, ),
Saving the Form
If we try to hot-reload the app - we will get an error. This is because we are calling the saveCategory
function that does not exist:
child: ElevatedButton( onPressed: () { saveCategory(); }, child: Text('Update'),),
So let's create it:
// ... class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; final _formKey = GlobalKey<FormState>(); late Category selectedCategory; final categoryNameController = TextEditingController(); Future<List<Category>> fetchCategories() async { final http.Response response = await http.get( Uri.parse('https://78ac-78-58-236-130.ngrok-free.app/api/categories')); final Map<String, dynamic> data = json.decode(response.body); if (!data.containsKey('data') || data['data'] is! List) { throw Exception('Failed to load categories'); } List categories = data['data']; return categories.map((category) => Category.fromJson(category)).toList(); } Future saveCategory() async { final form = _formKey.currentState; if (!form!.validate()) { return; } String url = 'https://78ac-78-58-236-130.ngrok-free.app/api/categories/${selectedCategory.id}'; // <-- Set your API URL final http.Response response = await http.put( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode(<String, String>{ 'name': categoryNameController.text, }), ); Navigator.pop(context); } // ...
Now, if we re-load the app and click on the edit icon - we will see the form:
We should be able to update the name. But once the form closes, we can't see it. That's because we are not yet updating the list.
Note: We will work on real-time updates a few lessons later.
For now, go back to the Register screen and press the Register
button. It should call the API again and retrieve the updated list.
Here's the complete code for the CategoriesList
:
import 'package:flutter/material.dart';import 'package:http/http.dart' as http;import 'dart:convert'; class Category { final int id; final String name; Category({required this.id, required this.name}); factory Category.fromJson(Map<String, dynamic> json) { return Category(id: json['id'], name: json['name']); }} class CategoriesList extends StatefulWidget { const CategoriesList({super.key}); @override CategoriesListState createState() => CategoriesListState();} class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; final _formKey = GlobalKey<FormState>(); late Category selectedCategory; final categoryNameController = TextEditingController(); Future<List<Category>> fetchCategories() async { final http.Response response = await http.get( Uri.parse('https://ec90-78-58-236-130.ngrok-free.app/api/categories')); final Map<String, dynamic> data = json.decode(response.body); if (!data.containsKey('data') || data['data'] is! List) { throw Exception('Failed to load categories'); } List categories = data['data']; return categories.map((category) => Category.fromJson(category)).toList(); } Future saveCategory() async { final form = _formKey.currentState; if (!form!.validate()) { return; } String url = 'https://ec90-78-58-236-130.ngrok-free.app/api/categories/${selectedCategory.id}'; final http.Response response = await http.put( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode(<String, String>{ 'name': categoryNameController.text, }), ); Navigator.pop(context); } @override void initState() { super.initState(); futureCategories = fetchCategories(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Categories List'), ), body: FutureBuilder<List<Category>>( future: futureCategories, builder: (context, snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { Category category = snapshot.data![index]; return ListTile( title: Text(category.name), trailing: IconButton( onPressed: () { selectedCategory = category; categoryNameController.text = category.name; showModalBottomSheet( context: context, builder: (BuildContext context) { return Container( padding: EdgeInsets.all(20), child: Form( key: _formKey, child: Column( children: [ Text('Edit Category'), TextFormField( decoration: InputDecoration( labelText: 'Category Name', ), controller: categoryNameController, validator: (String? value) { if (value == null || value.isEmpty) { return 'Please enter category name'; } return null; }, ), Padding( padding: EdgeInsets.only(top: 20), child: ElevatedButton( onPressed: () { saveCategory(); }, child: Text('Update'), ), ), ], ), ), ); }); }, icon: Icon(Icons.edit)), ); }); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } return CircularProgressIndicator(); }), ); }}
In the next lesson, we will examine more code optimizations. In this case, we will move the Edit Category form into a separate widget file.
Check out the GitHub Commit for this lesson.
No comments yet…