Logo
Back

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"

Click "Content-Type Builder" -> "Create new collection type" to start adding a new collection type

Set the Collection type's name to "Menu Item", then click "Continue"

Enter collection type name

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)

Add a new field with name = "name" and type = "Short text"

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.

Example using Strapi's Advanced Settings

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.

Click "Add another field" to add another field. After adding all fields, click "Save" in the top right corner

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.

Go to "Settings" -> "API tokens" -> "Create" to create a new api token. The API token will only be shown once. After you've written down the token elsewhere, click "Save"

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. GETting /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

  1. setup a new Flutter project
  2. added a home page to our app
  3. created a new Collection Type named Menu Item
  4. 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 ItemWidgets.

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

Blog
Writing cool articles about backends