Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project --quickstart
In this tutorial, you 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, Updating user data, and Deleting data.
To follow along with this tutorial, you need the following:
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.
Head refers to where you deliver your content through mobile or web applications. The term “headless” refers to 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 means you can choose the platform or head to which you send your content.
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 to host audio and video assets.
Building a full-stack application usually requires using 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.
To install strapi on your system, open your terminal and navigate to the folder where you want to install it. Then run the command below to install the strapi backend.
npx create-strapi-app@latest strapi-project
You will be prompted to choose the installation type. For this tutorial, select the Quickstart (recommended) option. After the prompts, your strapi project gets built, and a server starts automatically.
After installation, you will be redirected to a web page where you can access the admin dashboard using http://localhost:1337/admin.
You will be required to register and login to strapi to your dashboard. Create a new super admin account, as shown below.
After creating the account, you will be redirected to the Strapi dashboard, where you will have access to all the features offered by Strapi.
Next, you need to create a collection type that will store information about users, and this information can be easily manipulated via API. You will create a simple CRUD app demonstrating how to create, retrieve, update, and delete data from a collection type via API calls.
To create the collection type from the side menu of your dashboard, click on the collection type builder and select **Create New Collection Type**
, as shown in the image below:
When you click on the **Create new collection type**
button, a modal opens up requesting a display name for the new collection type. Named it the
**app**
, and then click on the **Continue**
button.
Next, you need to add the following fields to the collection type.
After creating the fields, click on **Finish**
and the **Save**
button at the top of the fields, as shown in the image above.
You cannot access the new app collection type endpoint via an API call at this level because you have not set the role and permissions for the collection type.
To set the app collection type roles and permissions, from the sidebar menu, navigate to Settings
→ Roles
→ Public
, as shown in the image below.
Next, select the app collection type under the permission section, and check all the options. This will give permission to perform CRUD operations. Then, click on the save
button, as shown in the image below.
The Strapi GraphQL plugin is a plugin that adds GraphQL support to the Strapi headless CMS (Content Management System). Strapi is a flexible and customizable CMS allowing developers to build and manage API-driven content easily.
The GraphQL plugin for Strapi enables developers to expose their Strapi content and data through a GraphQL API. It generates a GraphQL schema based on the existing content types and fields defined in Strapi. This schema defines the available queries, mutations, and types that can be used to interact with the data.
By using the Strapi GraphQL plugin, developers can leverage the power of GraphQL to query, filter, and retrieve data from their Strapi backend in a flexible and efficient manner. It provides a more tailored approach to data fetching, allowing clients to request exactly the data they need in a single request.
To install the Strapi GraphQL plugin, enter the command below in your terminal and restart your Strapi server.
npm install @strapi/plugin-graphql
npm run develop # to restart the server
If you need to install Flutter you can find the instructions [here(https://docs.flutter.dev/get-started/install).
Create a new flutter project in the terminal by running the commands below.
flutter create flutter_app
cd flutter_app
flutter run
To install GraphQL in your Flutter application, navigate to the Flutter project folder and run the command below on your terminal:
flutter pub add graphql_flutter
After the installation, navigate to the lib directory from the project root directory and open the main.dart
file.
In the main.dart file, import flutter``_``graphql
and configure the GraphQL client by adding the following code to the main.dart
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
final HttpLink httpLink = HttpLink("http://localhost:1337/graphql");
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
GraphQLClient(
link: httpLink,
cache: GraphQLCache(),
),
);
void main() async {
await initHiveForFlutter();
final HttpLink httpLink = HttpLink(
'https://api.github.com/graphql',
);
final AuthLink authLink = AuthLink(
getToken: () async => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
// OR
// getToken: () => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
);
final Link link = authLink.concat(httpLink);
ValueNotifier<GraphQLClient> client = ValueNotifier(
GraphQLClient(
link: link,
// The default store is the InMemoryStore, which does NOT persist to disk
cache: GraphQLCache(store: HiveStore()),
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return GraphQLProvider(
client: client,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => const RegisterScreen(),
'/users': (context) => const UsersScreen(),
'/edit': (context) => const EditProfileScreen(),
},
),
);
}
}
// . . .
In the code above, the application will consist of three screens: the registration screen, the profile screen, and the edit screen.
To insert data into the strapi backend, let’s create a simple form in the RegisterScreen that allow us to register new users and then store the user details using graphql query. To create the RegisterScreen, add the following code to main.dart
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// . . .
class RegisterScreen extends StatefulWidget {
const RegisterScreen({Key? key}) : super(key: key);
_RegisterScreenState createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
void dispose() {
_nameController.dispose();
_emailController.dispose();
_usernameController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Form is valid, proceed with form submission
String name = _nameController.text;
String email = _emailController.text;
String username = _usernameController.text;
// Perform desired operations with the form data
print('Name: $name');
print('Email: $email');
print('Username: $username');
// Mutation variables
final Map<String, dynamic> variables = {
'name': name,
'username': username,
'email': email,
};
MutationOptions options = MutationOptions(
document: gql("""
mutation CreateApp(\$name: String!, \$username: String!, \$email: String!) {
createApp(data: { name: \$name, username: \$username, email: \$email }) {
data {
attributes {
name
username
email
}
}
}
}
"""),
variables: variables,
onCompleted: (dynamic resultData) {
// Mutation was completed successfully
print('Mutation Result: $resultData');
// Redirect to the profile screen after successful registration
Navigator.pushNamed(context, '/users');
},
);
client.value.mutate(options);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Registration'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
SizedBox(height: 16.0),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your email';
}
// Add additional email validation if needed
return null;
},
),
SizedBox(height: 16.0),
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter a username';
}
return null;
},
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Register'),
),
],
),
),
),
);
}
}
// . . .
The code above represents the registration screen UI, handles form validation and submission using the _submitForm()
method, and performs a GraphQL mutation to create a new user upon successful form submission.
At this level, registering a new user on the application will save it as a draft in the Strapi backend. This will not be displayed on the frontend. To solve this in your Strapi backend, navigate to Content Type Builder
→ App
→ Edit
. Then, in the edit modal, click on Advanced Settings and disable the draft and publish features.
Display User screen
This screen will only retrieve the list of registered users from the strapi backend using graphql query. To create UserScreen
add the following code to main.dart
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// . . .
class UsersScreen extends StatelessWidget {
const UsersScreen({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users'),
),
body: Query(
options: QueryOptions(
document: gql("""
query {
apps {
data {
id
attributes {
name
username
email
}
}
}
}
"""),
),
builder: (result1, {fetchMore, refetch}) {
if (result1.hasException) {
// Query encountered an error
print('Query Error: ${result1.exception.toString()}');
return const Center(
child: Text('An error occurred'),
);
}
if (result1.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final posts = result1.data!\['apps'\]['data'];
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
final name = post\['attributes'\]['name'];
return ListTile(
leading: Icon(Icons.person),
title: Text(post\['attributes'\]['name']),
subtitle: Text(post\['attributes'\]['email']),
onTap: () {
Navigator.pushNamed(
context,
'/edit',
arguments: {
"userID": post['id'],
"name": post\['attributes'\]['name'],
"email": post\['attributes'\]['email'],
"username": post\['attributes'\]['username'],
},
);
},
);
},
);
},
),
);
}
}
// . . .
The code above represents a screen that displays a list of users obtained from a GraphQL query result. It handles loading and error states, allowing us to navigate to the edit screen for each user by clicking on the user details.
Creating EditProfileScreen
When you click on a user from the Users screen, you will be redirected to the EditProfileScreen, which allows you to update or delete registered users from the Strapi backend. Now update the mart.dart
file with the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// . . .
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({Key? key}) : super(key: key);
_EditProfileScreenState createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
final _useridController = TextEditingController();
void dispose() {
_nameController.dispose();
_emailController.dispose();
_usernameController.dispose();
_useridController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
String name = _nameController.text;
String email = _emailController.text;
String username = _usernameController.text;
String userid = _useridController.text;
final Map<String, dynamic> variables = {
'appId': userid,
'name': name,
'username': username,
'email': email,
};
MutationOptions options = MutationOptions(
document: gql("""
mutation UpdateApp(\$appId: ID!, \$name: String!, \$username: String!, \$email: String!) {
updateApp(id: \$appId, data: { name: \$name, username: \$username, email: \$email }) {
data {
attributes {
name
username
email
}
}
}
}
"""),
variables: variables,
onCompleted: (dynamic resultData) {
// Mutation was completed successfully
print('Mutation Result: $resultData');
// Redirect to the profile screen after successful update
Navigator.pushReplacementNamed(context, '/users');
},
);
client.value.mutate(options);
}
}
Widget build(BuildContext context) {
final Map<String, dynamic>? args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
final String userID = args?['userID'] as String? ?? '';
final String name = args?['name'] as String? ?? '';
final String email = args?['email'] as String? ?? '';
final String username = args?['username'] as String? ?? '';
return Scaffold(
appBar: AppBar(
title: const Text('Edit Profile'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController..text = name,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your full name';
}
return null;
},
),
SizedBox(height: 16.0),
TextFormField(
controller: _emailController..text = email,
decoration: const InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your email';
}
// Add additional email validation if needed
return null;
},
),
SizedBox(height: 16.0),
TextFormField(
controller: _usernameController..text = username,
decoration: const InputDecoration(
labelText: 'Username',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter a username';
}
return null;
},
),
SizedBox(height: 16.0),
TextFormField(
controller: _useridController..text = userID,
decoration: const InputDecoration(
labelText: 'UserID',
),
readOnly: true,
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your ID';
}
// Add additional email validation if needed
return null;
},
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Save Changes'),
),
SizedBox(height: 20.0),
ElevatedButton(
onPressed: () {
// Perform delete operation here
final Map<String, dynamic> variables = {
'id': userID,
};
MutationOptions options = MutationOptions(
document: gql('''
mutation DeleteApp(\$id: ID!) {
deleteApp(id: \$id) {
data {
attributes {
name
}
}
}
}
'''),
variables: variables,
onCompleted: (dynamic resultData) {
// Mutation was completed successfully
print('Mutation Result: $resultData');
// Redirect to the profile screen after successful deletion
Navigator.pushReplacementNamed(context, '/users');
},
);
client.value.mutate(options);
},
child: const Text('Delete User'),
style: ElevatedButton.styleFrom(
primary: Colors.red,
),
),
],
),
),
),
);
}
}
In the above code, the _submitForm method is called to update the inputted data when the Save Changes
button is pressed. Additionally, the onPressed
property of the delete user button is used to make a GraphQL query that will delete a user from the backend.
Now that the application is ready run the command below in your terminal to start it.
flutter run
You should have the application running, as shown in the image below.
If you are having an issue connecting to the localhost endpoint, then in your Flutter folder, navigate to macos/Runner/DebugProfile.entitlements
and macos/Runner/Release.entitlements
, and add the code below to the two files.
1
2
<key>com.apple.security.network.client</key>
<true/>
Finally, we came to the end of the tutorial. In the tutorial, we learned how to connect Strapi with our Flutter frontend using GraphQL and 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: Create User, Display Users, 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 straightforward, and you can choose any client, be it web, mobile app, or desktop.
You can access the full source code for this Flutter application on this repository CRUD-Application-Using-Flutter-Strapi.