These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Flutter?
Flutter is Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase. Instead of writing separate code for different platforms, you create apps that run seamlessly across iOS, Android, browsers, and desktops with one codebase. When you integrate Flutter with Strapi, you unlock even more potential for cross-platform development.
One of Flutter's core features is hot reload functionality, which lets you see code changes instantly in your running app. This significantly accelerates development—you can experiment with features, fix bugs, and refine designs without lengthy compile-and-deploy cycles.
In Flutter, everything is a widget. Buttons, text, layouts, animations—they're all widgets. This approach provides exceptional flexibility for custom interfaces while maintaining consistency across platforms. The framework includes pre-built widgets that follow each platform's design guidelines, ensuring your apps feel native on every device.
Flutter has experienced significant growth as a cross-platform development framework, winning over developers who need high-quality apps without the hassle of multiple codebases. It performs exceptionally well because it compiles to native machine code, avoiding the slow interpretation bridges that plague other cross-platform solutions.
What distinguishes Flutter is its rendering engine, which draws UI components directly to the screen rather than using platform-specific UI components. This provides consistent looks and behavior everywhere while delivering performance that rivals native applications.
For developers looking to reach users on every platform without doubling their workload or sacrificing quality, Flutter offers an effective balance between development efficiency and user experience. By choosing to integrate Flutter with Strapi, you can further enhance your app's capabilities with a powerful headless CMS.
Why Use Strapi with Flutter
When building modern applications, integrating Flutter with Strapi creates one of the most effective development stacks available. This combination brings together cross-platform development with headless CMS architecture, offering practical advantages that traditional approaches can't match.
Content Management Without Developer Bottlenecks
With this setup, your content team can update app content, launch campaigns, and change user-facing information without requiring developer intervention or waiting for app store approvals. Your Flutter app displays new content immediately—whether product details, blog posts, or promotional banners—while your development team focuses on building features rather than handling content requests.
API-First Architecture Built for Mobile
Strapi's approach as an API-First CMS aligns perfectly with how Flutter consumes data. Unlike traditional CMS platforms built primarily for websites, Strapi delivers clean, structured data that Flutter processes efficiently. You'll get a REST API out of the box, and can enable a GraphQL API by installing the official plugin—allowing you to choose whichever works best for your application's needs.
Customization Where It Matters
Both technologies excel at customization, providing an adaptable development environment. You can tailor Strapi's admin panel, build custom content types, and create complex data relationships that match your specific requirements. On the Flutter side, you have control over every aspect of the interface. This flexibility ensures you're never limited by rigid templates that don't fit your vision.
Single Backend, Multiple Platforms
Your Strapi instance serves your Flutter mobile app, web app, desktop version, and third-party integrations simultaneously. This eliminates the need to manage multiple backends or deal with content inconsistencies across platforms. Everything remains synchronized with identical content and consistent business logic.
Improved Developer Workflow
The separation of concerns creates a more efficient development experience. Frontend developers can work independently with mock data while backend developers configure content types and API endpoints. This parallel approach significantly reduces project timelines. Strapi's security features including role-based access control, JWT authentication, and data encryption provide robust protection without complex implementation.
Future-Proof Architecture
Strapi v5 introduces performance improvements and security enhancements that strengthen this integration. For techniques on optimizing your Strapi applications, refer to Strapi performance optimization. When combined with caching strategies and Flutter's efficient rendering, applications can perform well under heavy load. Headless CMS architecture also future-proofs your stack—you can adopt new frontend technologies without rebuilding your entire content system.
This combination scales effectively from small projects to enterprise applications. Strapi handles complex content relationships and high-traffic scenarios while Flutter ensures consistent performance across all devices and platforms.
Keep in touch with the latest Strapi and Flutter updates
How to Integrate Flutter with Strapi
Creating a solid integration between Flutter and Strapi requires a methodical approach to setup and implementation. Let's walk through connecting your headless CMS with your Flutter application, from environment setup to advanced content management.
Setting Up Your Development Environment
Before connecting Strapi with Flutter, you'll need the right tools and dependencies in place.
Your setup needs Node.js version 18 or later for Strapi. Check your installation with these commands:
node --version
npm --version
Install Flutter SDK following the official guide for your operating system, then verify with:
flutter --version
flutter doctor
The flutter doctor
command checks your environment and flags any issues you need to fix. Use Visual Studio Code with Flutter and Dart extensions for better coding experience with syntax highlighting, debugging, and integrated terminal access.
Create a project workspace:
mkdir flutter-strapi-integration
cd flutter-strapi-integration
Make sure your system PATH includes both Node.js and Flutter SDK directories. Double-check that your installed versions meet the minimum requirements for both tools. Windows users might find Windows Subsystem for Linux (WSL) helpful for a more consistent experience, especially with Node.js and npm packages.
To enhance your app's usability, you might consider building an offline-first application, allowing users to access content even without an internet connection.
Creating a Strapi Project
Your Strapi backend forms the foundation of your Flutter integration. Let's set it up with proper content types and API access.
Create a new Strapi project using the command line:
npx create-strapi-app@latest my-strapi-backend --quickstart
This creates a Strapi project with SQLite as the default database. The --quickstart
flag launches the development server after installation. Once finished, access your Strapi admin panel at http://localhost:1337/admin
and create your admin account.
Your project contains several key directories: /api
holds content types and custom controllers, /config
stores configuration files, /public
serves static files, and /src
contains your main application logic.
Head to the Content-Types Builder in your admin panel to create content types for your Flutter app. For example, create a "Product" type with fields like Name (Text), Description (Rich Text), Price (Number), and Image (Media).
Let's create a practical e-commerce product catalog:
- In the Content-Types Builder, click "Create new collection type"
- Name it "Product" and add these fields:
- Name (Text) - Required
- Description (Rich Text)
- Price (Number, decimal) - Required
- Discount (Number, decimal)
- Category (Enumeration: "Electronics", "Clothing", "Home", "Books")
- Featured (Boolean)
- Stock (Number, integer) - Required
- Images (Media, multiple)
- SKU (Text, unique) - Required
- Create another type called "Category" with:
- Name (Text) - Required
- Description (Text)
- Icon (Media)
- Establish a relation between Product and Category (many-to-one)
For development, enable API access in Settings > Users & Permissions Plugin > Roles > Public, and grant permissions for your content types. Enable at least find
and findOne
permissions for read access. For authenticated operations, configure the Authenticated
role with appropriate CRUD permissions.
Building a Flutter Application
Your Flutter app will consume content from your Strapi backend. Let's set it up with a clean structure.
Create your Flutter project:
flutter create my_flutter_app
cd my_flutter_app
Add these packages to your pubspec.yaml
file:
1dependencies:
2 flutter:
3 sdk: flutter
4 http: ^1.1.0
5 dio: ^5.3.2
6 flutter_secure_storage: ^9.0.0
7 provider: ^6.1.1
8 cached_network_image: ^3.3.0
Run flutter pub get
to install them.
Organize your project with a clear structure: /lib/models
for data models matching your Strapi content types, /lib/services
for API services, /lib/screens
for UI screens, /lib/widgets
for reusable components, and /lib/providers
for state management.
Create a product model that matches your Strapi content type:
1// lib/models/product.dart
2class Product {
3 final int id;
4 final String name;
5 final String description;
6 final double price;
7 final double? discount;
8 final String category;
9 final bool featured;
10 final int stock;
11 final List<String> imageUrls;
12 final String sku;
13
14 Product({
15 required this.id,
16 required this.name,
17 required this.description,
18 required this.price,
19 this.discount,
20 required this.category,
21 required this.featured,
22 required this.stock,
23 required this.imageUrls,
24 required this.sku,
25 });
26
27 factory Product.fromJson(Map<String, dynamic> json) {
28 return Product(
29 id: json['id'],
30 name: json['attributes']['name'],
31 description: json['attributes']['description'] ?? '',
32 price: double.parse(json['attributes']['price'].toString()),
33 discount: json['attributes']['discount'] != null
34 ? double.parse(json['attributes']['discount'].toString())
35 : null,
36 category: json['attributes']['category'] ?? '',
37 featured: json['attributes']['featured'] ?? false,
38 stock: json['attributes']['stock'] ?? 0,
39 imageUrls: (json['attributes']['images']['data'] as List?)?.map((img) =>
40 'http://localhost:1337${img['attributes']['url']}').toList() ?? [],
41 sku: json['attributes']['SKU'] ?? '',
42 );
43 }
44}
Create an API service to fetch products:
1// lib/services/api_service.dart
2import 'dart:convert';
3import 'package:http/http.dart' as http;
4import '../models/product.dart';
5
6class ApiService {
7 final String baseUrl = 'http://localhost:1337/api';
8
9 Future<List<Product>> getProducts() async {
10 final response = await http.get(Uri.parse('$baseUrl/products?populate=images'));
11
12 if (response.statusCode == 200) {
13 final jsonData = json.decode(response.body);
14 final productsData = jsonData['data'] as List;
15 return productsData.map((productJson) => Product.fromJson(productJson)).toList();
16 } else {
17 throw Exception('Failed to load products: ${response.statusCode}');
18 }
19 }
20
21 Future<Product> getProduct(int id) async {
22 final response = await http.get(Uri.parse('$baseUrl/products/$id?populate=images'));
23
24 if (response.statusCode == 200) {
25 final jsonData = json.decode(response.body);
26 return Product.fromJson(jsonData['data']);
27 } else {
28 throw Exception('Failed to load product: ${response.statusCode}');
29 }
30 }
31}
Set up your main application structure in lib/main.dart
:
1import 'package:flutter/material.dart';
2import 'package:provider/provider.dart';
3import 'screens/products_screen.dart';
4import 'providers/product_provider.dart';
5
6void main() {
7 runApp(MyApp());
8}
9
10class MyApp extends StatelessWidget {
11
12 Widget build(BuildContext context) {
13 return MultiProvider(
14 providers: [
15 ChangeNotifierProvider(create: (_) => ProductProvider()),
16 ],
17 child: MaterialApp(
18 title: 'Flutter Strapi E-commerce',
19 theme: ThemeData(
20 primarySwatch: Colors.blue,
21 visualDensity: VisualDensity.adaptivePlatformDensity,
22 ),
23 home: ProductsScreen(),
24 ),
25 );
26 }
27}
Create a provider for state management:
1// lib/providers/product_provider.dart
2import 'package:flutter/foundation.dart';
3import '../models/product.dart';
4import '../services/api_service.dart';
5
6class ProductProvider with ChangeNotifier {
7 List<Product> _products = [];
8 bool _isLoading = false;
9 final ApiService _apiService = ApiService();
10
11 List<Product> get products => _products;
12 bool get isLoading => _isLoading;
13
14 Future<void> fetchProducts() async {
15 _isLoading = true;
16 notifyListeners();
17
18 try {
19 _products = await _apiService.getProducts();
20 } catch (e) {
21 print('Error fetching products: $e');
22 } finally {
23 _isLoading = false;
24 notifyListeners();
25 }
26 }
27}
Create a screen to display products:
1// lib/screens/products_screen.dart
2import 'package:flutter/material.dart';
3import 'package:provider/provider.dart';
4import '../providers/product_provider.dart';
5import '../widgets/product_card.dart';
6
7class ProductsScreen extends StatefulWidget {
8
9 _ProductsScreenState createState() => _ProductsScreenState();
10}
11
12class _ProductsScreenState extends State<ProductsScreen> {
13
14 void initState() {
15 super.initState();
16 Future.microtask(() =>
17 Provider.of<ProductProvider>(context, listen: false).fetchProducts()
18 );
19 }
20
21
22 Widget build(BuildContext context) {
23 return Scaffold(
24 appBar: AppBar(
25 title: Text('Products'),
26 ),
27 body: Consumer<ProductProvider>(
28 builder: (ctx, productProvider, child) {
29 if (productProvider.isLoading) {
30 return Center(child: CircularProgressIndicator());
31 }
32
33 if (productProvider.products.isEmpty) {
34 return Center(child: Text('No products found'));
35 }
36
37 return GridView.builder(
38 padding: EdgeInsets.all(10),
39 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
40 crossAxisCount: 2,
41 childAspectRatio: 2/3,
42 crossAxisSpacing: 10,
43 mainAxisSpacing: 10,
44 ),
45 itemCount: productProvider.products.length,
46 itemBuilder: (ctx, i) => ProductCard(
47 product: productProvider.products[i],
48 ),
49 );
50 },
51 ),
52 );
53 }
54}
Create configuration files to manage different API endpoints for development and production environments. For instance, create an environment_config.dart
file:
1// lib/config/environment_config.dart
2class EnvironmentConfig {
3 static const bool isDevelopment = true;
4
5 static String get baseUrl => isDevelopment
6 ? 'http://localhost:1337/api'
7 : 'https://your-production-strapi.com/api';
8
9 static String get imageBaseUrl => isDevelopment
10 ? 'http://localhost:1337'
11 : 'https://your-production-strapi.com';
12}
This configuration approach allows you to easily switch between development and production environments without changing code throughout your application.
Implementing Authentication
Implementing authentication between Flutter and Strapi requires careful handling of JWT tokens and secure storage. For a foundational understanding of authentication in Strapi, you can refer to the beginner's guide.
Create an authentication service class:
1import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2import 'package:dio/dio.dart';
3import 'dart:convert';
4
5class AuthService {
6 final String baseUrl;
7 final Dio _dio = Dio();
8 final FlutterSecureStorage _storage = FlutterSecureStorage();
9
10 AuthService({required this.baseUrl});
11
12 Future<bool> login(String identifier, String password) async {
13 try {
14 final response = await _dio.post(
15 '$baseUrl/api/auth/local',
16 data: {
17 'identifier': identifier,
18 'password': password,
19 },
20 );
21
22 if (response.statusCode == 200) {
23 final token = response.data['jwt'];
24 final user = response.data['user'];
25
26 await _storage.write(key: 'jwt', value: token);
27 await _storage.write(key: 'user', value: jsonEncode(user));
28
29 return true;
30 }
31 return false;
32 } catch (e) {
33 print('Login error: $e');
34 return false;
35 }
36 }
37
38 Future<String?> getToken() async {
39 return await _storage.read(key: 'jwt');
40 }
41
42 Future<bool> isAuthenticated() async {
43 final token = await getToken();
44 return token != null;
45 }
46
47 Future<void> logout() async {
48 await _storage.delete(key: 'jwt');
49 await _storage.delete(key: 'user');
50 }
51}
The flutter_secure_storage
package encrypts your stored data, adding security for sensitive information like JWT tokens.
Implement token refresh mechanisms to maintain user sessions without frequent re-logins. Check token expiration before making API requests to prevent failed calls. Create widgets that respond to login state changes, and use Provider or similar state management to update your UI automatically when users log in or out.
Creating CRUD Operations
Building comprehensive CRUD functionality requires understanding both Strapi's API structure and Flutter's HTTP communication. To explore this further, consider building a CRUD application with Strapi, which involves handling API requests, responses, and errors properly.
Create a service class for your content types:
1class ProductService {
2 final String baseUrl;
3 final Dio _dio = Dio();
4
5 ProductService({required this.baseUrl});
6
7 Future<List<Product>> getProducts() async {
8 try {
9 final token = await AuthService().getToken();
10 final response = await _dio.get(
11 '$baseUrl/api/products',
12 options: Options(
13 headers: {
14 'Authorization': 'Bearer $token',
15 },
16 ),
17 );
18
19 if (response.statusCode == 200) {
20 final List<dynamic> data = response.data['data'];
21 return data.map((item) => Product.fromJson(item)).toList();
22 }
23 return [];
24 } catch (e) {
25 throw Exception('Failed to load products: $e');
26 }
27 }
28
29 Future<bool> createProduct(Product product) async {
30 try {
31 final token = await AuthService().getToken();
32 final response = await _dio.post(
33 '$baseUrl/api/products',
34 data: {
35 'data': product.toJson(),
36 },
37 options: Options(
38 headers: {
39 'Authorization': 'Bearer $token',
40 'Content-Type': 'application/json',
41 },
42 ),
43 );
44
45 return response.statusCode == 200 || response.statusCode == 201;
46 } catch (e) {
47 print('Create product error: $e');
48 return false;
49 }
50 }
51
52 Future<bool> updateProduct(int id, Product product) async {
53 try {
54 final token = await AuthService().getToken();
55 final response = await _dio.put(
56 '$baseUrl/api/products/$id',
57 data: {
58 'data': product.toJson(),
59 },
60 options: Options(
61 headers: {
62 'Authorization': 'Bearer $token',
63 'Content-Type': 'application/json',
64 },
65 ),
66 );
67
68 return response.statusCode == 200;
69 } catch (e) {
70 print('Update product error: $e');
71 return false;
72 }
73 }
74
75 Future<bool> deleteProduct(int id) async {
76 try {
77 final token = await AuthService().getToken();
78 final response = await _dio.delete(
79 '$baseUrl/api/products/$id',
80 options: Options(
81 headers: {
82 'Authorization': 'Bearer $token',
83 },
84 ),
85 );
86
87 return response.statusCode == 200;
88 } catch (e) {
89 print('Delete product error: $e');
90 return false;
91 }
92 }
93}
Create data models matching your Strapi content:
1class Product {
2 final int? id;
3 final String name;
4 final String description;
5 final double price;
6 final String? imageUrl;
7
8 Product({
9 this.id,
10 required this.name,
11 required this.description,
12 required this.price,
13 this.imageUrl,
14 });
15
16 factory Product.fromJson(Map<String, dynamic> json) {
17 return Product(
18 id: json['id'],
19 name: json['attributes']['name'],
20 description: json['attributes']['description'],
21 price: json['attributes']['price'].toDouble(),
22 imageUrl: json['attributes']['image']?['data']?['attributes']?['url'],
23 );
24 }
25
26 Map<String, dynamic> toJson() {
27 return {
28 'name': name,
29 'description': description,
30 'price': price,
31 };
32 }
33}
Add robust error handling with custom exception classes for different error types and retry mechanisms for temporary failures. Use loading states in your UI to show when operations are in progress, improving user experience.
Managing CMS Content Updates
Efficient content management requires minimizing API calls while keeping your Flutter UI updated with the latest CMS changes. This means implementing good state management, caching, and update strategies.
Create a provider-based state system:
1class ContentProvider extends ChangeNotifier {
2 List<Product> _products = [];
3 bool _isLoading = false;
4 String? _error;
5
6 List<Product> get products => _products;
7 bool get isLoading => _isLoading;
8 String? get error => _error;
9
10 final ProductService _productService = ProductService(baseUrl: 'http://localhost:1337');
11
12 Future<void> fetchProducts() async {
13 _isLoading = true;
14 _error = null;
15 notifyListeners();
16
17 try {
18 _products = await _productService.getProducts();
19 } catch (e) {
20 _error = e.toString();
21 } finally {
22 _isLoading = false;
23 notifyListeners();
24 }
25 }
26
27 Future<void> addProduct(Product product) async {
28 final success = await _productService.createProduct(product);
29 if (success) {
30 await fetchProducts(); // Refresh the list
31 }
32 }
33
34 Future<void> updateProduct(int id, Product product) async {
35 final success = await _productService.updateProduct(id, product);
36 if (success) {
37 await fetchProducts(); // Refresh the list
38 }
39 }
40
41 Future<void> deleteProduct(int id) async {
42 final success = await _productService.deleteProduct(id);
43 if (success) {
44 _products.removeWhere((product) => product.id == id);
45 notifyListeners();
46 }
47 }
48}
Implement local caching to reduce API calls and work offline. Use hive
or sqflite
for local data storage. Cache frequently accessed content and set up cache invalidation based on your freshness requirements.
For better user experience, implement optimistic updates where the UI shows changes immediately before server confirmation. If server operations fail, roll back the UI changes and tell the user what happened.
For real-time updates, consider WebSockets or periodic polling to catch content changes. Create component-based architectures where Flutter widgets directly map to CMS components, letting your UI adapt dynamically to content structure changes in your Strapi admin panel.
Keep in touch with the latest Strapi and Flutter updates
Project Example: Build a Product Catalog App with Flutter and Strapi
Let's walk through a real-world Product Catalog App that demonstrates how to integrate Flutter with Strapi in practice. This example brings together everything we've covered, from authentication to CRUD operations and performance tuning.
Case Study: Building a Product Catalog App
Strapi's product catalog app shows how to build a scalable e-commerce solution by integrating Flutter with Strapi. It includes product listing, detailed views, search, and user authentication—all following CRUD patterns tested in production.
The architecture separates concerns clearly: dedicated services handle API communication, state management updates the UI, and optimized data models parse information efficiently. The Flutter frontend communicates with Strapi through REST APIs with proper error handling and offline capabilities.
Here's how the authentication service handles JWT tokens:
1class AuthService {
2 final String baseUrl;
3 final storage = FlutterSecureStorage();
4
5 AuthService({required this.baseUrl});
6
7 Future<bool> login(String email, String password) async {
8 try {
9 final response = await http.post(
10 Uri.parse('$baseUrl/api/auth/local'),
11 body: {
12 'identifier': email,
13 'password': password,
14 },
15 );
16
17 if (response.statusCode == 200) {
18 final data = json.decode(response.body);
19 await storage.write(key: 'jwt', value: data['jwt']);
20 await storage.write(key: 'refreshToken', value: data['refreshToken']);
21 return true;
22 }
23 return false;
24 } catch (e) {
25 return false;
26 }
27 }
28
29 Future<String?> getValidToken() async {
30 final token = await storage.read(key: 'jwt');
31 if (token == null) return null;
32
33 final jwt = parseJwt(token);
34 final expiry = DateTime.fromMillisecondsSinceEpoch(jwt['exp'] * 1000);
35
36 if (DateTime.now().isAfter(expiry)) {
37 return refreshToken();
38 }
39
40 return token;
41 }
42}
The product service handles CRUD operations with retry logic and timeout handling:
1class ProductService {
2 final String baseUrl;
3 final AuthService authService;
4
5 ProductService({required this.baseUrl, required this.authService});
6
7 Future<List<Product>> getProducts({int page = 1, int pageSize = 20}) async {
8 final token = await authService.getValidToken();
9
10 final response = await http.get(
11 Uri.parse('$baseUrl/api/products?pagination[page]=$page&pagination[pageSize]=$pageSize'),
12 headers: {
13 'Authorization': 'Bearer $token',
14 'Content-Type': 'application/json',
15 },
16 ).timeout(const Duration(seconds: 10));
17
18 if (response.statusCode == 200) {
19 final data = json.decode(response.body);
20 return (data['data'] as List)
21 .map((item) => Product.fromJson(item))
22 .toList();
23 }
24
25 throw Exception('Failed to load products');
26 }
27
28 Future<bool> createProduct(Product product) async {
29 final token = await authService.getValidToken();
30
31 final response = await http.post(
32 Uri.parse('$baseUrl/api/products'),
33 headers: {
34 'Authorization': 'Bearer $token',
35 'Content-Type': 'application/json',
36 },
37 body: json.encode({
38 'data': product.toJson(),
39 }),
40 );
41
42 return response.statusCode == 200 || response.statusCode == 201;
43 }
44}
The app addressed several real-world challenges. Managing complex data relationships between products, categories, and user preferences required careful API design. Image handling needed optimization for different screen sizes and network conditions, solved with progressive loading and adaptive quality based on connection speed.
Authentication flow required particular attention, as the app needed to refresh tokens seamlessly without disrupting users. The solution includes automatic retry mechanisms and graceful login fallbacks when tokens expire.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours Monday through Friday at 12:30 pm – 1:30 pm CST.
For more details, visit the Strapi documentation and Flutter documentation.