How to Build a Simple CRUD Application Using Flutter and Strapi

Strapi
14 min readDec 8, 2021

--

In this tutorial, we will learn how to build a Create, Retrieve, Update and Delete (CRUD) application using Flutter and Strapi. We will call End-points provided to us by Strapi using the HTTP package in our app. We will build screens where different operations will take place like adding/creating a new user, Retrieve User data, Update user data and Delete data.

Prerequisite

To follow this tutorial, you need:

  • NPM
  • Node.js
  • Flutter SDK

What is Headless CMS

Headless CMS is the only content repository that serves as a back-end for your front-end applications. It is built to allow content to be accessed via RESTFUL API or GraphQL API i.e it provides your content as data over an API.

The term Head refers to where you deliver your content either through a mobile application or a web application. The term “headless” refers to the concept of removing the head from the body, which is usually the front end of a website. This does not mean that having ahead is not important, it just means that you get the flexibility to choose what platform or head you send your content to.

Why Strapi

Strapi is a JavaScript framework that simplifies the creation of REST APIs. It allows developers to create content types and their relationships between them. It also has a media library that will allow you to host audio and video assets.

Overview

Building a full-stack application usually requires the use of both the front-end and back-end components. These components are often interrelated and are required to complete the project. You can manage and create your API without the help of any backend developer. Strapi is a headless CMS that’s built on top of Node.js. This is a great alternative to traditional CMSes that are already in use.

Setup Flutter Project

In our terminal, we will create our flutter project

flutter create strapi_backend
cd strapi_backend

In our flutter app, we will create two folders and six files inside our lib folder just like our files structure below.

File Structure

├─ android                                                                            
│ ├─ app
│ │ ├─ src ├─ build
│ ├─ app ├─ ios ├─ lib
│ ├─ customisation
│ │ └─ textfield.dart
│ ├─ view
│ │ ├─ add_user.dart │ │ ├─ editUser.dart
│ │ ├─ show_users.dart
│ │ ├─ user.dart
│ │ └─ userDetail.dart
│ └─ main.dart
├─ README.md
├─ pubspec.lock
├─ pubspec.yaml
└─ strapi_backend.iml

Add Http library to fetch APIs

We will be needing the HTTP package from pub. dev to make requests to strapi you can run the command below to add the package to your pubspec.yamland download all dependencies we will need in our app.

flutter pub add http
flutter pub get

Setting Up Strapi

We will change the directory in our terminal to create a folder for Strapi using the following command

cd ../
npx create-strapi-app backend

At some point, we will be prompted to choose the installation type.

? Choose your installation type (Use arrow keys)
❯ Quickstart (recommended)
Custom (manual settings)

Please choose Quickstart as it is recommended.

After hitting enter, we will be prompted to choose a template, in my case, I did not.

After the prompts, your strapi project gets built and a server starts automatically.

Once the server starts, we will be redirected to a web page where we will access our admin panel at http://localhost:1337/admin. Fill out the fields to create credentials for a root admin user or a super admin and which accept the terms and click on LET' S START to take us to our dashboard.

Create Strapi Collection Types

Let’s create data collection for our mobile application via our dashboard. Just to the left of our dashboard are options to help us create content for our mobile application and we will be using the Content-Type Builderunder Plugin. We will be creating a simple CRUD app to create, retrieve, update and delete data, let’s dive right in!

First, under our Collection type, click on Create new collection type to create a new collection.

When we click on the Create new collection type a modal opens up requesting for a display name for the new collection type. We can simply name it App. Click on continue so we can create the fields needed for our application.

Add Fields

The fields we will be needing will just be three text fields

  • name
  • email
  • password

The name text field will just hold the name of the user, while the email text field will hold the user’s email address and the password will hold the possible password of the user.

Give the name “name” to the Text field. To add more fields, click Add another field to add fields for email and password. All we want is to create a user who will store the data so we can easily retrieve and make updates to the data

After creating the fields, click on finish and hit the Save button at the top of the fields.

NOTE: After hitting the Save button and toast appears to show that an error has occurred, you can simply reload the page and your data will be saved.

Adding Roles and Permissions

