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 import 'package:flutter/material.dart';
2 import 'package:graphql_flutter/graphql_flutter.dart';
3 final HttpLink httpLink = HttpLink("http://localhost:1337/graphql");
4 final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
5 GraphQLClient(
6 link: httpLink,
7 cache: GraphQLCache(),
8 ),
9 );
10 void main() async {
11 await initHiveForFlutter();
12 final HttpLink httpLink = HttpLink(
13 'https://api.github.com/graphql',
14 );
15 final AuthLink authLink = AuthLink(
16 getToken: () async => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
17 // OR
18 // getToken: () => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
19 );
20 final Link link = authLink.concat(httpLink);
21 ValueNotifier<GraphQLClient> client = ValueNotifier(
22 GraphQLClient(
23 link: link,
24 // The default store is the InMemoryStore, which does NOT persist to disk
25 cache: GraphQLCache(store: HiveStore()),
26 ),
27 );
28 runApp(const MyApp());
29 }
30
31 class MyApp extends StatelessWidget {
32 const MyApp({Key? key}) : super(key: key);
33
34 Widget build(BuildContext context) {
35 return GraphQLProvider(
36 client: client,
37 child: MaterialApp(
38 title: 'Flutter Demo',
39 theme: ThemeData(
40 primarySwatch: Colors.blue,
41 ),
42 initialRoute: '/',
43 routes: {
44 '/': (context) => const RegisterScreen(),
45 '/users': (context) => const UsersScreen(),
46 '/edit': (context) => const EditProfileScreen(),
47 },
48 ),
49 );
50 }
51 }
52 // . . .
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 class RegisterScreen extends StatefulWidget {
3 const RegisterScreen({Key? key}) : super(key: key);
4
5 _RegisterScreenState createState() => _RegisterScreenState();
6 }
7 class _RegisterScreenState extends State<RegisterScreen> {
8 final _formKey = GlobalKey<FormState>();
9 final _nameController = TextEditingController();
10 final _emailController = TextEditingController();
11 final _usernameController = TextEditingController();
12
13 void dispose() {
14 _nameController.dispose();
15 _emailController.dispose();
16 _usernameController.dispose();
17 super.dispose();
18 }
19 void _submitForm() {
20 if (_formKey.currentState!.validate()) {
21 // Form is valid, proceed with form submission
22 String name = _nameController.text;
23 String email = _emailController.text;
24 String username = _usernameController.text;
25 // Perform desired operations with the form data
26 print('Name: $name');
27 print('Email: $email');
28 print('Username: $username');
29 // Mutation variables
30 final Map<String, dynamic> variables = {
31 'name': name,
32 'username': username,
33 'email': email,
34 };
35 MutationOptions options = MutationOptions(
36 document: gql("""
37 mutation CreateApp(\$name: String!, \$username: String!, \$email: String!) {
38 createApp(data: { name: \$name, username: \$username, email: \$email }) {
39 data {
40 attributes {
41 name
42 username
43 email
44 }
45 }
46 }
47 }
48 """),
49 variables: variables,
50 onCompleted: (dynamic resultData) {
51 // Mutation was completed successfully
52 print('Mutation Result: $resultData');
53 // Redirect to the profile screen after successful registration
54 Navigator.pushNamed(context, '/users');
55 },
56 );
57 client.value.mutate(options);
58 }
59 }
60
61 Widget build(BuildContext context) {
62 return Scaffold(
63 appBar: AppBar(
64 title: const Text('User Registration'),
65 ),
66 body: Padding(
67 padding: const EdgeInsets.all(16.0),
68 child: Form(
69 key: _formKey,
70 child: Column(
71 children: [
72 TextFormField(
73 controller: _nameController,
74 decoration: const InputDecoration(
75 labelText: 'Name',
76 ),
77 validator: (value) {
78 if (value!.isEmpty) {
79 return 'Please enter your name';
80 }
81 return null;
82 },
83 ),
84 SizedBox(height: 16.0),
85 TextFormField(
86 controller: _emailController,
87 decoration: const InputDecoration(
88 labelText: 'Email',
89 ),
90 validator: (value) {
91 if (value!.isEmpty) {
92 return 'Please enter your email';
93 }
94 // Add additional email validation if needed
95 return null;
96 },
97 ),
98 SizedBox(height: 16.0),
99 TextFormField(
100 controller: _usernameController,
101 decoration: const InputDecoration(
102 labelText: 'Username',
103 ),
104 validator: (value) {
105 if (value!.isEmpty) {
106 return 'Please enter a username';
107 }
108 return null;
109 },
110 ),
111 SizedBox(height: 16.0),
112 ElevatedButton(
113 onPressed: _submitForm,
114 child: const Text('Register'),
115 ),
116 ],
117 ),
118 ),
119 ),
120 );
121 }
122 }
123 // . . .
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 class UsersScreen extends StatelessWidget {
3 const UsersScreen({Key? key}) : super(key: key);
4
5 Widget build(BuildContext context) {
6 return Scaffold(
7 appBar: AppBar(
8 title: const Text('Users'),
9 ),
10 body: Query(
11 options: QueryOptions(
12 document: gql("""
13 query {
14 apps {
15 data {
16 id
17 attributes {
18 name
19 username
20 email
21 }
22 }
23 }
24 }
25 """),
26 ),
27 builder: (result1, {fetchMore, refetch}) {
28 if (result1.hasException) {
29 // Query encountered an error
30 print('Query Error: ${result1.exception.toString()}');
31 return const Center(
32 child: Text('An error occurred'),
33 );
34 }
35 if (result1.isLoading) {
36 return const Center(
37 child: CircularProgressIndicator(),
38 );
39 }
40 final posts = result1.data!\['apps'\]['data'];
41 return ListView.builder(
42 itemCount: posts.length,
43 itemBuilder: (context, index) {
44 final post = posts[index];
45 final name = post\['attributes'\]['name'];
46 return ListTile(
47 leading: Icon(Icons.person),
48 title: Text(post\['attributes'\]['name']),
49 subtitle: Text(post\['attributes'\]['email']),
50 onTap: () {
51 Navigator.pushNamed(
52 context,
53 '/edit',
54 arguments: {
55 "userID": post['id'],
56 "name": post\['attributes'\]['name'],
57 "email": post\['attributes'\]['email'],
58 "username": post\['attributes'\]['username'],
59 },
60 );
61 },
62 );
63 },
64 );
65 },
66 ),
67 );
68 }
69 }
70 // . . .
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 class EditProfileScreen extends StatefulWidget {
3 const EditProfileScreen({Key? key}) : super(key: key);
4
5 _EditProfileScreenState createState() => _EditProfileScreenState();
6 }
7 class _EditProfileScreenState extends State<EditProfileScreen> {
8 final _formKey = GlobalKey<FormState>();
9 final _nameController = TextEditingController();
10 final _emailController = TextEditingController();
11 final _usernameController = TextEditingController();
12 final _useridController = TextEditingController();
13
14 void dispose() {
15 _nameController.dispose();
16 _emailController.dispose();
17 _usernameController.dispose();
18 _useridController.dispose();
19 super.dispose();
20 }
21 void _submitForm() {
22 if (_formKey.currentState!.validate()) {
23 String name = _nameController.text;
24 String email = _emailController.text;
25 String username = _usernameController.text;
26 String userid = _useridController.text;
27 final Map<String, dynamic> variables = {
28 'appId': userid,
29 'name': name,
30 'username': username,
31 'email': email,
32 };
33 MutationOptions options = MutationOptions(
34 document: gql("""
35 mutation UpdateApp(\$appId: ID!, \$name: String!, \$username: String!, \$email: String!) {
36 updateApp(id: \$appId, data: { name: \$name, username: \$username, email: \$email }) {
37 data {
38 attributes {
39 name
40 username
41 email
42 }
43 }
44 }
45 }
46 """),
47 variables: variables,
48 onCompleted: (dynamic resultData) {
49 // Mutation was completed successfully
50 print('Mutation Result: $resultData');
51 // Redirect to the profile screen after successful update
52 Navigator.pushReplacementNamed(context, '/users');
53 },
54 );
55 client.value.mutate(options);
56 }
57 }
58
59 Widget build(BuildContext context) {
60 final Map<String, dynamic>? args =
61 ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
62 final String userID = args?['userID'] as String? ?? '';
63 final String name = args?['name'] as String? ?? '';
64 final String email = args?['email'] as String? ?? '';
65 final String username = args?['username'] as String? ?? '';
66 return Scaffold(
67 appBar: AppBar(
68 title: const Text('Edit Profile'),
69 ),
70 body: Padding(
71 padding: const EdgeInsets.all(16.0),
72 child: Form(
73 key: _formKey,
74 child: Column(
75 children: [
76 TextFormField(
77 controller: _nameController..text = name,
78 decoration: const InputDecoration(
79 labelText: 'Name',
80 ),
81 validator: (value) {
82 if (value!.isEmpty) {
83 return 'Please enter your full name';
84 }
85 return null;
86 },
87 ),
88 SizedBox(height: 16.0),
89 TextFormField(
90 controller: _emailController..text = email,
91 decoration: const InputDecoration(
92 labelText: 'Email',
93 ),
94 validator: (value) {
95 if (value!.isEmpty) {
96 return 'Please enter your email';
97 }
98 // Add additional email validation if needed
99 return null;
100 },
101 ),
102 SizedBox(height: 16.0),
103 TextFormField(
104 controller: _usernameController..text = username,
105 decoration: const InputDecoration(
106 labelText: 'Username',
107 ),
108 validator: (value) {
109 if (value!.isEmpty) {
110 return 'Please enter a username';
111 }
112 return null;
113 },
114 ),
115 SizedBox(height: 16.0),
116 TextFormField(
117 controller: _useridController..text = userID,
118 decoration: const InputDecoration(
119 labelText: 'UserID',
120 ),
121 readOnly: true,
122 validator: (value) {
123 if (value!.isEmpty) {
124 return 'Please enter your ID';
125 }
126 // Add additional email validation if needed
127 return null;
128 },
129 ),
130 SizedBox(height: 16.0),
131 ElevatedButton(
132 onPressed: _submitForm,
133 child: const Text('Save Changes'),
134 ),
135 SizedBox(height: 20.0),
136 ElevatedButton(
137 onPressed: () {
138 // Perform delete operation here
139 final Map<String, dynamic> variables = {
140 'id': userID,
141 };
142 MutationOptions options = MutationOptions(
143 document: gql('''
144 mutation DeleteApp(\$id: ID!) {
145 deleteApp(id: \$id) {
146 data {
147 attributes {
148 name
149 }
150 }
151 }
152 }
153 '''),
154 variables: variables,
155 onCompleted: (dynamic resultData) {
156 // Mutation was completed successfully
157 print('Mutation Result: $resultData');
158 // Redirect to the profile screen after successful deletion
159 Navigator.pushReplacementNamed(context, '/users');
160 },
161 );
162 client.value.mutate(options);
163 },
164 child: const Text('Delete User'),
165 style: ElevatedButton.styleFrom(
166 primary: Colors.red,
167 ),
168 ),
169 ],
170 ),
171 ),
172 ),
173 );
174 }
175 }
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 <key>com.apple.security.network.client</key>
2 <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.