Courses

[NEW] Flutter 3 Mobile App with Laravel 12 API

Real Time List Updates

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:

  1. A list of categories
  2. Api Service to fetch categories
  3. Initialize the list of categories
  4. 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 our CategoryEdit 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.

Previous: Partial Widgets - Code Refactoring

No comments yet…

avatar
You can use Markdown