Next up, we will return to the sidebar of our dashboard, under the group title “general”, click on Settings. Here, we will be adding permissions for the users on what operations they can perform and these permissions will also allow us to easily access our API.

Right after the sidebar, there is another sidebar, click on Roles under Users & Permission Plugin

Under Roles, click on the icon at the extreme of Public to add permissions for the users.

We will give permissions to the user to perform CRUD operations, so you can go ahead and check the Select all checkbox and hit the save button at the top right of the page.

Build Screens

Recall that we have created our flutter project, so all we need to do is open the flutter project folder in our code Editor. I am using VS code. Let’s build our first screen

For our main. dart file. The main. dart file in flutter is where the app is being bootstrapped from. Main. dart is the entry point of our flutter app. copy the code below

import 'package:flutter/material.dart';
import 'package:strapi_backend/view/add_user.dart';
void main() {
runApp(Strapi());
}
class Strapi extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:CreateUser()
);
}
}

Create User screen

The screen will have a text field where users will enter their details to save to the entry point on our local strapi server. All users will have a specific ID once saved. The screen will be performing a POST request to store user details on our backend. The screen will take in the Name, Email, and Password using TextFields and its controllers to post the user details to strapi.

Copy-paste the code below into your add_user.dart file

import 'package:flutter/material.dart';
import 'package:strapi_backend/customisation/textfield.dart';
import 'package:strapi_backend/view/show_users.dart';
import 'package:http/http.dart' as http;
import 'package:strapi_backend/view/user.dart';
class CreateUser extends StatefulWidget {
final int id;
const CreateUser({ Key key,this.id });
@override
_CreateUserState createState() => _CreateUserState();
}
TextEditingController emailController = TextEditingController(text: users.email);
TextEditingController passwordController = TextEditingController(text: users.password);
TextEditingController nameController = TextEditingController(text: users.name);
Users users = Users(0, '', '', '');
class _CreateUserState extends State<CreateUser> {
Future save() async {
await http.post(Uri.parse("http://10.0.2.2:1337/apis/",),headers:<String, String> {
'Context-Type': 'application/json; charset=UTF-8',
},body: <String,String> { 'name':users.name,
'email': users.email,
'password': users.password,}
);
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()), (Route<dynamic> route) => false);
}


@override
Widget build(BuildContext context) {
// print(widget.id);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo[700],
elevation: 0.0,
title: Text('Create User'),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(top: 100,bottom: 100,left: 18,right: 18),
child: Container(
. height: 550,
width: 400,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.indigo[700],
),

child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 300,
decoration: BoxDecoration(boxShadow: [
]),
child: Textfield(
controller:nameController ,
onChanged: (val){
users.name = val;
},
hintText: 'Name',
)
),
SizedBox(height: 10,),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: [
]),
child: Textfield(
controller: emailController,
onChanged: (val){
users.email = val;
},
hintText: 'Email',
)
),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: [
]),
child: Textfield(
hintText: 'Password',
onChanged: (val){
users.password = val;
},
controller: passwordController,
)
),
SizedBox(
width: 100,
child: TextButton(
style: TextButton.styleFrom(backgroundColor: Colors.white),
onPressed:save, child: Text('Save')),
)
],
),
),
),
),
);

}

}

The code above has an asynchronous save function that returns a future response. Note that we have a link where we are posting our details to "http://10.0.2.2:1337/apis/``" this link is what we will be using to make every possible request around our app but our localhost runs on "http://localhost:1337/apis/", so why do we use "http://10.0.2.2:1337/apis/" is because we are running our flutter app on an emulator.

The HTTP requests made to this page are sent to the local host address. The reason why we don't use the local host is that the HTTP request will be forwarded to the destination port of the requested website.

Since our local host runs on localhost:1337 the emulator will make an HTTP request to 10.0.2.2:1337.

Users users = Users(0, '', '', '');
class _CreateUserState extends State<CreateUser> {
Future save() async {
// var jsonResponse = null;
await http.post(Uri.parse("http://10.0.2.2:1337/apis/",),headers:<String, String> {
'Context-Type': 'application/json; charset=UTF-8',
},body: <String,String> { 'name':users.name,
'email': users.email,
'password': users.password,}
);
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()), (Route<dynamic> route) => false);
}

