A UI is excellent but useless if it doesn't reflect changes immediately. In this chapter, we'll learn how to update our app in real time when the data changes on the server.
For this, we will use Providers, which allow us to listen to changes in the data and update the UI accordingly.
Here's what we'll do:
- Install Flutter package
provider
- Create our first CategoryProvider
- Refactor our CategoryList to use the CategoryProvider
- Refactor our Main Screen to support Providers
Install Flutter Package
To install our package, we should run this command:
flutter pub add provider
Creating Our First Provider
Now, we can create our First provider. We will create a new file in the providers
folder called category_provider.dart
:
import 'package:flutter/material.dart'; class CategoryProvider extends ChangeNotifier {}
This time, we use ChangeNotifier
instead of StatefulWidget
because we don't need to build a widget; we just need to notify the listeners when the data changes.
From here, we need a few things from our provider:
- A list of categories
- Api Service to fetch categories
- Initialize the list of categories
- A method to update categories
So, let's start by adding the list of categories and registering the API service:
class CategoryProvider extends ChangeNotifier { List<Category> categories = []; late ApiService apiService; CategoryProvider() { apiService = ApiService(); init(); } Future init() async { try { categories = await apiService.fetchCategories(); } catch (e) { print('Failed to load categories: $e'); } notifyListeners(); }}
In this code, we have only one new method: notifyListeners()
. This method will notify all the listeners that the data has changed and that they should rebuild. In a way, treat this like an Event DataChangedPleaseUpdate
, as this is our main focus.
Last, we need a way to update our Category:
class CategoryProvider extends ChangeNotifier { // ... Future updateCategory(Category category) async { try { Category updatedCategory = await apiService.saveCategory(category); int index = categories.indexOf(category); categories[index] = updatedCategory; notifyListeners(); } catch (e) { print('Failed to update category: $e'); } }}
We call the API service to save the category and then update the reactive categories list to include our changes. Of course, we have to notify the listeners in the end.
Enabling Reactivity in Main Screen
Once we have our provider - we need to register it. With Flutter, this is done in our main.dart
file. We need to wrap our MaterialApp
with MultiProvider
:
import 'package:laravel_api_flutter_app/providers/category_provider.dart';import 'package:provider/provider.dart'; // ... Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Login(), routes: { '/login': (context) => Login(), '/register': (context) => Register(), '/categories': (context) => CategoriesList(), }, return MultiProvider( providers: [ ChangeNotifierProvider<CategoryProvider>( create: (context) => CategoryProvider()), ], child: MaterialApp( title: 'Welcome to Flutter', home: Login(), routes: { '/login': (context) => Login(), '/register': (context) => Register(), '/categories': (context) => CategoriesList(), }, )););
This approach makes it easy to add more providers in the future. We can add them to the list of providers.
Refactoring Category List
Next, we will look at our categories list, and we must make it reactive. This is where things start to get different. For our Provider to be used, we have to tell our Widget that it is a Consumer
of the Provider.
This can be done by wrapping our Scaffold
in a Consumer
widget:
// ... @override Widget build(BuildContext context) { return Consumer<CategoryProvider>( builder: (context, provider, child) { List<Category> categories = provider.categories; return Scaffold( appBar: AppBar( title: Text('Categories List'), ), body: ListView.builder( itemCount: categories.length, itemBuilder: (context, index) { Category category = categories[index]; return ListTile( title: Text(category.name), trailing: IconButton( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) { return CategoryEdit(category, provider.updateCategory); }, ); }, icon: Icon(Icons.edit), ), ); }, ), ); }, );
In this code, you should take a look at:
return Consumer<CategoryProvider>( builder: (context, provider, child) { List<Category> categories = provider.categories;
This instructs our app to treat this widget as a Consumer of the CategoryProvider
. Then, we have context, provider, child
available. We will use provider.categories
to get the list of categories from our provider and not from our API.
Let's remove some unnecessary code from our CategoryList
:
// ... class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; ApiService apiService = ApiService(); @override void initState() { super.initState(); futureCategories = apiService.fetchCategories(); } // ...
These are no longer needed, as we are using the Provider to fetch the data.
Modifying our Edit Category for Reactivity
In our Category Edit file, we call API directly, which will not trigger any reactivity. For that to work, we have to change a few things:
- Add a
callback
function to ourCategoryEdit
widget constructor - this will be called instead of our API service - Remove our API service usage from the
CategoryEdit
widget - Add navigation to close the modal after the category is updated
All of this is just a couple lines of code:
class CategoryEdit extends StatefulWidget { final Category category; final Function categoryCallback; const CategoryEdit(this.category, {super.key}); const CategoryEdit(this.category, this.categoryCallback, {super.key}); @override CategoryEditState createState() => CategoryEditState();} class CategoryEditState extends State<CategoryEdit> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final categoryNameController = TextEditingController(); ApiService apiService = ApiService(); String errorMessage = ''; Future saveCategory(context) async { final form = _formKey.currentState; if (!form!.validate()) { return; } apiService .saveCategory(widget.category.id, categoryNameController.text) .then((dynamic response) => Navigator.pop(context)) .catchError((error) { setState(() { errorMessage = 'Failed to update category'; }); }); widget.category.name = categoryNameController.text; await widget.categoryCallback(widget.category); Navigator.pop(context); } // ...}
Once these are done, you should see an error happening at:
widget.category.name = categoryNameController.text;
So, let's fix that.
Modifying Our Category Model
We have to change our Model a little bit:
lib/models/category.dart
// ... final int id;final String name; Category({required this.id, required this.name});int _id;String _name; int get id => _id;set id(int id) => _id = id; String get name => _name;set name(String name) => _name = name; Category({required int id, required String name}) : _id = id, _name = name;
Once these are applied - our app should not complain about the Category
model missing the id
and name
.
Modifying Our API Service
Last step before we test our functionality - we have to modify our API service to accept Category
object instead of id
and name
:
Future saveCategory(id, name) async {Future saveCategory(Category category) async { String url = '$baseUrl/api/categories/$id'; String url = '$baseUrl/api/categories/${category.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': name, 'name': category.name, }), ); if (response.statusCode != 200) { throw Exception('Failed to update category'); } final Map<String, dynamic> data = json.decode(response.body); return Category.fromJson(data['data']); }
We can finally open our application and update one of the categories. We updated Food (Vegan)
to be Food (Vegan) - Gluten free
, and that change was reflected immediately in our UI:
In our next lesson, we will add more CRUD actions - specifically Delete with Confirmation.
Check out the GitHub Commit for this lesson.
No comments yet…