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.yaml
and 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 Builder
under 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
- 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 DELETE
buttons.
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