How to build a multi-platform native app in less than an hour using Flutter and Strapi

Using Strapi a headless CMS that helps you build API in minutes and Flutter a Google UI toolkit based on Dart that natively compile application for all devices. We will build an app in less than one hour

How to build a multi-platform native app in less than an hour using Flutter and Strapi

I love to discover new technologies, and I discovered Strapi like a year ago but never had the chance to build something from scratch until a couple of months ago. I wanted to create my own up for a movement call Stay Active and it was the perfect occasion to use Strapi but I needed the app itself... and I didn't want to build it twice, one for Android and one for iOS and I remembered we did a proof of concept on the project, replicating our app One Pay FX using Flutter,  the results were impressive so I had already selected the technologies I needed.

But... what is Strapi and what is Flutter?

Strapi is an open-source, Node.js based, headless CMS to manage content and make it available through a fully customizable API. It is designed to build practical, production-ready Node.js APIs in hours instead of weeks.

And what is Flutter?

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Sounds like heaven isn't it? After get some experience I decided to do a Friday Tech Talk in everis UK  bringing a quick introduction to both of them where we will built an app in less than an hour

PRE-Requisites

First of all we need to setup the environment depending on if you are using Mac OS, Windows, Linux or Chrome OS it will vary a bit, but the essence is the same. As the intention of this post is not to teach you how to install but how to code I will link you directly to the official guides, they will always be up to date.

If you have any issue during the installation leave a comment and I will help you!

Install Flutter:

  1. Install visual studio (if you don't have it already :O!):

    • https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=vs-2019
  2. Install Flutter

    • https://flutter.dev/docs/get-started/install
    • Setup Visual Studio for Flutter: https://flutter.dev/docs/get-started/editor?tab=vscode
  3. Install Strapi

    • https://strapi.io/documentation/v3.x/getting-started/installation.html

For Strapi, I personally use the CLI as provides me with faster development (it has hot reload) and Docker does not consume my laptop resources.  But feel free :)

For deployment, I do bundle it into a docker and deploy the docker using an external DB.

For testing the flutter app  I recommend to use a real device, the feedback is better and it uses less resources than the simulator.

Hands on

We will create an app that show the user a list of the best pubs around the everis UK office. So that we can decide easily where to go every Friday (yeah sure.. just on Fridays  🙄)

First we will create the model and APIs on Strapi.

Create Strapi project

yarn create strapi-app everis-fridays-pubs --quickstart

Navigate to http://localhost:1337/admin  Complete the form to create the first Administrator user and click Ready to start.

That's all we have now a full set of API ready to be consumed

Strapi welcome screen

Create Pub Content type

Now is time to create the Pub content type that the app will retrieve and show to the users.

  • Go to Content-Types Builder
  • Click "Create new Collection Type"
  • Enter your collection name, this example "Pubs"

We need to define the fields of our model "Pub" for this basic example we will create. User lowercase otherwise you'll have conflict with Flutter.

  • name - Text
  • address - Text
  • picture - Media
  • avgPrice - Number

After click "Save" our server will auto restart to pick up the changes.

In Collection Type you'll see now the "Pubs" collection. Go there and create a few pubs to be retrieved from the API.

You will probably be tempted to test it by using Postman, but...

There is one last step before the pubs API can be used. Permission your API! As you can see the API is working as expected as by the default Strapi creates the APIs with restricted permissions. For the simplicity of this example we will give public permission to the Pub API.

Go to "Roles & Permissions" -> "Public" and give permission to your API.

If you try now, you'll see how your data is returned when calling the /GET

What is happening behind the scenes? Strapi is nothing else than a nodeJS project that we can tune or completely code from our IDE. Actually any custom query will be coded in there. Any change in the code is reflected on the admin panel and vice-versa.

Create Flutter App

Time to switch to the front end and create our app.

  • flutter create everis_fridays_pubs_app
  • cd everis_fridays_pubs_app
  • flutter devices
  • flutter run

et voilà ! we have already an app!

Time to code! Go to lib/main.dart and delete all the content. Replace the content to with:

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(EverisFridayappEverisFridayApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Everis Fridays Pub',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Everis Fridays Pub'),
        ),
        body: Center(
          child: Text('Pubs will be listed here'),
        ),
      ),
    );
  }
}

In order to call the API to retrieve the pubs and handle the JSONs we need to libraries installed in our pubspec.yaml  

In dependencies add

  json_annotation: ^3.0.1
  http: ^0.12.1

and in dev_dependencies

  build_runner: ^1.10.0
  json_serializable: ^3.3.0

It should look like

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.3
  json_annotation: ^3.0.1
  http: ^0.12.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.0
  json_serializable: ^3.3.0

Save and Visual studio will automatically download the dependencies, other wise use the the terminal and execute: flutter pub get

We are ready to create our pubs.dart model create a folder  lib/models and inside the pubs.dart file. We need to represent the Pubs model created in Strapi here or at least the information we need.

import 'package:json_annotation/json_annotation.dart';
import 'pubPicture.dart';

part 'pubs.g.dart';

@JsonSerializable(explicitToJson: true)
class Pubs {
  Pubs(
    {
      this.id,
      this.name,
      this.address,
      this.picture,
    }
  );

  final int id;
  final String name;
  final String address;
  final PubPicture picture;

  factory Pubs.fromJson(Map<String, dynamic> json) =>
      _$PubsFromJson(json);

  Map<String, dynamic> toJson() => _$PubsToJson(this);

}