We will create a separate Users class file where we will list all possible data that will be posted to our backend. The Users class will help us have good control over how our data will be passed around in our application. The Users class will have the following variables: id, name, email, password and we will pass them on as constructors so we can access it when we use an instance of the Users class.

The function in the code above shows a simple POST request that has all the necessary data in the body and after the function is called and executed, we navigate to another screen where added users will display.

Display User screen This screen will only retrieve the data posted from the Create users screen. This screen will be performing a GET request to display as a list tile. Below is the code for the display screen

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:strapi_backend/view/user.dart';
import 'package:strapi_backend/view/userDetail.dart';
class DisplayUsers extends StatefulWidget {
const DisplayUsers({Key key}) : super(key: key);
@override
_DisplayUsersState createState() => _DisplayUsersState();
}
class _DisplayUsersState extends State<DisplayUsers> {
List<Users> user = [];
Future<List<Users>> getAll() async {
var response = await http.get(Uri.parse("http://10.0.2.2:1337/apis/"));

if(response.statusCode==200){
user.clear();
}
var decodedData = jsonDecode(response.body);
for (var u in decodedData) {
user.add(Users(u['id'], u['name'], u['email'], u['password']));
}
return user;
}
@override
Widget build(BuildContext context) {
getAll();
return Scaffold(
appBar: AppBar(
title: Text('Display Users'),
elevation: 0.0,
backgroundColor: Colors.indigo[700],
),
body: FutureBuilder(
future: getAll(),
builder: (context, AsyncSnapshot<List<Users>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, index) =>
InkWell(
child: ListTile(
title: Text(snapshot.data[index].name),
subtitle: Text(snapshot.data[index].email),
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (_)=>MyDetails(users: snapshot.data[index],)));
},
),
)
);
}
));
}
}

Let’s break the code above into segments to understand what is happening

List<Users> user = [];
Future<List<Users>> getAll() async {
var response = await http.get(Uri.parse("http://10.0.2.2:1337/apis/"));

if(response.statusCode==200){
user.clear();
}
var decodedData = jsonDecode(response.body);
for (var u in decodedData) {
user.add(Users(u['id'], u['name'], u['email'], u['password']));
}
return user;
}

In the code above, we created a list with the name user, with the type of Users. The function after the list performs a GET request that fetches data from our backend to display on our frontend and inside the function has a simple check statement that clears the list before adding a user, so we do not have multiple users displaying twice after decoding the response and looping through it.

FutureBuilder(
future: getAll(),
builder: (context, AsyncSnapshot<List<Users>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, index) =>
InkWell(
child: ListTile(
title: Text(snapshot.data[index].name),
subtitle: Text(snapshot.data[index].email),
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (_)=>MyDetails(users: snapshot.data[index],)));
},
),
)
);
}
));

Future builder is a widget that displays and builds widgets based on the latest snapshot of interaction with a future like the function above. We made a check to return the progress indicator if there is no data available or if it is fetching the data. We built a ListView.builder widget based on snapshots of data from Future Builder. We also made the ListTile widget clickable so it takes us to a new screen where all the details of a particular user are displayed and we also passed the snapshot to the next screen so we can use the data there instead of making new requests all over again.

onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (_)=>MyDetails(users: snapshot.data[index],)));
}

Create My Details Screen

This screen displays all the data of a particular user, his/her ID, password, email, and and name. This screen also has two TextButton widgets positioned below the container that displays the user details. These buttons are the EDIT and DELETEbuttons.

import 'package:flutter/material.dart';
import 'package:strapi_backend/view/editUser.dart';
import 'package:strapi_backend/view/show_users.dart';
import 'package:strapi_backend/view/user.dart';
import 'package:http/http.dart' as http;
class MyDetails extends StatefulWidget {
final Users users;
const MyDetails({this.users }) ;
@override
_MyDetailsState createState() => _MyDetailsState();
}

