Since our categories are currently hard-coded into the screen directly, we cannot update them without updating the code. This is not ideal, so let's fetch the categories from the API instead.
In this lesson, we will:
- Install HTTP package
- Create our Category Model
- Grant the necessary permissions to the API
- Fetch the categories from the API
Let's dive in!
Installing HTTP Package
Installing packages in Flutter is as easy as using Composer with PHP. All we have to do is type:
flutter pub add http
And it will be installed for us. Once it's installed, we can use it in our project.
Creating our Category Model
Before making our first API call, we must create a Model. This model will map the API response to a Dart object.
In other words, we will create a Model that looks like an Eloquent model, but instead of a Database connection, it will be used to map the API response.
lib/screens/categories/categories_list.dart
import 'package:flutter/material.dart'; 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']); }} // ...
Our model has two properties, id
and name
. The constructor takes these two properties and assigns them to the class properties.
We have a factory method to map the API response to our Model.
Granting the necessary permissions to the API
Our final preparation step - permissions. Mobile applications require permission to do various things. In our case, we need to add Internet permission to our AndroidManifest file.
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET"/> <application android:label="laravel_api_flutter_app" android:name="${applicationName}" <!-- ... -->
Note: iOS does not require any internet access permissions.
Fetching the categories from the API
Now that we have our Model and Permissions sorted out - we can make our first API call.
Note: Before we start, make sure your Laravel application is running, and you have ngrok
or some other tunneling service running. Localhost will not work for mobile applications!
Let's replace our class properties with a new List
of Category
Model:
lib/screens/categories/categories_list.dart
// ... final List<String> categories = ['Electronics','Fashion','Home & Garden','Sporting Goods']; int clicked = 0; Future<List<Category>>? futureCategories; // ...
Next, we need a function to fetch the categories from the API:
lib/screens/categories/categories_list.dart
// ... class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; 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')); // <-- Change this to your ngrok URL 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(); } // ...
In the fetchCategories
function, we are making a GET
request to our API endpoint. We then decode the response and check if the response contains the data
key and if the value of that key is a list.
Note: If you turn off Laravel Resource wrapping, you can remove the data
key check. We left it in as it is the default behavior.
Now that we have the fetch function, we need to call it as soon as the widget is created:
lib/screens/categories/categories_list.dart
// ... class CategoriesListState extends State<CategoriesList> { Future<List<Category>>? futureCategories; 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(); } @override void initState() { super.initState(); futureCategories = fetchCategories(); } // ...
In the initState
method, we call the fetchCategories
function and assign the result to the futureCategories
property.
Finally, we need to update our build
method to use the futureCategories
property:
lib/screens/categories/categories_list.dart
// ... @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), ); }); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } return CircularProgressIndicator(); }), ); }// ...
You might have noticed that we used the FutureBuilder
and future
keywords in multiple places. Here, we are telling Flutter to wait for the futureCategories
to resolve before rendering the UI.
In fact, you can treat the future variable
as a promise in JavaScript. It will resolve once the API call is done and the data is available. Until then, we are showing a CircularProgressIndicator
.
Here's the complete categories_list.dart
file:
lib/screens/categories/categories_list.dart
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; 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(); } @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), ); }); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } return CircularProgressIndicator(); }), ); }}
Modifying Laravel API
Before we can run the screen and check that it works - our Laravel code needs one small change. We need to remove the auth
middleware from the api
middleware group:
routes/api.php
// ... Route::group(['middleware' => 'auth:sanctum'], function () {/// Route::group([], function () {/// Route::apiResource('categories', CategoryController::class); Route::apiResource('transactions', TransactionController::class);}); // ...
Now, if you run your application, you should see the categories fetched from the API and displayed on the screen:
Note: If you don't see any categories, make sure your Laravel application is seeded!
In the next lesson, we will add an Edit button to the categories and implement the functionality to edit them.
Check out the GitHub Commit for this lesson.
No comments yet…