Make a food delivery app with DollarBackend + Flutter (1/3)
In these series of articles, I'll be teaching you how to make a food delivery app for iOS/Android using Strapi 4 as a ready-made backend and Dart + Flutter for writing the app.
The final app will have the following features:
- view all menu items (part 1)
- view a menu item's details (part 2)
- how to place an order (part 3)
- (bonus) how to see current orders and mark them as done using Strapi Dashboard
To follow along, you'll need to know the basics of Dart and Flutter. If you're unfamiliar with Flutter,
they have an official getting started code lab.
You can always look at the code in this article's GitHub repository.
With that being said, let's get to making our app.
Set up a new Flutter project
Create a new Flutter project.
We will be adding pages into a pages
folder not to move the files around in the next tutorial.
Let's create a simple home page in pages/home.dart
:
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// TODO: add home page
return Container();
}
}
Deciding how to use Strapi SDK
Strapi supports both GraphQL and REST. REST API endpoints are automatically created by Strapi out of the box, so we'll be using REST API today.
There are different ways to use the REST API. You can either make requests using an HTTP client, such as dio or via a library that wraps api calls into methods, like dart_strapi does.
We'll be making plain API calls in this tutorial to better understand Strapi's API. In the future, however, you would probably like to use a ready made library not to duplicate the logic and hunt for bugs twice.
Adding data to Strapi
Strapi supports three Content-types:
- Collection types - can manage several entries
- Single types - can manage one entry
- Components - a data structure that can be used in multiple collection types and single types.
Out of the box Strapi comes only with 1 Collection type User. This isn't very useful, as we're trying to have menu items there, so we'll need to create a new type. A restaurant's menu usually has more than one item, so we're choosing to create a Collection type.
Let's call a collection type that will be storing menu items "Menu Item". For now it needs 3 fields (we can always add more fields later if needed):
- name - a name of the dish (e.g. "Salad")
- photo (optional) - an optional photo of the dish
- price - the price of a dish.
Creating Menu Item Collection type
Go to Strapi's admin and select "Content-Type builder" -> "Create a new collection type"
Set the Collection type's name to "Menu Item", then click "Continue"
You'll see a list of field types our "Menu Item" can have. Let's add a Text field called "name" (select "Text field" option)
You can also click on "Advanced settings". These have helpful hints on what they are under the fields. Feel free to play around with them.
Click "Finish" to finish editing this field. You'll see all field types.
We need to add 2 more field types: photo (a Media field
) and price (a Number field
with Number format
= float
and Advanced Settings -> Required field
checked). Try adding those on your own.
When your're done adding fields, click "Finish". Strapi will restart the server and you'll see a new Collection type in the dashboard.
Creating the API token
Let's try making an API request to Strapi.
To list all Menu Items, we need to GET
/api/menu-items
:
curl http://strapi-flutter-demo.orena.io/api/menu-items
To that Strapi responds with a Forbidden Error:
{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {}
}
}
This is because Strapi requires Authorization: Bearer <api token>
header to be present with every request.
This token controls what each app (not each user!) can and cannot do.
To create this token, go to "Settings" -> "API Tokens" and click on the "Create new API Token" button. Enter the token's name and description (could be anything, like "A flutter app")
Make sure to write down your API token elsewhere. This token is only shown once. Someplace like a constants.dart
file in the Flutter project would be cool.
Click "Save" to save your api token.
Let's try making the API request again, this time using a working API token:
curl \
-H 'Authorization: bearer <api token>' \
http://strapi-flutter-demo.orena.io/api/menu-items
Result:
{
"data": [],
"meta": {
"pagination": {
"page": 0,
"pageSize": 0,
"pageCount": 0,
"total": 0
}
}
}
Great, everything works! Moving on
a tip: if you need to separate production and development environments in your app, you can keep API tokens in environment variables and use something like flutter dotenv to load them at runtime
Why doesn't Strapi return photos?
I have added a few entries to Menu Items in Strapi dashboard. GET
ting /api/menu-items
again now returns the results:
{
"data": [
{
"id": 1,
"attributes": {
"name": "Salad",
"price": 5.73,
"createdAt": "2022-10-04T16:20:55.555Z",
"updatedAt": "2022-10-04T16:25:57.641Z",
"publishedAt": "2022-10-04T16:25:57.639Z"
}
}
// .. other similar items
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 9
}
}
}
We have created a photo
field earlier that I filled in. Where is it?
For performance reasons, media fields and foreign relations are not populated by default. We need to use the populate parameter to do that.
Populate parameter can be a string, an array or an object.
If populate is a string "*"
, Strapi will populate all object's fields.
Populate can be more specific, like "photo"
. In this case, Strapi will populate only the "photo" field.
If populate is an array, like ["photo", "description"]
, Strapi will populate both "photo" and "description" fields.
Populate can also be an object, such as:
{
"populate": {
"author": {
"populate": ["company"]
}
}
}
In that case author
field will be populated and author
's company
subfield will be populated.
If you want to learn more about Strapi's populate field, check out Strapi's documentation on populate
Showing menu items in the app
To recap, we have
- setup a new Flutter project
- added a home page to our app
- created a new Collection Type named Menu Item
- found out how to use Strapi's API
One final thing left is: how to show results from Strapi in our app?
I have created a new folder called domain
. This folder will contain classes representing data structures the app is working with.
You don't have to create this folder if you don't want to, I just think structuring a project like that makes it a bit cleaner.
Let's create MenuItemData
class in domain/menu.dart
import 'package:flutter/foundation.dart';
import 'package:strapi_food_app/constants.dart';
class MenuItemData {
int id;
String name;
PhotoUrl? photo;
double price;
MenuItemData({
required this.id,
required this.name,
required this.price,
this.photo,
});
}
class PhotoUrl {
String? original;
String? small;
PhotoUrl({
this.original,
this.small,
});
}
MenuItemData
and PhotoUrl
are helper classes to store deserialized menu data. Having them, I can create a widget to show a grid of menu items (it will be in widgets/menu_grid.dart):
class MenuGrid extends StatelessWidget {
final List<MenuItemData> items;
const MenuGrid({
required this.items,
super.key,
});
@override
Widget build(BuildContext context) {
// build a grid of [ItemWidget]
}
}
class ItemWidget extends StatelessWidget {
final MenuItemData menuItem;
const ItemWidget({
required this.menuItem,
super.key,
});
@override
Widget build(BuildContext context) {
// a widget showing [MenuItemData] in the UI
}
}
Widget ItemWidget
takes a MenuItemData
and creates a square menu item with name, price and an optional photo. MenuGrid
takes a List<MenuItemData>
and shows them as a grid of ItemWidget
s.
Now we can use those widgets in pages/home.dart:
import 'package:flutter/material.dart';
import 'package:strapi_food_app/repositories/menu_items.dart';
import '../widgets/menu_grid.dart';
import '../domain/menu.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<MenuItemData> _items = [];
@override
void initState() {
super.initState();
_fetchMenuItems();
}
Future<void> _fetchMenuItems() async {
// TODO: fetch menu items here
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Food app"),
),
body: RefreshIndicator(
onRefresh: _fetchMenuItems,
child: MenuGrid(items: _items),
),
);
}
}
When HomePage
is initialized it calls initState
, which calls _fetchMenuItems
. When a user swipes up to refresh the page, RefreshIndicator.onRefresh
is called, which calls _fetchMenuItems
If you run the app right now, the screen will be empty. This is because we haven't added anything to _fetchMenuItems
Fetching data from Strapi
For retrieving data from Flutter we will create a new
MenuItemsRepository
class in the repositories
folder.
Creating a new class with wrapper methods is better than calling REST directly in code.
Code in a class can be reused and analyzed by Dart, giving us advantages Dart's type system provides.
import 'package:dio/dio.dart';
import 'package:strapi_food_app/constants.dart';
import '../domain/menu.dart';
class MenuItemsRepository {
static Future<List<MenuItemData>> getMenuItems() async {
// TODO: get MenuItemData from Strapi
}
}
MenuItemsRepository.getMenuItems
method will get a list of menu items from Strapi.
Let's make a request using curl to /api/menu-items?populate=photo
to see the json Strapi returns.
{
"data": [
{
"id": 1,
"attributes": {
// ...
"name": "Salad",
"price": 5.73,
"photo": {
"data": {
"id": 1,
"attributes": {
// ...
"name": "sina-piryae-bBzjWthTqb8-unsplash.jpg",
"formats": {
"thumbnail": {
// ...
"url": "/uploads/thumbnail_sina_piryae_b_Bzj_Wth_Tqb8_unsplash_26ec912a47.jpg"
},
"small": {
// ...
"url": "/uploads/small_sina_piryae_b_Bzj_Wth_Tqb8_unsplash_26ec912a47.jpg"
}
},
"url": "/uploads/sina_piryae_b_Bzj_Wth_Tqb8_unsplash_26ec912a47.jpg"
}
}
}
}
}
],
"meta": {
"pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 9 }
}
}
To make output JSON more readable, I have removed the fields that we will not be using today. A reminder: we are looking for name, price and a photo url.
To make a REST request in Flutter we'll be using dio
var dio = Dio();
var response = await dio.get(
// kServerUrl is a constant with server url
'$kServerUrl/api/menu-items',
options: Options(
// authorization required, otherwise Strapi returns an error
headers: {'Authorization': 'Bearer $kApiToken'},
),
// we need to populate a media field 'photo'
queryParameters: {"populate": "photo"},
);
var data = response.data["data"];
By default dio deserializes response.data
from text to Dart's dynamic
type.
We know that all the data we need is in the data
field, so we can assign var data
to response.data["data"]
.
We could deserialize it right in the repository, but we already have a class to represent
this data: MenuItemData
. Let's deserialize var data
there:
// a method of MenuItemData
static PhotoUrl fromStrapiList(dynamic data) {
List<MenuItemData> items = List.empty(growable: true);
for (var d in data) {
try {
var attrs = d["attributes"];
// dart doesn't like when we're trying to parse '10'
var price = double.parse(attrs["price"].toString());
var item = MenuItemData(
id: d["id"],
name: attrs["name"],
price: price,
);
var photo = attrs["photo"]?["data"];
if (photo != null) {
var url = photo["attributes"]["url"];
// strapi returns relative urls, like '/uploads/coffee.jpg'
item.photo = PhotoUrl(original: "$kServerUrl$url");
var formats = photo["attributes"]["formats"];
if (formats != null) {
var small = formats["small"]["url"];
item.photo?.small = "$kServerUrl$small";
}
}
items.add(item);
} catch (e) {
debugPrint("Error deserializing menuItem from Strapi: $e");
continue;
}
}
return items;
}
In our case, data
will have an array of Menu item.
I have put each deserialization inside of a loop in a try catch block not to loose
the records we could deserialize.
Parsing name
and price
is pretty simple: they're the object's direct children.
photo
field needs two urls: for an original photo and for a thumbnail
(as we have decided when creating a PhotoUrl
class).
We need to check that photo
field is not null (photos are optional for us) and if it isn't,
try getting photo's url
and thumbnail.url
.
Strapi returns relative urls,
and we need to prefix it with our server name to make it valid.
When all the results are ready, we can return List<MenuItemData>
back to MenuItemsRepository.getMenuItems
.
getMenuItems
now looks like this:
static Future<List<MenuItemData>> getMenuItems() async {
var dio = Dio();
var response = await dio.get(
'$kServerUrl/api/menu-items',
options: Options(
headers: {'Authorization': 'Bearer $kApiToken'},
),
queryParameters: {"populate": "photo"},
);
var data = response.data["data"];
var items = MenuItemData.fromStrapiList(data);
return items;
}
One last thing: we need to call getMenuItems
from _fetchMenuItems
in the HomePage
widget:
Future<void> _fetchMenuItems() async {
var items = await MenuItemsRepository.getMenuItems();
_items = items;
if (mounted) {
setState(() {});
}
}
In _fetchMenuItems
we first fetch menu items, then setState
to
re-render the component.
Running the app now will show a list of menu items that we got from Strapi. Our first part of the app is complete!
To sum up, we have:
- created a new Flutter project
- created a "Menu Items" Collection type in Strapi
- created an API token in Strapi
- fetched Menu Items using the API
- deserialized API's response into
MenuItemData
- showed
List<MenuItemData>
on the main page
In the next article, we will be adding menu items' details and showing them in our app
We can host Strapi and other open source backends for you! No need to set up and maintain your own server. Sign up here, no credit card required