class _MyDetailsState extends State<MyDetails> {
@override
Widget build(BuildContext context) {
void deleteUser()async{
await http.delete(Uri.parse("http://10.0.2.2:1337/apis/${widget.users.id}"));
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()),
(Route<dynamic> route) => false);
}
return Scaffold(
appBar: AppBar(
title: Text('My Details'),
elevation: 0.0,
backgroundColor: Colors.indigo[700],
),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 18,vertical:32),
child: Column(
children: [
Container(
height:50,
width: MediaQuery.of(context).size.width,
color: Colors.indigo[700],
child: Center(child: Text('Details',style: TextStyle(color: Color(0xffFFFFFF)),)),
),
Container(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18,vertical: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${widget.users.id}'),
SizedBox(height: 10,),
Text(widget.users.name),
SizedBox(height: 10,),
Text(widget.users.email),
SizedBox(height: 10,),
Text(widget.users.password),

],
),
),
// height: 455 ,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Color(0xffFFFFFF),
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(0,1),
),
]
),

),
Row(
children:[
TextButton(
onPressed: (){
Navigator.push(context, MaterialPageRoute(builder: (_)=>EditUser(users: widget.users,)));

}, child:Text('Edit'),
),
TextButton(
onPressed:(){
deleteUser();
}, child:Text('Delete'),
),
]
)
],
),
),
),
);
}
}

At the top of our class, we created a variable with the type of Users that takes a snapshot data from the previous screen. We displayed this data in a container, see the image below. Data above the state class of the statefulwidget cannot be accessed and if we have to access it, we have to do a lot of dependency injections passing one particular data over and over again till we get it to a point of access but flutter made it easier by using the keyword widget.

Let’s dive into the buttons below the container and their functionalities. The Edit button when clicked, takes us to a new screen where we can edit the details of a user based on his or her ID. We will create a new dart file called editUser.dart Copy-paste the code below into that file.

import 'package:flutter/material.dart';
import 'package:strapi_backend/customisation/textfield.dart';
import 'package:strapi_backend/view/show_users.dart';
import 'package:strapi_backend/view/user.dart';
import 'package:http/http.dart' as http;
class EditUser extends StatefulWidget {
final Users users;
const EditUser({Key key, this.users});
@override
_EditUserState createState() => _EditUserState();
}
class _EditUserState extends State<EditUser> {
void editUser(
{Users users, String email, String password, String name}) async {
final response = await http.put(
Uri.parse(
"http://10.0.2.2:1337/apis/${users.id}",
),
headers: <String, String>{
'Context-Type': 'application/json;charset=UTF-8',
},
body: <String, String>{
'name': name,
'email': email,
'password': password,
});
if (response.statusCode == 200) {
print(response.reasonPhrase);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()),
(Route<dynamic> route) => false);
} else {
print(response.statusCode);
print(response.reasonPhrase);
}
}
@override
Widget build(BuildContext context) {
TextEditingController emailController =
TextEditingController(text: widget.users.email);
TextEditingController passwordController =
TextEditingController(text: widget.users.password);
TextEditingController nameController =
TextEditingController(text: widget.users.name);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo[700],
elevation: 0.0,
title: Text('Edit User'),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(top: 100, bottom: 100, left: 18, right: 18),
child: Container(
height: 550,
width: 400,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.indigo[700],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 300,
decoration: BoxDecoration(boxShadow: [

]),
child: Textfield(
controller: nameController,
onChanged: (val) {
nameController.text = val;
},
hintText: 'Name',
)),
SizedBox(
height: 10,
),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: []),
child: Textfield(
controller: emailController,
onChanged: (val) {
emailController.text = val;
},
hintText: 'Email',
)),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: []),
child: Textfield(
hintText: 'Password',
onChanged: (val) {
passwordController.text = val;
},
controller: passwordController,
)),
SizedBox(
width: 100,
child: TextButton(
style:
TextButton.styleFrom(backgroundColor: Colors.white),
onPressed: () {
editUser(
users: widget.users,
email: emailController.text,
password: passwordController.text,
name: nameController.text);
},
child: Text('Save')),
)
],
),
),
),
),
);
}
}

Edit Function