You will notice this will fail, do not worry there is still a couple of things to do. First we need to create the model for picture as it is an object inside the pub. More info here

NOTE: the id type will vary on the database your use in Strapi, if you are using sqllite as per default, int will be fine, if you decide to use mongo, you will need to use String.

Inside Models create a new file pubPicture.dart

import 'package:json_annotation/json_annotation.dart';


part 'pubPicture.g.dart';

@JsonSerializable(explicitToJson: true)
class PubPicture {
  PubPicture({this.id, this.name, this.url});

  final int id;
  final String name; 
  final String url;

  factory PubPicture.fromJson(Map<String, dynamic> json) =>
      _$PubPictureFromJson(json);

  Map<String, dynamic> toJson() => _$PubPictureToJson(this);
}

You probably have noticed we are only mapping three fields even if the JSON object has more, but we do not need more than the URL to be displayed.

Now is time to generate the models run, flutter packages pub run build_runner build and all errors will disappear

Before continue working on lib/main.dart. We want to create the Pub Card. This widget will be the representation of each individual item of the list we will show.


import 'package:flutter/material.dart';

import 'models/pubs.dart';

class PubCard extends StatelessWidget {
  const PubCard([
    this.pub,
  ]);

  final Pubs pub;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: ConstrainedBox(
        constraints: BoxConstraints(
          minWidth: 44,
          minHeight: 44,
          maxWidth: 64,
          maxHeight: 64,
        ),
        child: Image.network(
            'http://yourlocalIP:1337' +
                pub.picture.url,
            fit: BoxFit.cover),
      ),
        title: Text(
          pub.name
        ),
        subtitle: Text(pub.address),
        trailing: Column(children:<Widget>[Text('Avg Price'),Text(pub.avgPrice.toString())]),
        contentPadding: EdgeInsets.symmetric(vertical: 40.0, horizontal: 20.0),
      ),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
    );
  } 
}

NOTE: if you are debugging on a physical device this must be connected to the same network and the URL yourlocalIP must be the computer's IP that is running Strapi in my case 192.168.68.111  

We are ready to work on showing the list on our main.dart file.  Flutters uses FutureBuilder to make asynchronous calls. We need to implement it to call the server.

Import the packages

import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'dart:convert';

Create a function getPubs

Future<String> getPubs(_listPubs) async {
  final Response response = await http.get('http://yourlocalIP:1337/pubs/');

  if (response.statusCode == 200) {
    List<dynamic> pubsListRaw = jsonDecode(response.body);
    for (var i = 0; i < pubsListRaw.length; i++) {
      _listPubs.add(Pubs.fromJson(pubsListRaw[i]));
    }

    return "Success!";
  } else {
    throw Exception('Failed to load data');
  }
}

Let's add the final code to show the list of pubs. We need to create the function _buildPubs

Widget _buildPubs() {
    return FutureBuilder(
      builder: (context, projectSnap) {
        if (projectSnap.connectionState == ConnectionState.none &&
            projectSnap.hasData == null) {
          return Container();
        }
        return ListView.builder(
          itemCount: _listPubs.length,
          itemBuilder: (context, index) {
            return PubCard(_listPubs[index]);
          },
        );
      },
      future: futurePubs,
    );
  }

To finalise we have to call the futurePubs when the app is loaded the first time but this will fail because initially we declared our app as Stateless which mean it won't change the state once is created however as we are using Future and asynchronous calls the content of the app changes once is created we need to twitch a bit the code and make it Statefull

Final main.dart code

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:everis_fridays_pubs_app/pub_card.dart';
import 'package:flutter/material.dart';

import 'models/pubs.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'dart:convert';

void main() => runApp(EverisFridayApp());

class EverisFridayApp extends StatefulWidget {
  @override
  EverisFridayState createState() => EverisFridayState();
}

class EverisFridayState extends State<EverisFridayApp> {

  final List<Pubs> _listPubs = <Pubs>[];
  
  Future<String> futurePubs;
  
  @override
  void initState() {
    super.initState();
    futurePubs = getPubs(_listPubs);
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Everis Fridays Pub',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Everis Fridays Pub'),
          backgroundColor: Color(0xff9aae04),
        ),
        body: Center(
          child: _buildPubs(),
        ),
      ),
    );
  }

  Widget _buildPubs() {
    return FutureBuilder(
      builder: (context, projectSnap) {
        if (projectSnap.connectionState == ConnectionState.none &&
            projectSnap.hasData == null) {
          return Container();
        }
        return ListView.builder(
          itemCount: _listPubs.length,
          itemBuilder: (context, index) {
            return PubCard(_listPubs[index]);
          },
        );
      },
      future: futurePubs,
    );
  }
}

Future<String> getPubs(_listPubs) async {
  final Response response = await http.get('http://192.168.68.111:1337/pubs');

  if (response.statusCode == 200) {
    List<dynamic> pubsListRaw = jsonDecode(response.body);
    for (var i = 0; i < pubsListRaw.length; i++) {
      _listPubs.add(Pubs.fromJson(pubsListRaw[i]));
    }

    return "Success!";
  } else {
    throw Exception('Failed to load data');
  }
}

As you can see the power and speed of the combination Flutter and Strapi is astonish.  This app could be perfectly bundled and release to Android and iOS and it only took us one hour to code it.

If you have any issue or blocker drop a comment and I'll happily help you.

You can find the repository in GitHub

If you liked the post, please share it! and do not forget to subscribe :)

The result