Creating an application that requires a strong backend and a slick mobile frontend can be challenging when you are a mobile developer who has minimal backend development knowledge. Also, setting up a backend server, database, securing, and scaling can consume much of the productive time used in development. This is where Strapi comes in, providing headless backend service and helping you spin up a backend faster, allowing focus on building and launching apps faster.
In this tutorial, I'll show you how to build a full-featured mobile CRUD application with Flutter and Strapi 5.
Before we jump in, ensure you've installed the following tool on your machine:
Below is a demo of the project you'll build throughout this tutorial.
The full project code is available on my Github repository. The backend code is in the main
branch, while the Flutter is in the mobile-app
branch.
Strapi 5 was just launched recently, and trust me, it's packed with some seriously cool upgrades. Let me break down the good stuff:
Do you recall how complicated it was to manage drafts earlier? Strapi 5 gives you two neat tabs, one for what you are working on and the other for what's live, no more content clutter. Also, the new history feature lets you return to any previous version. It's like ctrl+z for your whole content system. You can see exactly how your content will look on your site before it goes live—no more surprises after publishing.
The team has completely rewritten the core API with a new Document Service. It is so much faster, easier to use, and less messy than the old Entitiy Service. REST and GraphQL API both had a facelift—responses are more refined and easier to understand. For those who work with GraphQL, you'll love the new Relay-style query support. The new Strapi SDK makes it easier for developers to build custom plugins.
They've also introduced an AI chatbot that will recall the previous questions you asked, faster navigation in the docs with new tags that enable you to skip straight to the location you were interested in, and tons of guides and examples so you can build better stuff.
You can learn more about what is new for developers in Strapi 5: top 10 changes.
Let's get your backend up and running. Open your terminal and run the following command to scaffold a new Strapi 5 project.
npx create-strapi-app@latest my-project --quickstart
cd my-project
The above command will prompt you to select whether you want to Create a free account on Strapi Cloudor skip it for now. Then, the command will install the required project dependencies.
With your Strapi project created, Sign in to your admin panel.
From the admin panel, click on the Create your first content type button to create a new content type called **Product. "
Then add the following fields and click on the Save button:
name
(Text): Name of products on your app.Description
(Text): Description for each product.price
(Number): Price for products.seller
(One to Many relationship between User and Products): To allow your users access to the products from your Flutter application app, you need to configure your Strapi project permissions. Go to Settings → Users & Permissions plugin → Roles → Public and enable the following permissions for Product:
find
: Access to see all the products.findOne
: Access to view individual products.create
: Access to create a new product.update
: Access update products they created. delete
: Access to delete products they created.Next, locate the Users-permissions plugin, give the User collection, and give the Public role the following:
find
: Access to see who the seller of the product is. findOne
: Access to the individual product seller's details.We are done with setting up your backend. Without any server setup, you created a fully functional CRUD application all of this without writing a single line of boilerplate code.
Now, you can focus on building your frontend, knowing that your backend is secure, scalable, and ready for production.
Let's test the backend using Postman to ensure everything works as expected. Send a GET
request to the /api/products
endpoint to get all products.
Repeat the steps to test the Create(POST
request), Update(PUT
request), and Delete
endpoints.
Now, you are set to start integrating your Strapi backend with your Flutter application.
With your Strapi application fully configured, let's create the Flutter application. First, create a new Flutter project with the following command:
flutter create flutter_strapi_crud
cd flutter_strapi_crud
Once the project is successfully created, update your pubspec.yaml
file to add the dio package. The dio
package will allow you to make HTTP requests to your Strapi backend:
1dependencies:
2 flutter:
3 sdk: flutter
4 dio: ^5.4.0
Now run the command flutter pub get
to install the package new package you added.
Next, create lib/models/product.dart
file and add the code snippet below:
1class Product {
2 final int? id;
3 final String? documentId;
4 final String name;
5 final String description;
6 final double price;
7 final DateTime? createdAt;
8 final DateTime? updatedAt;
9 final DateTime? publishedAt;
10
11 Product({
12 this.id,
13 this.documentId,
14 required this.name,
15 required this.description,
16 required this.price,
17 this.createdAt,
18 this.updatedAt,
19 this.publishedAt,
20 });
21
22 factory Product.fromJson(Map<String, dynamic> json) {
23 if (json['attributes'] != null) {
24 final attributes = json['attributes'];
25 return Product(
26 id: json['id'],
27 documentId: attributes['documentId'],
28 name: attributes['name'],
29 description: attributes['description'],
30 price: double.parse(attributes['price'].toString()),
31 createdAt: DateTime.parse(attributes['createdAt']),
32 updatedAt: DateTime.parse(attributes['updatedAt']),
33 publishedAt: DateTime.parse(attributes['publishedAt']),
34 );
35 }
36
37 return Product(
38 id: int.parse(json['id'].toString()),
39 documentId: json['documentId'],
40 name: json['name'],
41 description: json['description'],
42 price: double.parse(json['price'].toString()),
43 createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
44 updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
45 publishedAt: json['publishedAt'] != null ? DateTime.parse(json['publishedAt']) : null,
46 );
47 }
48
49 Map<String, dynamic> toJson() {
50 return {
51 'name': name,
52 'description': description,
53 'price': price,
54 };
55 }
56}
The above model code defines our Product
class with JSON serialization support. The @JsonSerializable()
annotation and part
directive tell the code generator to create the corresponding product.g.dart
file, which will contain the implementation of the fromJson
and toJson
methods.
Now, generate this file by running the following command in your terminal:
1flutter pub run build_runner build
This will create the necessary serialization code for converting JSON data to Product objects and vice versa. If you change the model later, you must run this command again to regenerate the serialization code.
For faster development, you can use
flutter pub run build_runner watch
to automatically regenerate the code whenever you make changes.
Now, let's create our API service to handle all our products' CRUD (Create, Read, Update, Delete) operations. This service will use Dio to make HTTP requests to our Strapi backend. Create lib/services/product_service.dart
file and add the code snippet below:
1import 'package:dio/dio.dart';
2import 'package:flutter_strapi_crud/models/product.dart';
3
4class ProductService {
5 final Dio _dio;
6
7 ProductService()
8 : _dio = Dio(BaseOptions(
9 baseUrl: 'http://localhost:1337/api',
10 headers: {
11 'Content-Type': 'application/json',
12 },
13 ));
14
15 Future<List<Product>> getProducts() async {
16 try {
17 final response = await _dio.get('/products');
18 final List<dynamic> data = response.data['data'];
19 return data.map((item) => Product.fromJson(item)).toList();
20 } catch (e) {
21 throw 'Failed to load products: ${e.toString()}';
22 }
23 }
24
25 Future<Product> createProduct(Product product) async {
26 try {
27 final response = await _dio.post('/products', data: {
28 'data': {
29 'name': product.name,
30 'description': product.description,
31 'price': product.price,
32 }
33 });
34 return Product.fromJson(response.data['data']);
35 } catch (e) {
36 throw 'Failed to create product: ${e.toString()}';
37 }
38 }
39
40 Future<Product> updateProduct(String id, Product product) async {
41 try {
42 print(id);
43 final response = await _dio.put('/products/$id', data: {
44 'data': {
45 'name': product.name,
46 'description': product.description,
47 'price': product.price,
48 }
49 });
50 return Product.fromJson(response.data['data']);
51 } catch (e) {
52 throw 'Failed to update product: ${e.toString()}';
53 }
54 }
55
56 Future<void> deleteProduct(String id) async {
57 try {
58 await _dio.delete('/products/$id');
59 } catch (e) {
60 throw 'Failed to delete product: ${e.toString()}';
61 }
62 }
63}
The above code handles the CRUD (Create, Read, Update, Delete) operations using the Dio HTTP client for our Product
model. The ProductService
class is created with a baseURL
set to the Android emulator IP address (10.0.2.2)
and default headers set to accept the content type.
The service provides four main methods: getProducts()
, which retrieves and deserializes products into a List<Product>
createProduct()
, which is a POST request for creating the products using the data entered updateProduct()
which is a PUT request for updating existing products deleteProduct()
which is a delete request for removing the products.
Now, we need to create the main product screen, which will be responsible for showing the products and also for creating, updating, and even deleting products.
This screen will utilize our ProductService
to communicate with the Strapi backend while offering easy functionality for the user to manage products. It supports form handling with TextEditingControllers
, create/edit
dialogs, and the swipe to-delete feature. Create lib/screens/product_screen.dart
file and add the code snippet below:
1import 'package:flutter/material.dart';
2import 'package:flutter_strapi_crud/models/product.dart';
3import 'package:flutter_strapi_crud/services/product_service.dart';
4
5class ProductsScreen extends StatefulWidget {
6
7 _ProductsScreenState createState() => _ProductsScreenState();
8}
9
10class _ProductsScreenState extends State<ProductsScreen> {
11 final ProductService _productService = ProductService();
12 late Future<List<Product>> _productsFuture;
13
14 // Controllers for the form
15 final _nameController = TextEditingController();
16 final _descriptionController = TextEditingController();
17 final _priceController = TextEditingController();
18
19
20 void initState() {
21 super.initState();
22 _refreshProducts();
23 }
24
25
26 void dispose() {
27 _nameController.dispose();
28 _descriptionController.dispose();
29 _priceController.dispose();
30 super.dispose();
31 }
32
33 Future<void> _refreshProducts() async {
34 setState(() {
35 _productsFuture = _productService.getProducts();
36 });
37 }
38
39 void _showProductForm({Product? product}) {
40 // If editing, populate the controllers
41 if (product != null) {
42 _nameController.text = product.name;
43 _descriptionController.text = product.description;
44 _priceController.text = product.price.toString();
45 } else {
46 // Clear the controllers if creating new
47 _nameController.clear();
48 _descriptionController.clear();
49 _priceController.clear();
50 }
51
52 showDialog(
53 context: context,
54 builder: (context) => AlertDialog(
55 title: Text(product == null ? 'Create Product' : 'Edit Product'),
56 content: SingleChildScrollView(
57 child: Column(
58 mainAxisSize: MainAxisSize.min,
59 children: [
60 TextField(
61 controller: _nameController,
62 decoration: InputDecoration(labelText: 'Name'),
63 ),
64 TextField(
65 controller: _descriptionController,
66 decoration: InputDecoration(labelText: 'Description'),
67 maxLines: 3,
68 ),
69 TextField(
70 controller: _priceController,
71 decoration: InputDecoration(labelText: 'Price'),
72 keyboardType: TextInputType.number,
73 ),
74 ],
75 ),
76 ),
77 actions: [
78 TextButton(
79 onPressed: () => Navigator.pop(context),
80 child: Text('Cancel'),
81 ),
82 TextButton(
83 onPressed: () async {
84 try {
85 final newProduct = Product(
86 name: _nameController.text,
87 description: _descriptionController.text,
88 price: double.parse(_priceController.text),
89 );
90
91 if (product == null) {
92 await _productService.createProduct(newProduct);
93 } else {
94 await _productService.updateProduct(
95 product.documentId!, newProduct);
96 }
97
98 Navigator.pop(context);
99 _refreshProducts();
100 _showSnackBar(
101 'Product ${product == null ? 'created' : 'updated'} successfully');
102 } catch (e) {
103 _showSnackBar(e.toString());
104 }
105 },
106 child: Text(product == null ? 'Create' : 'Update'),
107 ),
108 ],
109 ),
110 );
111 }
112
113 Future<void> _deleteProduct(Product product) async {
114 final confirm = await showDialog<bool>(
115 context: context,
116 builder: (context) => AlertDialog(
117 title: Text('Delete Product'),
118 content: Text('Are you sure you want to delete ${product.name}?'),
119 actions: [
120 TextButton(
121 onPressed: () => Navigator.pop(context, false),
122 child: Text('Cancel'),
123 ),
124 TextButton(
125 onPressed: () => Navigator.pop(context, true),
126 child: Text('Delete'),
127 ),
128 ],
129 ),
130 );
131
132 if (confirm == true) {
133 try {
134 await _productService.deleteProduct(product.documentId!);
135 _refreshProducts();
136 _showSnackBar('Product deleted successfully');
137 } catch (e) {
138 _showSnackBar(e.toString());
139 }
140 }
141 }
142
143 void _showSnackBar(String message) {
144 ScaffoldMessenger.of(context).showSnackBar(
145 SnackBar(content: Text(message)),
146 );
147 }
148
149
150 Widget build(BuildContext context) {
151 return Scaffold(
152 appBar: AppBar(
153 title: Text('Products'),
154 ),
155 body: RefreshIndicator(
156 onRefresh: _refreshProducts,
157 child: FutureBuilder<List<Product>>(
158 future: _productsFuture,
159 builder: (context, snapshot) {
160 if (snapshot.connectionState == ConnectionState.waiting) {
161 return Center(child: CircularProgressIndicator());
162 }
163
164 if (snapshot.hasError) {
165 return Center(child: Text('Error: ${snapshot.error}'));
166 }
167
168 final products = snapshot.data!;
169
170 return ListView.builder(
171 itemCount: products.length,
172 itemBuilder: (context, index) {
173 final product = products[index];
174 return Dismissible(
175 key: Key(product.id.toString()),
176 direction: DismissDirection.endToStart,
177 background: Container(
178 color: Colors.red,
179 alignment: Alignment.centerRight,
180 padding: EdgeInsets.only(right: 16),
181 child: Icon(Icons.delete, color: Colors.white),
182 ),
183 onDismissed: (_) => _deleteProduct(product),
184 child: ListTile(
185 title: Text(product.name),
186 subtitle: Text(
187 '${product.description}\nPrice: \$${product.price.toStringAsFixed(2)}',
188 ),
189 trailing: Row(
190 mainAxisSize: MainAxisSize
191 .min, // Makes the Row take minimum space
192 children: [
193 IconButton(
194 icon: Icon(Icons.edit),
195 onPressed: () => _showProductForm(product: product),
196 ),
197 IconButton(
198 icon: Icon(Icons.delete),
199 onPressed: () => _deleteProduct(product),
200 ),
201 ],
202 ),
203 ));
204 },
205 );
206 },
207 ),
208 ),
209 floatingActionButton: FloatingActionButton(
210 onPressed: () => _showProductForm(),
211 child: Icon(Icons.add),
212 ),
213 );
214 }
215}
The following UI code creates a comprehensive product management screen in Flutter material design. The ProductsScreen
is a StatefulWidget
that stores the product data and employs the FutureBuilder
for the asynchronous computations.
For form input management, create/edit
forms, and delete confirmations, the widget uses TextEditingControllers
and AlertDialogs
; for delete functionality, it uses ListView.builder
with Dismissible
widgets. It uses RefreshIndicator
for pull-to-refresh
action, loading states with CircularProgressIndicator
and user feedback with SnackBar
notifications.
Now update your lib.main.dart
file to render the ProductsScreen
as the home screen:
1import 'package:flutter_strapi_crud/screens/product_screen.dart';
2
3
4//...
5
6class MyApp extends StatelessWidget {
7 const MyApp({super.key});
8 // This widget is the root of your application.
9
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 // Application name
13 title: 'Flutter Hello World',
14 // Application theme data, you can set the colors for the application as
15 // you want
16 theme: ThemeData(
17 // useMaterial3: false,
18 primarySwatch: Colors.blue,
19 ),
20 // A widget which will be started on application startup
21 home: ProductsScreen(),
22 );
23 }
24}
25
26//...
Now, let's test your Flutter CRUD application. Start your Flutter application by running the following commands:
flutter run
Then, create a new product using the floating action button.
Then, view the list of products on the main screen.
Next, Edit a product by tapping the edit icon.
Lastly, delete a product by clicking the delete icon.
Glad you made it to the end of this tutorial. Throughout this tutorial, you've learned how to create a CRUD application using Strapi 5 and Flutter. You started by creating a new Strapi 5 project, creating a collection, and defining permissions. Then, you went further to create a new Flutter application, installed dependencies and created a model, service, and product screens to perform CRUD operation by sending API requests to the endpoints generated with Strapi.
To learn more about building a backend with Strapi 5, visit the Strapi documentation and the Flutter official documentation if you which to add more features to your application. Happy coding!
I am Software Engineer and Technical Writer. Proficient Server-side scripting and Database setup. Agile knowledge of Python, NodeJS, ReactJS, and PHP. When am not coding, I share my knowledge and experience with the other developers through technical articles