When the Edit Text Button is clicked, we get to navigate to a new screen where we can edit the details of a particular user, and once saved, the user’s detail gets updated.

TextButton(
onPressed: (){
Navigator.push(context, MaterialPageRoute(builder: (_)=>EditUser(users: widget.users,)));

}, child:Text('Edit'),
),

Every time we click the Edit button, the user’s details appear on the Text Field to be edited. See code below

import 'package:flutter/material.dart';
import 'package:strapi_backend/customisation/textfield.dart';
import 'package:strapi_backend/view/show_users.dart';
import 'package:strapi_backend/view/user.dart';
import 'package:http/http.dart' as http;
class EditUser extends StatefulWidget {
final Users users;
const EditUser({Key key, this.users});
@override
_EditUserState createState() => _EditUserState();
}
class _EditUserState extends State<EditUser> {
void editUser(
{Users users, String email, String password, String name}) async {
final response = await http.put(
Uri.parse(
"http://10.0.2.2:1337/apis/${users.id}",
),
headers: <String, String>{
'Context-Type': 'application/json;charset=UTF-8',
},
body: <String, String>{
'name': name,
'email': email,
'password': password,
});
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()),
(Route<dynamic> route) => false);
}
}
@override
Widget build(BuildContext context) {
TextEditingController emailController =
TextEditingController(text: widget.users.email);
TextEditingController passwordController =
TextEditingController(text: widget.users.password);
TextEditingController nameController =
TextEditingController(text: widget.users.name);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo[700],
elevation: 0.0,
title: Text('Edit User'),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(top: 100, bottom: 100, left: 18, right: 18),
child: Container(
height: 550,
width: 400,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.indigo[700],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 300,
decoration: BoxDecoration(boxShadow: [

]),
child: Textfield(
controller: nameController,
onChanged: (val) {
nameController.text = val;
},
hintText: 'Name',
)),
SizedBox(
height: 10,
),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: []),
child: Textfield(
controller: emailController,
onChanged: (val) {
emailController.text = val;
},
hintText: 'Email',
)),
Container(
width: 300,
decoration: BoxDecoration(boxShadow: []),
child: Textfield(
hintText: 'Password',
onChanged: (val) {
passwordController.text = val;
},
controller: passwordController,
)),
SizedBox(
width: 100,
child: TextButton(
style:
TextButton.styleFrom(backgroundColor: Colors.white),
onPressed: () {
editUser(
users: widget.users,
email: emailController.text,
password: passwordController.text,
name: nameController.text);
},
child: Text('Save')),
)
],
),
),
),
),
);
}
}

Let’s break the code into segments to understand all the events happening above

void editUser(
{Users users, String email, String password, String name}) async {
final response = await http.put(
Uri.parse(
"http://10.0.2.2:1337/apis/${users.id}",
),
headers: <String, String>{
'Context-Type': 'application/json;charset=UTF-8',
},
body: <String, String>{
'name': name,
'email': email,
'password': password,
});
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()),
(Route<dynamic> route) => false);
}

The above function performs a PUT request based on the ID of the user passed and this function has parameters when called in the onPressed function, takes the needed data from the Text field, and afterward Navigates to the Display Users screen.

Delete Function

The delete TextButton deletes a particular user based on the ID passed.

void deleteUser()async{
await http.delete(Uri.parse("http://10.0.2.2:1337/apis/${widget.users.id}"));
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => DisplayUsers()),
(Route<dynamic> route) => false);
}

Test App

Conclusion

Finally, we came to the end of the tutorial. In the tutorial, we learned how to connect strapi with our flutter frontend using RESTFUL API and we used it to fetch data. In the process, we created three fields in strapi to accept data and we created four screens on our frontend using the flutter framework namely: Create user, Display User, My Details, and Edit User. We also added permissions to allow us to perform CRUD operations.

We had a hands-on tutorial and this has proven how easy it is to use Strapi. Strapi is straight forward and you can choose any client web, mobile app, or Desktops

Resources

Github code

--

--

Strapi
Strapi

Written by Strapi

The open source Headless CMS Front-End Developers love.

No responses yet