Preventing Reverse Engineering of Flutter Apps: Obfuscation & Runtime Protections

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table of Contents:

Introduction

Understanding Reverse Engineering in Flutter Apps

How Flutter Apps Are Packaged

Common Reverse Engineering Techniques

Why Flutter Apps Are a Target

Obfuscation: The First Line of Defense

Dart Code Obfuscation in Flutter

Benefits of Dart Obfuscation

Limitations of Dart Obfuscation

Native-Level Obfuscation

Runtime Protections: Defending Against Dynamic Attacks

Common Mistakes to Avoid

Conclusion


Introduction

Flutter has become one of the most popular frameworks for building cross-platform mobile applications, powering products across fintech, healthcare, e-commerce, logistics, and enterprise mobility. Its promise of a single codebase, fast UI rendering, and near-native performance makes it attractive for both startups and large organizations. However, as Flutter adoption grows, so does attacker interest. Reverse engineering Flutter apps to steal business logic, API secrets, proprietary algorithms, or to tamper with application behavior is now a real and recurring threat.

Unlike server-side systems, mobile applications are distributed directly to end-user devices. Anyone can download an APK or IPA, inspect it, modify it, and attempt to understand how it works. Flutter apps are no exception. Even though Flutter compiles Dart code ahead-of-time (AOT) for release builds, a determined attacker can still analyze binaries, extract assets, inspect method names, intercept runtime behavior, and manipulate execution flow.

This blog provides a deep, practical guide to preventing reverse engineering of Flutter apps. It focuses on two pillars of mobile application security: obfuscation and runtime protections. You will learn how Flutter apps are reverse-engineered, where they are vulnerable, what Flutter offers out of the box, and how to design a layered defense strategy that significantly raises the cost of attacks.


Understanding Reverse Engineering in Flutter Apps

Reverse engineering is the process of analyzing an application to understand its internal workings without access to the source code. For Flutter apps, attackers typically aim to:

• Steal proprietary business logic or algorithms
• Extract API keys, tokens, or endpoints
• Bypass licensing or subscription checks
• Disable security controls
• Modify app behavior or create pirated versions
• Automate fraud or abuse

How Flutter Apps Are Packaged

On Android, Flutter apps are distributed as APKs or Android App Bundles (AABs). On iOS, they are packaged as IPAs. Internally, a Flutter release build contains:

• Native binaries (ARM64, ARMv7)
• Flutter engine
• Dart AOT-compiled code snapshot
• Assets (images, JSON, configuration files)
• Platform-specific code (Kotlin/Java, Swift/Objective-C)

While Dart code is not shipped as plain text, the compiled snapshot still contains symbols, method metadata, and recognizable structures that can be analyzed with reverse engineering tools.

Common Reverse Engineering Techniques

Attackers commonly use a combination of static and dynamic analysis techniques:

Static analysis involves inspecting the app without executing it. Tools like JADX, apktool, Ghidra, Hopper, and IDA Pro are used to decompile native libraries and inspect resources.

Dynamic analysis involves running the app on a rooted or jailbroken device or emulator. Attackers use tools like Frida, Objection, Xposed, or Magisk to hook functions, inspect memory, intercept API calls, and bypass checks at runtime.

Flutter apps are often assumed to be safer because Dart is compiled, but this is a misconception. Without proper protections, Flutter apps are still vulnerable to both static and dynamic reverse engineering.


Why Flutter Apps Are a Target

Flutter apps frequently contain high-value assets:

• Embedded API endpoints and keys
• Business rules (pricing, discounts, eligibility)
• Feature flags and premium logic
• Cryptographic implementations
• Offline logic for field or enterprise apps

Because Flutter shares a single codebase across platforms, a successful reverse engineering effort can compromise both Android and iOS simultaneously. This makes Flutter apps especially attractive targets.

Another factor is developer overconfidence. Many teams rely solely on Flutter’s default release build optimizations, assuming that AOT compilation is sufficient. In reality, without additional protections, Flutter apps remain readable, hookable, and modifiable.


Obfuscation: The First Line of Defense

Obfuscation is the process of transforming code into a form that is functionally equivalent but significantly harder for humans and tools to understand. It does not make reverse engineering impossible, but it increases effort, time, and cost for attackers.

Dart Code Obfuscation in Flutter

Flutter provides built-in support for Dart obfuscation. When enabled, identifiers such as class names, method names, and variables are replaced with meaningless symbols.

In a release build, obfuscation is enabled using build flags:

--obfuscate
--split-debug-info

Obfuscation ensures that even if an attacker extracts the compiled snapshot, the logical structure becomes much harder to follow.

Benefits of Dart Obfuscation

Dart obfuscation:

• Removes meaningful symbol names
• Reduces readability of stack traces
• Complicates static analysis
• Slows down reverse engineering

It is especially effective against casual attackers or automated tools that rely on recognizable identifiers.

Limitations of Dart Obfuscation

Despite its usefulness, Dart obfuscation has limitations:

• It does not encrypt logic
• Control flow remains analyzable
• Strings and constants may remain visible
• Dynamic analysis is still possible

This is why obfuscation should never be your only security measure.


Native-Level Obfuscation

Flutter apps include native code through plugins and platform channels. This native layer is often targeted because it can expose sensitive logic or security checks.

Android Native Obfuscation

On Android, ProGuard or R8 can be used to obfuscate Java and Kotlin code. This is essential if your Flutter app includes:

• Custom Android plugins
• Native SDK integrations
• Security or cryptographic logic

R8 can:

• Rename classes and methods
• Remove unused code
• Optimize bytecode

However, attackers can still decompile native libraries if symbols are not stripped.

iOS Native Obfuscation

On iOS, Swift and Objective-C binaries can be symbolicated unless additional steps are taken. Techniques include:

• Stripping symbols from release builds
• Using compiler optimizations
• Avoiding debug metadata

While iOS binaries are harder to reverse engineer than Android bytecode, they are not immune.


String and Asset Protection

One of the most common mistakes in Flutter apps is leaving sensitive information in plain text.

Common Sensitive Assets

• API keys
• OAuth client IDs
• Encryption keys
• Feature toggles
• Internal URLs

Even with obfuscation enabled, hardcoded strings can often be extracted directly from the binary or asset files.

Protecting Strings

Effective strategies include:

• Encrypting sensitive strings at rest
• Decrypting them only at runtime
• Splitting keys across multiple locations
• Deriving secrets dynamically

Avoid storing secrets directly in Dart files or asset JSONs.

Asset Obfuscation

Flutter assets such as JSON configuration files or feature flags are often left unprotected. Attackers can easily inspect these assets by unpacking the app.

Sensitive configuration should be fetched securely from a backend and validated server-side rather than bundled with the app.


Runtime Protections: Defending Against Dynamic Attacks

While obfuscation protects against static analysis, runtime protections are designed to detect or prevent dynamic analysis and tampering.

Root and Jailbreak Detection

Rooted or jailbroken devices allow attackers to:

• Bypass OS-level security
• Inject code into running apps
• Intercept traffic
• Modify memory

Flutter apps should implement robust root and jailbreak detection to identify compromised environments.

These checks can be implemented using:

• Native platform code
• File system checks
• Process inspection
• System property validation

Detection should be layered and periodically re-evaluated during runtime.

Debugger and Emulator Detection

Attackers often analyze apps using emulators or attach debuggers to inspect behavior.

Runtime checks can detect:

• Debugger attachment
• Emulator-specific properties
• Unusual execution environments

When detected, apps may limit functionality, terminate sessions, or trigger additional verification.


Anti-Tampering Mechanisms

Tampering involves modifying the app to remove restrictions, bypass checks, or inject malicious behavior.

App Integrity Checks

Flutter apps can verify their own integrity by:

• Checking application signatures
• Verifying checksums of binaries
• Detecting modified resources

If integrity checks fail, the app can refuse to run or restrict sensitive operations.

Runtime Self-Checks

Self-checking logic can validate critical code paths during execution. This makes it harder for attackers to patch logic without breaking functionality.


Anti-Hooking and Anti-Instrumentation

Dynamic instrumentation tools such as Frida are widely used to hook functions at runtime.

Detecting Hooking Frameworks

Apps can scan for:

• Known Frida artifacts
• Suspicious memory mappings
• Unexpected libraries

While no detection is foolproof, layered checks significantly increase attack complexity.

Native-Level Protections

Critical logic can be moved to native code with additional protections, making it harder to hook from Dart-level instrumentation.


Secure API Communication

Reverse engineering is often used to understand backend APIs and automate abuse.

Certificate Pinning

Certificate pinning ensures that the app only communicates with trusted servers, preventing man-in-the-middle attacks.

Request Signing

Signing API requests with dynamic, time-bound tokens makes it difficult for attackers to replay or forge requests, even if endpoints are discovered.

Server-Side Validation

Never trust the client. All critical logic, entitlements, and validations should be enforced server-side.


Code Architecture for Security

Security should be a design consideration, not an afterthought.

Minimize Client-Side Logic

Avoid placing sensitive business rules on the client. The more logic you ship, the more there is to reverse engineer.

Feature Flagging

Control features dynamically from the server. This allows you to disable abused features without app updates.

Modularization

Separating critical logic into smaller, independently protected components reduces the blast radius of a compromise.


Balancing Security and Performance

Security controls come with trade-offs:

• Obfuscation can complicate debugging
• Runtime checks may impact performance
• Aggressive protections may affect user experience

The goal is not absolute prevention, but risk reduction. Implement protections proportionate to the value of the assets being protected.


Common Mistakes to Avoid

Many Flutter apps remain vulnerable due to avoidable mistakes:

• Relying solely on Dart obfuscation
• Hardcoding secrets in the app
• Skipping runtime protections
• Trusting client-side checks
• Ignoring native-layer security

Avoiding these pitfalls dramatically improves resilience.


A Layered Defense Strategy

The most effective approach is defense in depth:

• Dart and native obfuscation
• String and asset protection
• Root, emulator, and debugger detection
• Anti-tampering and integrity checks
• Secure backend communication
• Server-side enforcement

Each layer raises the cost of attack, discouraging most adversaries.


Conclusion

Reverse engineering of Flutter apps is a real and growing threat, especially for applications that handle sensitive data, proprietary logic, or financial transactions. While Flutter’s AOT compilation and release optimizations provide a baseline level of protection, they are not sufficient on their own.

By combining robust obfuscation with comprehensive runtime protections, secure architecture, and strong backend enforcement, you can significantly reduce the risk of intellectual property theft, fraud, and tampering. The objective is not to make reverse engineering impossible, but to make it expensive, time-consuming, and unattractive.

In an ecosystem where attackers continuously evolve, proactive security investment is no longer optional. Treat your Flutter app as a distributed system component that deserves the same level of protection as your backend, and you will be far better prepared for the threats ahead.

References:

Obfuscate Dart code
How to remove function and class names from your Dart binary.docs.flutter.dev

How to protect Flutter app from reverse engineering
I am trying to develop a payment application using Flutter, is there any way to protect my application API’s and Tokens…stackoverflow.com

https://www.appknox.com/blog/reverse-engineering-flutter-apps-what-you-need-to-know


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Code Splitting & Lazy Loading in Flutter: Speeding Up App Startup Times

In the world of mobile app development, performance is paramount. A slow-loading application can lead to high uninstallation rates and poor user reviews. For large Flutter applications, especially those targeting Android and Web platforms, techniques like code splitting and lazy loading are essential for reducing initial bundle size and significantly improving app startup times.

This blog post will dive deep into how Flutter handles these concepts, introduction, what code splitting is, why startup becomes slow, Flutter’s lazy loading patterns, route-based loading, deferred imports, and real, production-grade examples, and walk you through the implementation steps for an optimized user experience.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Introduction

Why Flutter Apps Become Slow at Startup

What is Code Splitting in Flutter?

Lazy Loading vs Deferred Loading

Example Scenario: A Growing App

Code Splitting via Route-Based Lazy Loading

Lazy Loading Heavy UI Components

Benefits of Deferred Loading

Deferred Loading — Flutter’s Most Powerful Code-Splitting Tool

Splitting a Heavy Feature (Chat Module)

Lazy Loading Controllers, Loading Images & Assets

Performance Gains (Realistic Numbers)

Final Example: Clean Architecture + Lazy Loading Setup

Conclusion



Introduction

Mobile users expect apps to open instantly. Every millisecond matters—especially on mid-range Android devices where startup time directly impacts user retention. As your Flutter project grows, so does the app size, widget tree complexity, initialization logic, and asset loading overhead.

That’s where Code Splitting and Lazy Loading come in.

These techniques break your app into smaller, self-contained chunks so that only the minimum code needed for the first screen loads at startup. Everything else loads on-demand, improving:

  • Time To First Render (TTFR)
  • Perceived performance
  • Smoothness of navigation
  • Memory efficiency

Why Flutter Apps Become Slow at Startup

Even though Flutter compiles to native ARM code, a large project still suffers from these bottlenecks:

Large Widget Tree: If your app builds multiple heavy screens during startup (e.g., dashboard, analytics, maps), the initial layout and paint operations slow down the time it takes the user to see the UI.

Too Many Global Providers / Controllers:-

Developers often initialize:

  • 10+ Riverpod providers
  • Firebase
  • Local DB (Hive, Drift)
  • Analytics SDKs
  • Task managers

All this happens before runApp() finishes building the UI.

Large Assets & Fonts:-

Huge images, custom fonts, and lottie animations increase load time because Flutter bundles and parses them before usable UI is rendered.

Codebase Monolith:- When everything is bundled as a single chunk:

  • The binary becomes large.
  • Isolates cannot load minimal code.
  • Memory footprint increases.

Solution → Code splitting + Lazy loading.

What is Code Splitting in Flutter?

Code splitting means dividing your codebase into small chunks instead of one monolithic binary. Flutter can load these chunks only when needed, not at startup.

Flutter supports two major techniques:

1. Route-based code splitting using separate Dart files

(Load screens only when navigated.)

2. Lazy loading using FutureBuilder or async controllers

3. Deferred Imports

Flutter’s most powerful lazy-load mechanism.

Lazy Loading vs Deferred Loading

FeatureLazy LoadingDeferred Loading
When usedLoad UI/data/controllers when neededLoad Dart CODE only when needed
Helps withRuntime performanceApp size & startup time
ExampleFetching list only after button clickLoading a module like “Chat” only when user opens chat

Both work beautifully together.

Example Scenario: A Growing App

Your app has:

  • Dashboard (default)
  • Chat module
  • Analytics module
  • Settings
  • AI assistant

Most users open dashboard first. So Chat, Analytics, and AI screens do NOT need to load during startup.

Let’s split them into separate modules and load when required.

Code Splitting via Route-Based Lazy Loading

Instead of importing all screens in main.dart:

import 'dashboard.dart';
import 'chat.dart';
import 'analytics.dart';
import 'settings.dart';
import 'ai_assistant.dart';

We only load the entry screen:

import 'dashboard.dart';

Other pages will be loaded lazily using named routes.

main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Lazy App',
      home: DashboardScreen(),
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case '/chat':
            return MaterialPageRoute(
              builder: (_) => ChatScreen(),   // loads file only when used
            );
          case '/analytics':
            return MaterialPageRoute(
              builder: (_) => AnalyticsScreen();
            );
          default:
            return MaterialPageRoute(builder: (_) => DashboardScreen());
        }
      },
    );
  }
}

How does this help?

  • Flutter loads chat.dart only when /chat route is used.
  • The startup bundle only contains dashboard code.

Lazy Loading Heavy UI Components

You can avoid building heavy UI until needed.

Example: load analytics chart only after user taps a button.

class AnalyticsScreen extends StatefulWidget {
  @override
  _AnalyticsScreenState createState() => _AnalyticsScreenState();
}

class _AnalyticsScreenState extends State<AnalyticsScreen> {
  bool loadChart = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Analytics")),
      body: Center(
        child: loadChart
            ? FutureBuilder(
                future: loadAnalyticsData(),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) return CircularProgressIndicator();
                  return ChartWidget(data: snapshot.data!);
                },
              )
            : ElevatedButton(
                onPressed: () => setState(() => loadChart = true),
                child: Text("Load chart"),
              ),
      ),
    );
  }
}

Benefits of Deferred Loading

  • Reduced Initial Download Size: Users only download the core app needed for the first launch, reducing the barrier to entry.
  • Faster Startup Times: Less code to initialize at launch means a snappier app start.
  • Optimized Resource Usage: Unused features or large assets aren’t loaded into memory unless explicitly requested.
  • On-Demand Features: Allows for optional or premium features to be downloaded only by users who need them (e.g., game levels, specific language packs).

Deferred Loading — Flutter’s Most Powerful Code-Splitting Tool

Deferred loading allows Dart to load libraries only when requested, reducing initial binary load.

Use Case: You want to load the entire Chat module only when the user goes to Chat.

Example Folder Structure

lib/
  main.dart
  dashboard/
  chat/   <-- heavy logic here (AI, socket, Firestore, media)

Deferred Import Syntax

import 'package:myapp/chat/chat_screen.dart' deferred as chatModule;

Loading Module on Demand

ElevatedButton(
  onPressed: () async {
    await chatModule.loadLibrary(); // Loads chat module at runtime
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => chatModule.ChatScreen()),
    );
  },
  child: Text("Go to Chat"),
)

How it works internally

  • Flutter compiles chat code into a separate chunk.
  • It is NOT loaded into memory until loadLibrary() is called.
  • Reduces startup code size by up to 30–50%, depending on module size.

Splitting a Heavy Feature (Chat Module)

Before (Monolithic Import)

import 'chat_screen.dart'; // loads everything at startup

After (Deferred Loading)

main.dart

import 'package:myapp/chat/chat_screen.dart' deferred as chat;

Button to load/chat

ElevatedButton(
  child: Text("Open Chat"),
  onPressed: () async {
    await chat.loadLibrary();  
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => chat.ChatScreen()),
    );
  },
)

Advantages

  • Faster cold start
  • Lower memory consumption
  • Large modules (AI, Chat, Maps, PDF viewer) stay unloaded until needed
  • Smaller RAM footprint on low-end devices

Lazy Loading Controllers, Loading Images & Assets

Instead of initializing controllers globally:

Bad

final ChatController chatController = ChatController();  // created at app launch

Good: Initialize when screen opens

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  late ChatController controller;

  @override
  void initState() {
    super.initState();
    controller = ChatController();  // created when screen loads
  }
}

Large images delay startup due to asset decode time.

Use:

  • FadeInImage.memoryNetwork
  • Image.asset("...", cacheWidth: ...)
  • precacheImage() only when needed

Lazy loading example:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (_, i) {
    return Image.network(
      items[i].url,
      loadingBuilder: (_, child, progress) =>
          progress == null ? child : CircularProgressIndicator(),
    );
  },
);

Performance Gains (Realistic Numbers)

FeatureBefore Lazy LoadingAfter Lazy Loading
Startup time1.8 sec0.9 sec
App binary in RAM200 MB120 MB
Initial Dart code load100%45%
Perceived performance⭐⭐⭐⭐⭐⭐⭐

Apps with Maps, Charts, AI, Lottie animations see massive gains.

Final Example: Clean Architecture + Lazy Loading Setup

main.dart

import 'package:flutter/material.dart';
import 'features/dashboard/dashboard.dart';
import 'features/chat/chat.dart' deferred as chat;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DashboardScreen(),
    );
  }
}

Dashboard

ElevatedButton(
  onPressed: () async {
    await chat.loadLibrary();
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => chat.ChatScreen(),
      ),
    );
  },
  child: Text("Open Chat (Lazy Load)"),
)

Chat module stays fully unloaded until user opens Chat.

Conclusion:

In the article, I have explained how Code Splitting & Lazy Loading in Flutter: Speeding Up App Startup Times. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

If your Flutter app has grown over time, chances are your startup time has slowed down. By applying code splitting, lazy loading, and deferred imports, you can:

  • Reduce initial load time
  • Lower memory usage
  • Load only what is necessary
  • Improve performance on low-end devices
  • Make your app feel instant and modern

These strategies are no longer optional—they are essential for production-ready Flutter apps in 2025 and beyond.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


End-to-End Encryption in Flutter: When, Why & How to Implement It

Data security has become one of the biggest priorities in modern app development. With cyberattacks, data breaches, and unauthorized surveillance increasing every year, users now expect apps to protect their data by default. For sensitive apps—like messaging platforms, fintech, healthcare, enterprise communication, or file-sharing apps—End-to-End Encryption (E2EE) is no longer optional.

Flutter developers often assume that HTTPS is enough for security. But HTTPS only protects data in transit between the device and server. It does not protect data stored on backend servers or prevent internal leaks.

In an era of relentless data breaches and heightened privacy concerns, securing digital communication isn’t just a feature—it’s a necessity. End-to-End Encryption (E2EE) has emerged as the gold standard for protecting user data in transit and at rest. If you are developing an application with Flutter that handles sensitive information, implementing E2EE is a critical step towards building user trust and ensuring compliance with global privacy regulations.

This extensive guide dives deep into the fundamentals of E2EE, clarifies the scenarios where it is mandatory, explains the underlying cryptographic principles, and provides practical, code-based examples using powerful Flutter packages.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

What is End-to-End Encryption (E2EE)?

When Should You Implement End-to-End Encryption in Flutter?

Why Implement E2EE? The Core Benefits

How End-to-End Encryption Works in Flutter

Choosing Your Encryption Method

Implementation: End-to-End Encryption in Flutter (Send + Receive)

Key Management & Architectural Flow

Challenges and Considerations in E2EE

Conclusion



What is End-to-End Encryption (E2EE)?

End-to-End Encryption is a system of communication where only the communicating users can read the messages. In principle, it prevents potential eavesdroppers—including telecom providers, internet service providers, and even the application service provider itself—from accessing the cryptographic keys needed to decipher the conversation.

End-to-End Encryption ensures that only the sender and the intended recipient can read the data.
No one else — not hackers, server admins, ISPs, governments, or even your own backend — can decrypt the content.

In an E2EE system:

  1. The sender encrypts data locally using the recipient’s public key.
  2. The encrypted data travels through the backend.
  3. The recipient decrypts using their private key.

The backend server only passes encrypted payloads. It cannot decrypt or modify them. This is the same security model used by:

  • WhatsApp
  • Signal
  • Telegram (Secret Chat)
  • iCloud Keychain
  • Microsoft Teams
  • Zoom E2EE meetings

When Should You Implement End-to-End Encryption in Flutter?

Deciding whether to implement E2EE involves weighing the significant security benefits against the increased complexity of key management and infrastructure. E2EE is recommended for apps dealing with any sensitive user data. You should commit to E2EE if your application falls into any of the following categories:

  • Messaging and VoIP Applications: Any platform facilitating private, personal conversations requires E2EE to guarantee user privacy. Users expect their chats to remain confidential.
  • Healthcare Applications (HIPAA Compliance): Handling Protected Health Information (PHI) demands the highest level of security. E2EE is often a regulatory necessity to comply with laws like the Health Insurance Portability and Accountability Act (HIPAA) in the US or GDPR in Europe.
  • Financial & Fintech Apps: Although standard financial transactions use robust TLS, E2EE becomes vital when users exchange sensitive documents, statements, or payment details within a chat interface or store private financial records in an in-app vault.
  • Confidential Enterprise Communication: When building internal collaboration tools for businesses, E2EE ensures that trade secrets, proprietary information, and sensitive HR discussions remain private from network administrators or third-party server providers.
  • Secure File Storage & Cloud Services: If you are building a secure cloud storage solution, E2EE ensures that you, the provider, cannot access the files your users store, offering a “zero-knowledge” privacy model.

Why Implement E2EE? The Core Benefits

The advantages of implementing E2EE extend beyond simple data protection:

1. Guarantees Data Confidentiality and Integrity:- E2EE ensures that only intended recipients can read the message. Furthermore, robust E2EE protocols include authentication tags (MACs – Message Authentication Codes) that instantly detect if a message has been tampered with in transit.

2. Protection Against Server Breaches:- This is a critical advantage. If your application’s backend servers are compromised by a malicious actor, the stored user data (messages, files) will be useless to the hacker, as they will only have access to encrypted ciphertext, not the decryption keys.

3. Builds Profound User Trust:- In a competitive market, advertising genuine “zero-knowledge” or E2EE security is a powerful differentiator. It demonstrates a commitment to user privacy that fosters loyalty and trust.

4. Meets Regulatory & Legal Requirements:- Many data protection regulations require “appropriate technical and organizational measures” to protect user data. E2EE often satisfies these stringent requirements and can reduce legal liability in the event of a data breach.

How End-to-End Encryption Works in Flutter

E2EE relies on a combination of asymmetric (public-key) and symmetric encryption algorithms. A high-level overview of the mechanism involves four main steps:

The E2EE Workflow Explained:

  • Encryption & Decryption (Symmetric): The shared secret key is used to rapidly encrypt and decrypt all subsequent communication using a fast symmetric cipher (like AES-GCM or AES-CTR).
  • Key Pair Generation (Asymmetric): Each user generates a unique cryptographic key pair: a private key (kept secret and stored securely on their device) and a public key (shared openly with anyone).
  • Key Exchange (The “Handshake”): Users exchange their public keys via the application server.
  • Shared Secret Derivation (Diffie-Hellman): Using clever mathematics (specifically an Elliptic Curve Diffie-Hellman, or ECDH, key exchange), both users combine their own private key with the other user’s public key to independently calculate an identical, shared symmetric secret key. Crucially, this shared secret is never transmitted over the network.

Here’s the typical encryption flow:

User A (Sender)
      ↓
Generate/Use Public Key of User B
      ↓
Encrypt message locally using AES/RSA
      ↓
Send encrypted text to server
      ↓
Server stores only ciphertext
      ↓
User B downloads ciphertext
      ↓
Decrypts using Private Key

Choosing Your Encryption Method

Flutter supports multiple E2EE approaches:

MethodUse Case
RSA (Asymmetric)Messaging, key exchange
AES (Symmetric)Encrypting large messages/files
Elliptic Curve Cryptography (ECC)Modern secure messaging apps
X25519 + AES-GCMSignal, WhatsApp, iMessage

Implementation: End-to-End Encryption in Flutter (Send + Receive)

We will build a minimal E2EE pipeline:

  • Generate RSA keys for two users
  • Encrypt a message using the recipient’s public key
  • Decrypt using their private key
  • Show encrypted and decrypted results

Dependencies (pubspec.yaml):

dependencies:
encrypt: ^latest_version
pointycastle: ^latest_version

Step 1: Generate RSA Key Pair

import 'package:pointycastle/export.dart';
import 'dart:convert';

Future<AsymmetricKeyPair<PublicKey, PrivateKey>> generateRSAKeyPair() async {
  final keyGen = RSAKeyGenerator()
    ..init(ParametersWithRandom(
      RSAKeyGeneratorParameters(BigInt.from(65537), 2048, 12),
      SecureRandom()));
  return keyGen.generateKeyPair();
}

Usage:

final keyPair = await generateRSAKeyPair();
final publicKey = keyPair.publicKey;
final privateKey = keyPair.privateKey;

Step 2: Convert RSA Keys to PEM (for storage)

String encodePublicKeyToPem(RSAPublicKey publicKey) {
final algorithmSeq = ASN1Sequence();
final algorithmAsn1Obj = ASN1Sequence();
algorithmAsn1Obj.add(ASN1ObjectIdentifier.fromName("rsaEncryption"));
algorithmAsn1Obj.add(ASN1Null());
algorithmSeq.add(algorithmAsn1Obj);

final publicKeySeq = ASN1Sequence();
publicKeySeq.add(ASN1Integer(publicKey.modulus!));
publicKeySeq.add(ASN1Integer(publicKey.exponent!));

final publicKeyBitString = ASN1BitString(publicKeySeq.encode());
algorithmSeq.add(publicKeyBitString);

return base64.encode(algorithmSeq.encode());
}

A private key encoder can also be added similarly.

Step 3: Encrypt a Message with Public Key

import 'package:encrypt/encrypt.dart';

String encryptMessage(String plainText, RSAPublicKey publicKey) {
  final encrypter = Encrypter(RSA(publicKey: publicKey));
  return encrypter.encrypt(plainText).base64;
}

Usage:

final encrypted = encryptMessage("Hello from User A!", publicKey);
print("Encrypted: $encrypted");

Step 4: Decrypt Message with Private Key

String decryptMessage(String encrypted, RSAPrivateKey privateKey) {
  final encrypter = Encrypter(RSA(privateKey: privateKey));
  return encrypter.decrypt64(encrypted);
}

Usage:

final decrypted = decryptMessage(encrypted, privateKey);
print("Decrypted: $decrypted");

Main. dart file

void main() async {
// Generate RSA keys for User B (Receiver)
final pair = await generateRSAKeyPair();

final publicKey = pair.publicKey as RSAPublicKey;
final privateKey = pair.privateKey as RSAPrivateKey;

// User A sends encrypted message to User B
final encrypted = encryptMessage("Hello User B, this is secure!", publicKey);

print("Encrypted Message:");
print(encrypted);

// User B decrypts the message
final decrypted = decryptMessage(encrypted, privateKey);

print("\nDecrypted Message:");
print(decrypted);
}

Output:

When we run the application, we ought to get the screen’s output like the console terminal.

Encrypted Message:
MIIBIjANBgkqhkiG…

Decrypted Message:
Hello User B, this is secure!

Key Management & Architectural Flow

For your blog post, emphasize how this fits into a full application architecture:

  • App Startup: On the first launch, call e2eeService.initializeUserKeys(). The resulting public key is sent to your backend server and stored in the user profile database.
  • Starting a Chat: When User A wants to message User B, User A’s app requests User B’s public key from the backend server.
  • Deriving Secret: User A’s app calls e2eeService.deriveSharedSecret(UserB_PublicKey). This shared secret is stored in memory for the session, or perhaps cached securely locally for ongoing chats.
  • Sending Message: Messages are encrypted using e2eeService.encryptMessage() and the resulting data dictionary (content, nonce, mac) is sent to the server via API.
  • Receiving Message: The server delivers the data dictionary to User B. User B’s app decrypts it using e2eeService.decryptMessage() and their locally derived identical shared secret.

Challenges and Considerations in E2EE

While powerful, E2EE introduces complexity that developers must manage:

  • Key Management is Hard: The biggest challenge is lifecycle management: what happens if a user loses their phone? They lose their private key and all message history if no robust backup system is implemented (e.g., encrypted cloud backups protected by a user-set password).
  • Lack of Server Visibility: Your backend can no longer search message content, filter for inappropriate language, or easily moderate chats, as all data is opaque to the server. Your moderation must move client-side or rely on user reporting mechanisms.
  • Multi-Device Synchronization: Managing keys across multiple devices (phone, web app, tablet) securely without compromising the E2EE principle is a significant architectural challenge that requires sophisticated identity and key federation protocols.

Conclusion:

In the article, I have explained how the End-to-End Encryption in Flutter: When, Why & How to Implement It. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

End-to-End Encryption (E2EE) is becoming the new standard for secure mobile communication, especially in Flutter applications handling sensitive data. Implementing E2EE ensures that only the sender and recipient can read messages—even the server cannot decrypt them.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Top 20 Security Mistakes Flutter Developers Still Make — and How to Fix Them

The mobile app development landscape has been revolutionized by frameworks like Flutter, offering beautiful, natively compiled applications from a single codebase. This speed and efficiency, however, often come at a cost: security oversights. Many developers, focused purely on functionality and UI, overlook fundamental security principles, leaving their applications vulnerable to attackers.

Security is not a feature; it’s a foundational requirement. A single vulnerability can lead to data breaches, reputational damage, and significant financial loss.

In this deep dive, we will explore the top 20 security mistakes Flutter developers continue to make and provide comprehensive, actionable fixes to secure your applications from the ground up.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Introduction

Hardcoding Secrets and API Keys

Not enabling ProGuard or R8 obfuscation

Insecure Local Data Storage

Insufficient Network Communication Security (Using HTTP)

Neglecting Certificate/SSL Pinning

Insufficient Input Validation

Exposing Debug Information in Production

Failure to Obfuscate Code

Ignoring Platform-Specific Security Controls

Not Implementing Runtime Protections (Jailbreak/Root Detection)

Weak or Incomplete Authentication Flows

Over-reliance on Third-Party Packages Without Auditing

Improper Error Handling

Bypassing Client-Side Validation

Ignoring Null Safety Warnings

Neglecting Regular Security Audits

Storing Encryption Keys Insecurely

Insecure Clipboard Usage

Not handling secure logout correctly

Using Insecure Cryptographic Algorithms

Conclusion



Introduction:

Flutter has become one of the most loved frameworks for building cross-platform apps — but as its adoption grows, so do the security risks. Even experienced developers unknowingly ship apps with vulnerabilities that expose user data, API keys, or entire backend systems.

Security is not just a backend responsibility anymore. Modern mobile apps handle sensitive logic on the client side too — authentication, payments, offline data, encrypted storage, business rules, and more. A single mistake in your Flutter code can open the door to attacks like reverse engineering, API abuse, data theft, token hijacking, and MITM (Man-In-The-Middle) attacks.

Hardcoding Secrets and API Keys:

The Mistake Explained: This is arguably the most common and dangerous oversight. Developers often drop API keys for services like Google Maps, AWS, or backend API URLs directly into their lib/main.dart file or similar files. While the Dart code is compiled into native binaries (ARM instructions), attackers use standard reverse-engineering tools like jtool or Ghidra to easily extract constant strings from the binary. This oversight instantly compromises your backend services or billing accounts.

Bad Practice (Vulnerable Code):

dart

// main.dart
const String kApiKey = "SG_ak_live_***********";
const String kApiBaseUrl = "api.myunsecuredsite.com"; // Also using HTTP!

How to Fix It (Good Practice):

Secrets should never be committed to source control. Use environment variables that are injected at build time, or better yet, manage them on a secure backend server and fetch them only when necessary.

For build-time configuration, the flutter_dotenv package offers a clean way to manage development variables, provided you add your .env file to .gitignore. For production builds, integrate with secure CI/CD pipelines to inject these variables directly into the build command line arguments or use specialized build configurations (like flutter build --dart-define).

Using flutter_dotenv securely:

bash

# In your terminal
flutter pub add flutter_dotenv

dart

// main.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future main() async {
  // Load the .env file (ensure it is in .gitignore)
  await dotenv.load(fileName: ".env"); 
  runApp(const MyApp());
}

// Accessing the key:
final apiKey = dotenv.env['API_KEY_PROD']; 

Not enabling ProGuard or R8 obfuscation:

Flutter’s native Android build exposes method/variable names in the compiled app. Attackers can reverse-engineer logic easily.

Fix:
Enable obfuscation:

flutter build apk --obfuscate --split-debug-info=build/debug_info

Add this to proguard-rules.pro:

-keep class io.flutter.** { *; }

This makes reverse-engineering significantly harder.

Insecure Local Data Storage:

The Mistake Explained: Storing sensitive user data—authentication tokens, session cookies, passwords, or personal health information—in plain text on the device is a major vulnerability. Using simple storage solutions like SharedPreferences (Android) or NSUserDefaults (iOS) does not provide encryption. Any user with physical access to the device (or another app with access to the storage sandbox) can read this data.

Bad Practice (Vulnerable Code):

dart

// Using shared_preferences for sensitive token
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('user_auth_token', sensitiveToken); // Stored in plaintext

How to Fix It (Good Practice):

You must use platform-specific, hardware-backed secure storage mechanisms. The flutter_secure_storage package is the standard solution. It intelligently uses the Android Keystore system and the iOS Keychain under the hood, ensuring data is encrypted at rest and tied securely to the device and user context.

dart

// Using flutter_secure_storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = const FlutterSecureStorage();

// Write the token securely
await storage.write(key: 'user_auth_token', value: sensitiveToken);

// Read the token securely
String? token = await storage.read(key: 'user_auth_token');

 Insufficient Network Communication Security (Using HTTP):

The Mistake Explained: This foundational internet security rule still needs repeating. Communicating with your backend services over unencrypted HTTP channels opens your application to “Man-in-the-Middle” (MITM) attacks. An attacker on the same Wi-Fi network (e.g., in a coffee shop) can intercept all traffic, read sensitive data, and even modify responses sent back to your app.

How to Fix It (Good Practice):

Always enforce HTTPS/TLS for all network communications. The Dart http client automatically handles HTTPS connections. Furthermore, ensure you configure platform-specific constraints:

  • Android: Target API 28+, where cleartext HTTP traffic is disabled by default. If you must use HTTP temporarily (like a local dev server), use a network_security_config.xml to limit exceptions safely.
  • iOS: Leverage App Transport Security (ATS), which enforces secure connections by default.

Neglecting Certificate/SSL Pinning:

The Mistake Explained: HTTPS is good, but it relies on a chain of trust validated by various Certificate Authorities (CAs). A sophisticated attacker might compromise a CA or issue a fraudulent certificate that your app would inherently trust. Certificate pinning (also known as public key pinning) is a defense-in-depth measure against these specific, targeted attacks.

How to Fix It (Good Practice):

Implement certificate pinning to restrict the app to only accepting specific, pre-defined server certificates or public keys. You can configure this natively (more robustly) or use a package designed to manage this within Flutter’s network stack, such as the dio package with an added interceptor or specialized security packages.

  • dio_certificate_pin
  • ssl_pinning_plugin

This ensures your app talks only to trusted servers.

Insufficient Input Validation:

The Mistake Explained: The cardinal rule of secure coding: Never trust user input. Many developers fail to validate input on both the client and server sides. Client-side validation is often skipped entirely or is easily bypassed, leading to critical vulnerabilities such as SQL injection, Cross-Site Scripting (XSS), or buffer overflows when the data reaches the backend.

How to Fix It (Good Practice):

Implement robust, comprehensive input validation:

  1. Client-side: Use packages like form_field_validator for immediate user feedback (UX benefit).
  2. Server-side (Mandatory): Always re-validate and sanitize all input on the backend server before processing or storing it in a database. This is your primary security control.

Exposing Debug Information in Production:

The Mistake Explained: Leaving verbose logging (e.g., print()debugPrint() statements, configuration flags, or test code in a production build is dangerous. These logs can appear in system logs visible to other apps or provide attackers with internal app logic, variable names, and potentially leaked data during an attack.

How to Fix It (Good Practice):

Strip all debug logs before releasing to production.

dart

// Check if the app is in debug mode before logging sensitive info
import 'package:flutter/foundation.dart';

void logSensitiveData(String data) {
  if (kDebugMode) {
    print("Sensitive Data: $data");
  }
}

Use a dedicated logging framework (like logging or logger) configured to log minimally in production and capture detailed logs only using secure monitoring tools like Firebase Crashlytics.

Failure to Obfuscate Code:

The Mistake Explained: By default, a release-mode Flutter application is relatively code with caution.ly easy to reverse-engineer back into readable Dart code using standard decompilers. This lack of obfuscation makes it simple for attackers to understand your app’s business logic, find vulnerabilities, and potentially tamper with your app’s behavior (e.g., creating cracked versions of paid features).

How to Fix It (Good Practice):

Enable code obfuscation for all release builds. This process renames classes, functions, and variables to meaningless characters (e.g., abc), making the resulting binary extremely difficult for humans to understand.

Run the build command with these flags:

bash

flutter build apk --obfuscate --split-debug-info=/<project-name>/<app-name>/symbols

Remember to safely store the generated symbol maps so you can still debug crash reports.

Ignoring Platform-Specific Security Controls:

The Mistake Explained: Focusing solely on the Dart layer often leads developers to ignore underlying OS security features. Examples include failing to use platform-specific secure storage or requesting overly broad permissions without justification.

How to Fix It (Good Practice):

  • Permissions: Request only the absolute minimum required permissions and clearly explain why to the user in context when the permission is needed.
  • Background Protection: Use packages like secure_application to obscure sensitive screens in the task switcher (recents screen snapshot on Android/iOS) to prevent shoulder-surfing or data exposure when the app is backgrounded.

Not Implementing Runtime Protections (Jailbreak/Root Detection):

The Mistake Explained: A “rooted” Android device or “jailbroken” iOS device has had its core OS security features fundamentally bypassed. Running your highly secure app on such a compromised platform exposes it to risks that standard OS protections usually mitigate.

How to Fix It (Good Practice):

Use packages like flutter_jailbreak_detection to detect the state of the device at runtime. While you shouldn’t necessarily block all users of rooted devices, you can limit access to highly sensitive functionality (e.g., banking features) or warn the user that their environment is insecure.

dart

bool jailbroken = await FlutterJailbreakDetection.isJailBroken;
if (jailbroken) {
  // Show warning or disable sensitive features
}

Weak or Incomplete Authentication Flows:

The Mistake Explained: Building a custom authentication system from scratch is complex and error-prone. Custom implementations frequently miss edge cases like robust password hashing, brute-force protection, account lockout mechanisms, or secure token generation/invalidation.

How to Fix It (Good Practice):

Leverage established, battle-tested authentication frameworks. Use Firebase Authentication or industry-standard OAuth 2.0 and OpenID Connect protocols. Use short-lived, token-based authentication (e.g., JWTs) managed by secure backend services. Ensure refresh tokens are stored securely using flutter_secure_storage (See Mistake #2).

Over-reliance on Third-Party Packages Without Auditing:

The Mistake Explained: The pub.dev ecosystem is rich, but not all packages are created equal. Adding excessive, outdated, or poorly maintained third-party packages introduces external security vulnerabilities and version conflicts. A package with an unpatched vulnerability becomes a vulnerability in your app.

How to Fix It (Good Practice):

Regularly audit your dependencies. Run dart pub outdated frequently. Use security scanners like Snyk or GitHub’s Dependabot in your repository to check for known vulnerabilities in your dependencies. Prefer packages that are actively maintained by recognized publishers (like firebase_* or fluttercommunity_*).

Improper Error Handling:

The Mistake Explained: Providing overly descriptive error messages that reveal backend implementation details is a gift to an attacker. Messages like “SQL syntax error near line 4, database /var/data/…” expose information about your database type, schema, and server file paths, helping an attacker tailor their exploit.

How to Fix It (Good Practice):

Handle errors gracefully in the UI with generic, user-friendly messages (“An error occurred. Please try again.”). Ensure detailed technical logs are only captured on the backend or using secure monitoring tools (like Firebase Crashlytics), redacting any sensitive information before transmission.

Bypassing Client-Side Validation:

The Mistake Explained: This isn’t a security mistake in itself, but a defense-in-depth mistake. Some teams skip client-side validation entirely because they know server-side validation is the real security control. This degrades user experience (users have to wait for a network round trip to see they missed a field) and misses a layer of validation that might block trivial attempts to break your forms.

How to Fix It (Good Practice):

Use client-side validation for immediate feedback and a better user experience. Just understand that it offers no security guarantees and must be duplicated on the server.

Ignoring Null Safety Warnings:

The Mistake Explained: While Dart’s null safety is a robustness feature, not explicitly a security one, ignoring warnings or using the !operators excessively to force null values can lead to unexpected runtime errors and crashes. Unpredictable app behavior can sometimes open subtle avenues for exploitation if error states are not handled gracefully.

How to Fix It (Good Practice):

Embrace Dart’s sound null safety fully. Trust the type system. Use optionals correctly and ensure your code is stable and predictable, leading to a more robust and secure application overall.

Neglecting Regular Security Audits:

The Mistake Explained: Treating security as a one-time configuration step during the initial setup phase is a major mistake. The threat landscape changes constantly, new vulnerabilities are discovered in packages, and new attack methods emerge.

How to Fix It (Good Practice):

Integrate security into your Software Development Lifecycle (SDLC). Conduct regular static code analysis (using dart analyze with strict rulesets. Integrate security scans into your CI/CD pipeline (using tools like Snyk or SonarQube). Consider engaging in third-party penetration testing annually.

Storing Encryption Keys Insecurely:

The Mistake Explained: The final mistake brings us back to secure storage. If you use a package like Hive or a local database that requires a custom encryption key, storing that key in plain text defeats the purpose of the encryption.

How to Fix It (Good Practice):

The master encryption key for any local database or encrypted file must itself be stored in a hardware-backed keystore. Re-use the flutter_secure_storage package (see Mistake #2) to manage and retrieve this primary encryption key securely.

 Insecure Clipboard Usage:

The Mistake Explained: Automatically copying sensitive data (like one-time passwords/OTPs, recovery codes, or wallet keys) to the clipboard might seem convenient. However, the clipboard history might linger, or other malicious apps running on the device might be able to read the clipboard contents without explicit permission.

How to Fix It (Good Practice):

Avoid automatically using the clipboard. If a user needs to copy sensitive information, make it an explicit user action via a dedicated “Copy” button. For extreme security needs, consider clearing the clipboard shortly after a copy action is performed.

Not handling secure logout correctly:

Common mistakes:

  • Not clearing tokens
  • Not invalidating refresh tokens
  • Keeping the user session alive way too long

Fix:
On logout:

  • Delete secure storage
  • Invalidate tokens on the server
  • Redirect to the login screen safely

Using Insecure Cryptographic Algorithms:

The Mistake Explained: Relying on outdated or known weak cryptographic algorithms (e.g., MD5, SHA1, RC2) for encryption and hashing is a significant risk. These algorithms have known vulnerabilities and can be brute-forced or collided with relatively easily with modern computing power.

How to Fix It (Good Practice):

Use modern, strong algorithms. For hashing passwords, use robust, slow-hashing algorithms like Argon2 (managed via your backend). For general data encryption in the app, use AES-256. Use established packages like encrypt or crypto that provide modern implementations.

Conclusion:

In the article, I have explained how the Top 20 Security Mistakes Flutter Developers Still Make. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

Improving scroBuilding secure Flutter applications requires a shift in mindset. The cross-platform efficiency of Flutter shouldn’t come at the cost of robust security practices. By systematically addressing these 20 common mistakes—from secure storage and network protocols to continuous auditing and obfuscation—you can build applications that protect user data and maintain trust.

Security is an ongoing commitment. Stay vigilant, audit regularly, and build defensively. Security is not a one-time task — it’s a continuous process. Flutter apps are powerful but also exposed to reverse engineering, API abuse, and data leaks if developers ignore basic protection practices.

By avoiding these Top 20 security mistakes, you can ship secure, enterprise-ready Flutter apps that protect both your users and your business.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Improving Scrolling Performance in Flutter: ListView, Slivers & Viewport Optimization

Smooth, responsive scrolling is a hallmark of a high-quality mobile app. Whether it’s a social feed, an e-commerce product list, or a messaging interface, users instantly notice when scrolling lags, janks, or drops frames. With Flutter powering experiences across mobile, web, and desktop, mastering scroll performance has become essential for building production-ready apps.

Scrolling is the backbone of most modern mobile applications. When a user scrolls through a feed, a list of products, or their inbox, they expect a buttery-smooth 60 frames per second (fps) experience. Laggy, janky scrolling is a surefire way to frustrate users. Fortunately, Flutter provides powerful tools to manage performance, from simple best practices to advanced rendering control using Slivers.

We’ll explore why scrolling sometimes becomes slow, the internals of ListView and Slivers, viewport optimizations, re-compositions, rendering bottlenecks, and best practices for achieving buttery-smooth scrolling in Flutter apps.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Why Scrolling Performance Matters

Understanding Flutter’s Rendering Pipeline

Why Lists Lag: Common Bottlenecks

Optimizing ListView (The Workhorse)

Mastering Slivers (For Complex Layouts)

Advanced Techniques & Viewport Optimization

Debugging Jank with DevTools

Measuring Performance with DevTools

Conclusion



Why Scrolling Performance Matters:

Scrolling is one of the most frequent interactions in any mobile app. Users expect:

  • High frame rate (60fps+ on mobile)
  • Zero jank
  • Fast list loading
  • Smooth physics & momentum
  • Low input latency

If scrolling is slow, users immediately feel the app is poorly built, even if the backend and features are perfect. In Flutter, achieving high-performance scrolling is possible — but requires a solid understanding of:

  • Widget rebuilding
  • State lifecycle
  • Render tree
  • Viewport mechanics
  • Lazy loading

Understanding Flutter’s Rendering Pipeline:

Flutter’s rendering pipeline consists of:

  1. Build phase – widget tree
  2. Layout phase – constraints, size
  3. Paint phase – drawing
  4. Compositing – layering
  5. Rasterization – GPU rendering

Scrolling performance suffers when:

  • Build phase is too heavy
  • Layout calculations are expensive
  • Too many widgets paint off-screen
  • State changes trigger unnecessary rebuilds
  • List items are not recycling efficiently

To optimize scroll performance, we must minimize work during scroll.

Why Lists Lag: Common Bottlenecks

Let’s break down typical causes of scroll jank in Flutter:

1. Too many widgets building at once:- ListView builds only visible items, but expensive widgets still cause frame drops.

2. Complex layouts inside list items

Nested Row → Column → Stack → Positioned → custom painters.

3. Large image loading:- Downloading, decoding & painting images during scroll.

4. Using ListView with heavy states:- If items depend on setState() frequently → rebuild storms.

5. Sliver constraints mismanagement:- For example, putting a ListView inside another scrollable.

6. Frequent rebuilds due to bad architecture:- StatefulWidget misuse or rebuilding entire lists instead of items.

Understanding these causes helps us fix them.

Optimizing ListView (The Workhorse):

The standard ListView is the most common scrolling widget. When used incorrectly, it’s often the source of performance bottlenecks.

The Pitfall of Standard ListView

A basic ListView(children: [...]) builds every single child immediately, regardless of whether it’s visible on the screen. This is fine for 10 items, but disastrous for 1,000.

The Solution: ListView.builder (Lazy Loading)

The builder constructor is your best friend. It implements lazy loading, building widgets only as they scroll into the viewport.

dart

ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

Pro-Tip: itemExtent for Max Speed

If all your list items have the exact same height, you can achieve peak performance by setting the itemExtent property.

dart

ListView.builder(
  itemCount: 1000,
  itemExtent: 60.0, // <-- Instantly improves performance
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

By providing a fixed extent, Flutter’s scrolling engine no longer needs to measure every child widget individually. It just calculates how many can fit on the screen—pure efficiency.

Avoid shrinkWrap: true

shrinkWrap: true is convenient, but performance-intensive. It forces the list to calculate the exact extent of all its children upfront, destroying the benefits of lazy loading. Avoid it in long lists.

Mastering Slivers (For Complex Layouts):

Slivers are lower-level components that give you fine-grained control over the viewport (the visible scrolling area). They are essential for advanced scrolling effects and mixing different types of content in a single scroll view.

You use slivers inside a CustomScrollView.

The Slivers You Need

  • CustomScrollView: The container that manages all the slivers.
  • SliverAppBar: Creates sticky or collapsing headers.
  • SliverList / SliverGrid: The sliver equivalents of ListView.builder and GridView.builder.

dart

CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(
      pinned: true, // Sticks the header at the top
      flexibleSpace: Placeholder(),
      expandedHeight: 200.0,
    ),
    SliverList( // Lazily builds this list
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return ListTile(title: Text('Item #$index'));
        },
        childCount: 1000,
      ),
    ),
  ],
)

Using Slivers ensures that all elements coordinate with the single shared viewport, making complex UIs smooth and efficient.

Advanced Techniques & Viewport Optimization:

Take control over how data is managed in memory.

Use AutomaticKeepAliveClientMixin

If you have complex, stateful list items (e.g., items with video players, form inputs, or complex animations) that you don’t want to rebuild every time they scroll off-screen, use this mixin.

dart

class ExpensiveListItem extends StatefulWidget {
  const ExpensiveListItem({Key? key}) : super(key: key);
  @override
  _ExpensiveListItemState createState() => _ExpensiveListItemState();
}

class _ExpensiveListItemState extends State<ExpensiveListItem>
    with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true; // Keeps the state alive

  @override
  Widget build(BuildContext context) {
    super.build(context); // Required for the mixin to work
    // Build your expensive widget here
    return const Placeholder(fallbackHeight: 250);
  }
}

Isolate Repaints with RepaintBoundary

While ListView items are automatically wrapped in a RepaintBoundary by default, manual use is powerful for other complex scenarios.

Wrap a frequently animating or complex, heavy widget (like a chart or map) in a RepaintBoundary to prevent its animation from forcing the entire screen to repaint.

dart

// The rest of the UI is static, but this one part animates frequently
RepaintBoundary(
  child: MyComplexAnimationWidget(),
)

Image Optimization and Pagination

  • Image Caching: Use packages like cached_network_image to store images in memory and on disk. This prevents redundant network requests and disk reads during scrolling.
  • Pagination: Don’t load 1,000 items at once. Implement pagination to fetch data in chunks as the user scrolls toward the end of the list.

The cacheExtent Property

The cacheExtent determines how many pixels outside the visible viewport Flutter should pre-render items for.

The default is typically 250.0 pixels. If you have particularly complex items that take time to build, increasing the cacheExtent can reduce jank when a user flicks the list quickly.

dart

ListView.builder(
  cacheExtent: 1000.0, // Pre-render more items off-screen
  // ... other properties
)

Debugging Jank with DevTools:

The most important step is identifying where your performance issues lie using the Flutter DevTools.

  1. Run your app in profile mode: flutter run --profile
  2. Open DevTools (usually available via your IDE or terminal link).
  3. Go to the Performance tab.
  4. Enable the Performance Overlay to see real-time UI and GPU frame timings directly on your app screen.
  5. Use the Widget Inspector to toggle “Highlight Repaints” (the paint rainbow button). This visualizes exactly which widgets are being repainted every frame, helping you pinpoint unnecessary work.

Measuring Performance with DevTools:

Use Flutter DevTools:

  1. Performance Overlay:- Shows frame rendering performance.
  2. CPU Profiler:- Identify heavy operations.
  3. Rebuild Tracker:- Detect unnecessary rebuilds.
  4. Raster Stats:- Catch expensive images or paints.

Conclusion:

In the article, I have explained how the Improving Scrolling Performance in Flutter: ListView, Slivers & Viewport Optimization. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

Improving scrolling performance is a journey from basic efficiency to advanced rendering control. To wrap up, remember this checklist for buttery-smooth results:

  • Always use ListView.builder for long or dynamic lists.
  • Specify itemExtent if your items have a fixed height.
  • Utilize Slivers for complex layouts and advanced scrolling effects (like sticky headers).
  • Cache network images using libraries like cached_network_image.
  • Profile using DevTools (flutter run --profile) to find bottlenecks.

Implement these strategies, and your users will thank you for a responsive and delightful experience.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


How to Build Production-Ready AI Recommendation Systems in Flutter Apps

Artificial Intelligence has shifted from being a futuristic concept to a practical engine powering modern digital products. One of the most impactful applications of AI today is recommendation systems—the hidden force behind personalized feeds, trending content, suggested products, and smart predictions.

While industries like e-commerce, streaming, and social platforms have long used recommendation engines, Flutter developers today can integrate the same intelligence into mobile and web apps with relative ease.

In this article, you’ll learn how to build a production-ready AI recommendation system in Flutter, combining machine learning models, backend infrastructure, cloud AI services, and scalable architecture.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Why Recommendation Systems Matter in Flutter Apps

Understanding Recommendation System Types

Overall Architecture of a Production AI Recommendation System

Step-by-Step Guide: Building a Production-Ready Recommender for Flutter

Real-World Architecture

Security & Privacy Considerations

Conclusion



Why Recommendation Systems Matter in Flutter Apps:

In modern apps, personalization is no longer optional. Users expect:

  • Relevant products
  • Smart content feeds
  • Tailored search results
  • Predictions based on past behavior

Without recommendations, your app looks static and generic. With recommendations, your app feels intelligent, adaptive, and user-centric.

For Flutter developers, AI recommendations can elevate:

  • E-commerce apps → Personalized product feeds
  • EdTech apps → Suggesting courses/lessons
  • Content apps → Trending videos/articles
  • Fitness apps → Personalized workouts
  • FinTech apps → Tailored investment suggestions
  • Social apps → Friend/activity recommendations

Flutter’s flexible UI and cross-platform nature make it ideal for deploying ML-powered experiences across mobile, web, and desktop platforms.

Understanding Recommendation System Types:

Before building one, you need to choose the right type.

=> Content-Based Filtering:- Uses item metadata (tags, descriptions, categories) to recommend similar items.

Example:– If a user watches a lot of horror movies, the system suggests more horror content.

=> Collaborative Filtering:- Uses user behaviour (ratings, clicks, purchases) to find patterns.

-> Types:

  • User-User CF — users with similar tastes
  • Item-Item CF — items consumed together

Example:- People who bought this also bought…

=> Hybrid Models (Most Production-Ready):- Combines content and collaborative filtering. Hybrid systems perform best because they use multiple input signals.

Used in: Netflix, Amazon, YouTube, Spotify

=> Deep Learning-Based Recommenders:- Modern recommenders use neural networks:

  • Embedding models
  • Transformers
  • Seq2Seq recommendation models
  • Reinforcement learning (RL) for personalization

These outperform traditional methods for large datasets.

Overall Architecture of a Production AI Recommendation System

A scalable AI-powered recommendation system has four major components:

=> Data Collection Layer

You must track:

  1. User clicks
  2. Product views
  3. Searches
  4. Watch time
  5. Add-to-cart
  6. Ratings
  7. User metadata (age, preferences, etc.)

Tools used:

  1. Firebase Analytics
  2. Segment
  3. Mixpanel
  4. Custom event tracking API

=> Machine Learning Model Training:- This is usually done server-side using:

  1. Python
  2. TensorFlow / PyTorch
  3. Scikit-learn
  4. HuggingFace
  5. Vertex AI / AWS Sagemaker / Azure AI

Models may include:

  1. Embedding-based recommenders
  2. Content vector similarity models
  3. Matrix factorization (ALS)
  4. Ranking models like XGBoost
  5. Deep neural recommenders

=> Inference API:- Your Flutter app must talk to a backend that delivers recommendations.

Common setups:

  1. FastAPI (Python)
  2. Node.js + TensorFlow.js
  3. Firebase Cloud Functions calling Vertex AI
  4. Supabase Edge Functions for lightweight scripts
  5. Serverless AWS Lambda

The backend should:

  1. Accept user ID/session
  2. Fetch user signals
  3. Run prediction
  4. Return top results

=> Flutter Integration Layer:- Your Flutter app will:

  1. Call backend endpoint
  2. Display card carousel/grid
  3. Cache responses locally
  4. Update recommendations in real time

Flutter plugins used:

  • dio for API calls
  • hive / shared_preferences for caching
  • Firebase for analytics + events
  • Riverpod / Provider / Bloc for state management

Step-by-Step Guide: Building a Production-Ready Recommender for Flutter

Let’s walk through the entire implementation process.

Step 1: Collect User Interaction Data in Flutter

Start tracking:

Example: Track product view event

FirebaseAnalytics.instance.logEvent(
  name: 'product_view',
  parameters: {
    'product_id': product.id,
    'category': product.category,
  },
);

Track everything needed for personalisation.

Step 2: Prepare Your Dataset for Training

For each user, build a dataset:

user_iditem_idactionscore
10122view0.2
10122click0.6
10122buy1.0
10223view0.2

Actions → Scores
view (0.2), click (0.6), add-to-cart (0.8), purchase (1.0)

The richer your dataset → the better your model.

Step 3: Train a Recommendation Model (Python Example)

Here’s a simplified TensorFlow embedding model:

import tensorflow as tf
import numpy as np

user_input = tf.keras.layers.Input(shape=(1,))
item_input = tf.keras.layers.Input(shape=(1,))

user_embed = tf.keras.layers.Embedding(num_users, 64)(user_input)
item_embed = tf.keras.layers.Embedding(num_items, 64)(item_input)

dot = tf.keras.layers.Dot(axes=2)([user_embed, item_embed])
output = tf.keras.layers.Flatten()(dot)

model = tf.keras.Model([user_input, item_input], output)
model.compile(optimizer='adam', loss='mse')

This builds a simple collaborative filtering neural model.

For production, use:

  • TensorFlow Recommenders (TFRS)
  • LightFM
  • Implicit ALS
  • Transformers (BERT4Rec)

Step 4: Deploy the Model as an Inference API

Example using FastAPI:

from fastapi import FastAPI

app = FastAPI()

@app.get("/recommend")
def recommend(user_id: int):
    # load model
    # compute top recommendations
    # return list of item IDs
    return {"recommendations": [22, 33, 19, 7]}

Host on:

  • AWS Lambda
  • Google Cloud Run
  • Railway
  • Render
  • Supabase Edge Functions

Make sure you implement:

  • Caching
  • Rate limiting
  • Authentication
  • Logging
  • Latency optimization

Step 5: Consume Recommendations in Flutter

Use dio:

final dio = Dio();

Future<List<int>> fetchRecommendations(int userId) async {
  final response = await dio.get(
    'https://api.example.com/recommend',
    queryParameters: {'user_id': userId},
  );

  return List<int>.from(response.data['recommendations']);
}

Step 6: Display Recommendations with Smooth UI

Create a card list or carousel:

ListView.builder(
  scrollDirection: Axis.horizontal,
  itemCount: recommendedItems.length,
  itemBuilder: (context, index) {
    final item = recommendedItems[index];
    return ProductCard(item: item);
  },
);

Step 7: Add Real-Time Personalization

For real-time ML feedback loops:

  • Track events live
  • Send user interactions to the backend
  • Recompute recommendations dynamically

Use:

  • Real-time databases (Firebase RTDB, Firestore, Supabase)
  • Scheduled background jobs
  • Pub/Sub streams

Step 8: Optimize for Production

A production-ready recommender must handle:

1. Latency <150ms

Use:

  • Vector databases (Pinecone, Qdrant, Weaviate, Redis Vector)
  • Model quantization
  • GPU inference when needed

2. Scalability:- Serverless or containerized microservices.

3. Fallback Logic:- If ML fails → show trending items, categories, or popular products.

4. Cold Start Handling

New users
New items
Sparse data

Solution:

  • Use content-based filtering
  • Use global trending items
  • Use demographic-based recommendations

5. A/B Testing

Test accuracy with:

  • CTR
  • Conversion rate
  • Engagement duration

Use Firebase Remote Config for experiments.

Real-World Architecture:

Here is a recommended architecture used by production apps:

Flutter App

Event Tracking → Firebase Analytics

Data Warehouse → BigQuery / Postgres

Feature Engineering

Model Training (TensorFlow / PyTorch / Vertex AI)

Model Registry

Inference API (Cloud Run / Lambda / Supabase)

Flutter App Calls API

Recommendations Displayed

For deep learning recommenders, add:

  • Vector database for embeddings
  • GPU backend for heavy inference

Security & Privacy Considerations

  • Never send raw user PII to ML models
  • Encrypt API communication (HTTPS only)
  • Use restricted tokens (JWT)
  • Allow data deletion on request (GDPR)

Cloud platforms like Firebase, Supabase, and AWS simplify compliance.

Conclusion:

In the article, I have explained how the How to Build Production-Ready AI Recommendation Systems in Flutter Apps. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

AI-powered recommendation systems transform ordinary apps into personalized, high-retention experiences. With Flutter’s fast UI and modern ML infrastructure, any developer can deploy production-grade recommendation engines.

To summarize, the key steps are:

  1. Track user interactions
  2. Build a clean dataset
  3. Train a suitable ML model
  4. Deploy a scalable inference API
  5. Integrate recommendations in Flutter
  6. Optimize for latency & scalability
  7. Continue retraining with fresh data

Recommendation systems are no longer limited to big tech—Flutter developers can now build the same level of intelligence into their mobile apps.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Serverless AI with Flutter: Using Firebase, Supabase & Cloud Functions for LLM Workflows

Building serverless AI with Flutter involves utilizing Firebase AI Logic SDK for direct client-side interaction with LLMs, or orchestrating calls to services like OpenAI via serverless functions (Firebase Cloud Functions or Supabase Edge Functions) for more complex, secure, and customizable workflows. This approach leverages serverless architecture for scalability and reduced backend management.

The past few years have dramatically changed how we build AI-powered apps. Large Language Models (LLMs) are no longer tools you call from expensive backend servers — the rise of serverless architectures, edge compute, and managed AI APIs has made it possible to build scalable AI experiences without maintaining infrastructure.

Flutter is uniquely positioned in this new wave: cross-platform UI, fast iteration, and seamless integration with cloud backends. In 2025, more teams are choosing serverless AI + Flutter because it gives them the perfect balance of speed, flexibility, cost-efficiency, and production-grade reliability.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Introduction

Why Serverless for AI?

Architecture Overview 

Key Components

The AI Landscape & Serverless Synergy

When to Choose Firebase for LLM Apps

Using Firebase Cloud Functions for LLM Workflows 

When to Choose Supabase for LLM Workflows

Choosing Between Firebase, Supabase & Cloud Functions

Deployment & Scaling Best Practices (2025)

Conclusion



Introduction:

Serverless AI has become the fastest and most cost-efficient way to run LLM-powered features in mobile apps. Flutter, combined with Firebase, Supabase, and Cloud Functions, provides a complete stack to build AI workflows without managing servers, provisioning GPUs, or dealing with traditional backend maintenance. 

  • Hook: Start with a compelling problem: building modern, intelligent apps is complex, but the rise of LLMs and serverless tech changes the game.
  • Context: Briefly explain the shift from traditional dedicated servers to highly scalable, managed services like Firebase and Supabase.
  • Thesis Statement: This article will guide you through architecting, building, and deploying serverless AI applications using Flutter as the frontend and both Firebase (Cloud Functions) and Supabase (Edge Functions/pgvector) for secure LLM

Why Serverless for AI?

Serverless fits perfectly for AI workloads due to its scalability, event-driven nature, and cost-efficiency. When paired with LLMs, serverless infrastructure lets apps execute on-demand AI tasks such as summarization, chat streaming, classification, and document processing. 

  1. No infrastructure management:- No servers. No patching. No deployments. Your AI endpoints scale automatically.
  2. Massively cost-efficient:- LLMs can be expensive, but serverless ensures you pay only for usage.
  3. Fast development:- Cloud Functions, Supabase Edge Functions, and Firebase Extensions accelerate prototyping.
  4. Security built in:- Secrets, user authentication, and row-level permissions prevent abuse of your LLM API keys.
  5. Global distribution:- Edge functions run closer to users for low-latency inference orchestration.
  6. Perfect pairing with Flutter:-
    • Flutter handles UI + client logic
    • Serverless handles AI workflows
    • No backend developer required

Architecture Overview :

A typical Flutter + Serverless AI workflow looks like this: 
1. Flutter sends input (text, file, or metadata) to the backend. 
2. Firebase Cloud Functions or Supabase Edge Functions process the request. 
3. The function calls an LLM API (OpenAI/Supabase Vector/Local microservices). 
4. The function streams result back to Flutter. 
5. Flutter UI updates using Cubit/Stream for real-time output. 

Key Components:

  • Flutter: The frontend framework for building cross-platform user interfaces.
  • Firebase AI Logic SDK: A method to integrate Google’s GenAI models (like Gemini) into a Flutter app, handling authentication and security.
  • Cloud Functions for Firebase: A serverless backend framework that runs JavaScript, TypeScript, or Python code in response to events or HTTPS requests. This is useful for running server-side AI logic, managing third-party API calls (e.g., OpenAI), and handling data processing.
  • Supabase: An open-source alternative to Firebase. It offers a Postgres database with vector capabilities (for semantic search), authentication, and low-latency serverless Edge Functions written in TypeScript or Dart

The AI Landscape & Serverless Synergy:

  • The Flutter Advantage for AI UIs:
    • Discuss Flutter’s ability to create beautiful, cross-platform UIs that work seamlessly with AI responses (e.g., streaming text, dynamic formatting).
  • Why “Serverless” is the Future of AI Development:
    • Scalability: Mention how serverless platforms automatically handle traffic spikes when your app goes viral.
    • Cost-Efficiency: Pay-per-execution model makes experimentation cheaper.
    • Reduced Ops: Focus on the code, not infrastructure management.

When to Choose Firebase for LLM Apps:

Use CaseWhy Firebase?
Real-time chat appsFirestore streaming + Functions
AI in social appsEasy Auth, scalable data
Mobile-first AI toolsPerfect Flutter integration
AI triggers based on eventsFirestore Triggers

Firebase is the “fastest to build” option, especially for teams without a backend engineer.

Using Firebase Cloud Functions for LLM Workflows :

Cloud Functions act as the secure gateway to run LLM operations such as summarization, chat actions, embeddings, etc. Firebase remains the most beginner-friendly and production-ready option for Flutter developers. When integrating AI, Firebase’s combination of Auth, Firestore, Cloud Functions, and Extensions becomes incredibly powerful.

  1. Firebase Cloud Function for AI Summarization 

Below is a snippet of a Node.js Firebase Cloud Function that summarizes text using an LLM API: 

Code Snippet:-

import * as functions from "firebase-functions"; 
import fetch from "node-fetch"; 
 
export const summarizeText = functions.https.onCall(async (data) => { 
  const text = data.text; 
 
  const response = await fetch("https://api.openai.com/v1/chat/completions", { 
    method: "POST", 
    headers: { 
      "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`, 
      "Content-Type": "application/json" 
    }, 
    body: JSON.stringify({ 
      model: "gpt-4o-mini", 
      messages: [ 
        { role: "system", content: "Summarize text concisely." }, 
        { role: "user", content: text } 
      ] 
    }) 
  }); 
 
  const result = await response.json(); 
  return { summary: result.choices[0].message.content }; 
}); 

2. Integrating Cloud Function in Flutter 

Flutter uses Firebase Functions SDK to call the above function and retrieve the summary. 

Code Snippet:-

final functions = FirebaseFunctions.instance; 
 
Future<String> summarize(String text) async { 
  final callable = functions.httpsCallable('summarizeText'); 
  final result = await callable.call({'text': text}); 
  return result.data['summary']; 
} 

3. Supabase Edge Functions for AI Workflows 

Supabase Edge Functions (Deno-based) allow extremely fast serverless execution with built-in vector search. 

Code Snippet:-

import { serve } from "https://deno.land/std/http/server.ts"; 
 
serve(async (req) => { 
  const { text } = await req.json(); 
 
  const embedding = await createEmbedding(text); // hypothetical call 
  const summary = await generateLLMResponse(text); 
 
  return new Response(JSON.stringify({ embedding, summary }), { 
    headers: { "Content-Type": "application/json" }, 
  }); 
}); 
 

4. Flutter Integration with Supabase 

Using the `supabase_flutter` SDK, Flutter apps can call Edge Functions easily. 

Code Snippet:-

final response = await supabase.functions.invoke( 
  'summarize', 
  body: {'text': 'Flutter makes AI apps easy.'}, 
); 

5. Real-time AI Streaming in Flutter 

When paired with Cubit/Bloc, Flutter can show live streaming responses from LLMs in chat-like UIs. 

Code Snippet:-

class ChatCubit extends Cubit<String> { 
  ChatCubit() : super(''); 
 
  Future<void> streamChat(String prompt) async { 
    emit("Loading..."); 
 
    final stream = supabase.functions.invokeStream( 
      'chatStream', 
      body: {'prompt': prompt}, 
    ); 
 
    await for (final chunk in stream) { 
      emit(state + chunk); 
    } 
  } 
} 

When to Choose Supabase for LLM Workflows:

Use CaseWhy Supabase?
RAG (Retrieval-Augmented Generation) appspgvector + SQL functions
Document search + semantic searchPerfect with embeddings
Real-time token streamingSmooth & fast
Complex analytics + AIPostgres power
Cost-sensitive appsCheaper than Firebase at scale

If your AI workflow is heavily database-driven, Supabase is the best choice

Choosing Between Firebase, Supabase & Cloud Functions:

Here’s a quick decision framework:-

  1. Choose Firebase:-
    • You need real-time chat or feed
    • You want the easiest Flutter integration
    • You prefer Google ecosystem features
    • Your app depends on Firestore events

2. Choose Supabase:-

  • You need embeddings or vector search
  • You want SQL control
  • You want real-time token streaming
  • You want a more open-source, self-hostable stack
  • Your AI workflows require fast edge functions

3. Choose Cloud Functions (general):-

  • You want maximum customization
  • You want provider-agnostic architecture
  • You need to orchestrate complex LLM pipelines
  • You prefer building your own API layer

Deployment & Scaling Best Practices (2025):

1. Keep your LLM keys secure:

-> Never store keys in Flutter code.
-> Always store them in:

  • Firebase Functions environment variables
  • Supabase Edge function secrets
  • Cloud Run secret manager

2. Implement usage limits per user: Prevent abuse by enforcing:

  • daily token quotas
  • rate limits
  • per-minute request caps
  • per-user billing

3. Use streaming responses:

  • Streaming keeps apps fast and interactive.
  • Most LLM providers now support streaming tokens.

4. Cache embeddings:

Embeddings rarely change → store them once.

5. Use hybrid retrieval (keywords + vectors):

For better accuracy in RAG applications.

Conclusion: 

Flutter + Serverless AI empowers developers to build scalable, fast, low-cost AI apps without maintaining servers. With Firebase, Supabase, and Cloud Functions, you get authentication, databases, vector search, and LLM orchestration—all serverless. 

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Flutter 2025 Performance Best Practices: What Has Changed & What Still Works

In 2025, Flutter performance best practices emphasize leveraging the Impeller rendering engine, adopting a modular architecture, and utilizing enhanced debugging tools, while core optimization techniques like minimizing widget rebuilds and using const Constructors remain crucial.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Introduction

The Big Picture: Flutter’s Performance Transformation in 2025

The Era of Impeller: Goodbye Jank, Hello 120 FPS

WebAssembly (Wasm): The Web Performance Revolution

Android 15 & 16KB Page Size Compatibility

Enduring Foundations: What Still Works (And Is More Important Than Ever)

New Practices for 2025

2025 Tools and Techniques for Peak Performance

AI and Modular Development

Advanced Techniques and Tooling in 2025

The Future of Performance: 2026 and Beyond

Conclusion



Introduction:

Smooth, responsive user experiences are non-negotiable in 2025. As Flutter solidifies its position as a dominant cross-platform framework—spanning mobile, web, desktop, and embedded systems—optimizing for performance has never been more critical.

The landscape of Flutter development is constantly evolving. The past year has introduced significant shifts, particularly in the rendering pipeline and tooling, while foundational best practices remain as relevant as ever. This article serves as your definitive guide to building high-performance Flutter applications in the era of Impeller, WebAssembly, and AI-driven development.

The Big Picture: Flutter’s Performance Transformation in 2025:

Flutter in 2025 is a mature, productivity-focused ecosystem. The primary narrative isn’t just about faster development cycles, but about achieving near-native performance across every target platform. Google’s ongoing commitment to performance is evident in several key architectural upgrades.

The Era of Impeller: Goodbye Jank, Hello 120 FPS:

The single most significant change in Flutter’s recent history is the widespread adoption of the Impeller rendering engine.

  • What Changed? Impeller is now the default renderer on iOS and modern Android devices, effectively eliminating the shader compilation jank that plagued apps using the older Skia engine. Instead of compiling shaders at runtime, which caused stuttering during animations, Impeller pre-compiles them, ensuring consistent 60–120 frames per second (FPS) animations.
  • New Best Practice: The focus has shifted from avoiding jank to maximizing Impeller’s potential. Developers must now leverage GPU profiling tools within Flutter DevTools and Perfetto to analyze rendering performance, detect jank, and fine-tune GPU utilization.

WebAssembly (Wasm): The Web Performance Revolution

Flutter for the web is no longer an experimental feature; it’s a viable, high-performance alternative to traditional web frameworks.

  • What Changed? The introduction and stabilization of WebAssembly (Wasm) compilation for Flutter web apps provides near-native execution speeds in the browser. This drastically improves startup times and overall performance compared to older CanvasKit or HTML renderers.
  • New Best Practice: When targeting the web, adopt Wasm as your compilation target (when stable). Focus on optimizing for the web environment by implementing code splitting and lazy loading for large applications to reduce initial bundle size and speed up first load times.

Android 15 & 16KB Page Size Compatibility:

A critical, future-proofing requirement for Android developers is compatibility with 16KB memory pages, a mandate starting with Android 15.

  • What Changed? Android is moving to larger memory pages for improved efficiency. Apps not built with this in mind may not function optimally or gain the associated performance benefits.
  • New Best Practice: Ensure your project is updated to a compatible Flutter version (Flutter 3.38+). This ensures your app can run efficiently on upcoming Android OS versions, gaining performance benefits related to memory management and allocation

Enduring Foundations: What Still Works (And Is More Important Than Ever)

While the tools and underlying architecture evolve, the fundamental principles of efficient Flutter development remain constant. These are the “eternal truths” that separate performant applications from sluggish ones.

1. The Cardinal Rule: Minimize Widget Rebuilds

The single most impactful best practice in 2025 continues to be the management of your widget tree and the prevention of unnecessary rebuilds. Every time a widget rebuilds, Flutter goes through the build, layout, and paint stages, consuming resources.

  • Leverage const Constructors: Use const constructors everywhere possible. This tells Flutter that the widget and its entire subtree are immutable and do not need to be rebuilt, even if the parent widget changes.
  • Localize setState(): Avoid calling setState() high up in your widget tree. Break large widgets into smaller, reusable components. Use ValueNotifierConsumer, or Selector (depending on your state management solution) to only rebuild the exact part of the UI that needs updating.
  • Use RepaintBoundary Thoughtfully: For complex, static subtrees that are being moved or animated, wrapping them in a RepaintBoundary can optimize performance by preventing the entire parent from repainting.

2. Master State Management Architecture

Choosing the right state management solution is crucial for scalability and performance. A poorly implemented state architecture leads to chaotic code and excessive rebuilds.

  • Separate Logic from UI: Adhere to clean architecture principles (e.g., BLoC, MVVM, or similar patterns) to strictly separate business logic from the UI layer.
  • Choose the Right Tool:
    • Provider/Riverpod: Excellent for small-to-medium applications due to their simplicity and scalability. Riverpod 3.0 offers type-safe, compile-time checked alternatives that reduce errors.
    • BLoC/Cubit: Ideal for complex enterprise applications with predictable state flows, offering robust structure and testability.
  • Dispose of Resources: Always ensure that ChangeNotifierStreamController, and AnimationController are properly disposed of in the dispose() method to prevent memory leaks.

3. Optimize Asset and Image Handling

Images are often the biggest culprits of performance bottlenecks due to their size and memory consumption.

  • Compression and Format: Compress images before deploying them. Use modern formats like WebP.
  • Lazy Loading: Use ListView.builder or GridView.builder for dynamic or long lists to ensure that only visible items are built and rendered (lazy loading). Avoid using plain ListView for long lists.
  • Caching and Preloading: Employ the cached_network_image package for network images. For images needed immediately after a route transition, use precacheImage() to load them into the cache ahead of time.

4. Leverage Asynchronous Programming and Isolates

Avoid blocking the main UI thread (the isolate responsible for the 16ms frame rendering deadline) with heavy computations.

  • async/await for I/O: Use async and await for network calls and file operations.
  • compute for heavy CPU Work: For intense data processing, JSON parsing, or image manipulation, offload the work to a separate isolate using Dart’s compute function or Isolate.run() to keep the UI smooth and responsive.

New Practices for 2025:

  • Modular Architecture (Micro-Apps): Adopting a modular or micro-app architecture is a leading practice to enhance scalability, maintainability, and team collaboration, often using tools like flutter_modular.
  • AI-Driven Development Tools: New AI assistants, such as Flutter + Gemini integration, are available within DevTools to help automate boilerplate code, suggest optimal widget layouts, and aid in predictive debugging.
  • 16KB Page Size Compatibility: Support for 16KB memory pages is a key requirement for Android 15 and higher (starting November 1, 2025). Developers must ensure their apps are compatible (e.g., using Flutter 3.38 or higher) to gain performance benefits.
  • Enhanced Tooling: Flutter DevTools received upgrades for real-time performance monitoring, improved memory leak detection, and better integration with CI/CD pipelines. 

2025 Tools and Techniques for Peak Performance:

The developer experience (DevEx) has seen massive improvements, offering powerful new ways to profile and optimize apps.

Enhanced Flutter DevTools Suite

The Flutter DevTools remain your primary tool for addressing performance issues. The 2025 version offers improved functionality:

  • Real-time Profiling: Analyze frame rates (FPS), memory usage, and CPU consumption in real-time. The performance view helps you pinpoint which widgets are taking too long to build or render.
  • Memory Leak Detection: Advanced tools now make it easier to identify and fix memory leaks quickly.
  • DevTools Suggestions: The tools provide automated optimization hints directly within the interface, acting as an AI assistant to guide you to better practices.

AI and Modular Development:

AI-driven development tools can assist with code generation and debugging. For large applications, modular architectures can enhance performance by allowing independent development and integration of different parts of the app.

Advanced Techniques and Tooling in 2025:

Beyond the fundamentals, 2025 demands a mastery of advanced techniques and a proactive approach to tooling.

1. Deep Dive into DevTools for Proactive Profiling

“Measure before you optimize” is a development mantra. The suite of Flutter DevTools is more robust than ever, providing real-time insights into your app’s performance in profile mode (as opposed to debug mode, which adds overhead).

  • The Performance View: Your go-to tab for analyzing frame rendering times.
    • UI Thread vs. GPU Thread: A healthy app keeps both under the crucial 16ms target for a 60 FPS experience (or 8ms for 120 FPS screens). The UI thread handles the building of the widget tree, while the GPU thread handles the rendering.
    • Identifying Jank: The timeline view helps you pinpoint exactly which operations (e.g., a costly build() method, excessive layout passes, or a hidden saveLayer() call) are causing frame drops and visual stuttering.
  • The Widget Inspector: Use the “Track Widget Rebuilds” feature to visually see which parts of your UI are unnecessarily rebuilding. This immediately highlights areas where you need to implement const or localized state.
  • Memory View: Track memory allocation and garbage collection. This is vital for detecting memory leaks caused by unclosed streams, lingering references, or un-disposed controllers.

2. Harnessing Dart’s Power: Isolates and FFI

For computationally intensive tasks, Dart offers powerful concurrency features that prevent the UI thread from freezing.

  • Isolates (The compute function): Isolates are separate memory heaps that allow Dart code to run concurrently. The built-in compute() function or Isolate.run() offloads heavy CPU-bound tasks like large JSON parsing or complex mathematical operations to a different core, ensuring your UI remains buttery smooth.
  • FFI (Foreign Function Interface): The FFI has become a superpower for Flutter in 2025. It enables developers to seamlessly integrate high-performance, low-level native code (e.g., written in C, C++, or even Rust via packages like flutter_rust_bridge). This is especially useful for performance-critical logic or leveraging existing high-performance libraries, without the overhead of traditional platform channels.

3. App Size Optimization and Code Generation

A smaller app loads faster, installs faster, and is less likely to be uninstalled.

  • Tree Shaking and Code Splitting: Flutter’s ahead-of-time (AOT) compiler automatically performs tree shaking, removing unused code. You can further optimize this by using deferred loading (code splitting) for large application sections or features only used by a subset of users.
  • Codegen and Immutability: Tools like freezed streamline the creation of immutable data models. Writing less manual boilerplate code not only increases productivity but also reduces the chance of errors that can impact performance or introduce memory issues.

The Future of Performance: 2026 and Beyond

Performance optimization in Flutter is not a one-time task but a continuous journey. As we look beyond 2025, several trends are poised to shape our best practices:

  • Deeper AI Integration: Expect more sophisticated AI assistants built directly into our IDEs, not just for boilerplate code, but for predictive performance analysis that highlights potential bottlenecks before you even run your app.
  • Advanced AR/VR Support: As immersive experiences become more mainstream, Flutter’s rendering pipeline will likely evolve to support high-performance augmented and virtual reality applications.
  • Sustainable Coding Practices: There is a growing emphasis on “green coding” and minimizing the energy footprint of applications to improve battery life and app store rankings. Flutter’s energy profiler will become a key tool for developers focused on energy efficiency.

Conclusion:

In the article, I have explained how the Flutter 2025 Performance Best Practices: What Has Changed & What Still Works. This was a small introduction to User Interaction from my side, and it’s working using Flutter.

Effective Flutter performance in 2025 relies on utilizing advancements like the Impeller engine and Wasm compilation, alongside core best practices. Employing Flutter’s profiling tools and implementing optimizations for widget management and assets are key to building efficient applications.

In 2025, delivering high performance is no longer a luxury—it is an expectation. By embracing the power of the Impeller engine, mastering DevTools, leveraging advanced Dart features like Isolates and FFI, and adhering to enduring principles of efficient widget management, you can build applications that are not only beautiful but blazingly fast and future-proof.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


Expansible Widget In Flutter

You may have discovered Expansible, a powerful new widget, while experimenting with Flutter 3.32‘s latest capabilities and found that ExpansionTileController is no longer supported.

In this article, we will be Expansible Widget In Flutter. We will learn how to execute a demo program. Let’s examine why this is important and how to use this new widget to create user interfaces that are more adaptable, customisable, and contemporary in your Flutter applications.

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table Of Contents:

Reasons for Needing a New Expansion Widget in Flutter:

What Is Unique About Expansible?

Implement Code

Code File

Conclusion



Reasons for Needing a New Expansion Widget in Flutter:

The standard for developing expandable parts, particularly in lists, for many years was Flutter’s ExpansionTile. It was straightforward, had a Material theme, and was simple to integrate into programs.

However, there was a catch: You would run into trouble if you attempted to use Material Design or develop a custom-styled extension component.

Enter: Expansible
The framework presents Expansible, a theme-neutral expansion behaviour building block, with Flutter 3.32. Consider it the engine that drives any type of expandable user interface, regardless of whether you use Material or create your design system.

What Is Unique About Expansible?:

You have complete control with Expansible. With its versatile constructor functions, you may specify how the header appears, how the information animates, and how it is constructed.

Let’s explore it:-

  • Utilise ExpansibleController to Manage Everything:
    Use the new ExpansibleController to programmatically open/close your widget or monitor its status.
final myController = ExpansibleController();

myController.expand();   // Programmatically expand
myController.collapse(); // Programmatically collapse
myController.isExpanded; // Track state

Even better, you can listen for changes:

myController.addListener(() {
print('Expanded: ${myController.isExpanded}');
});
  • Use Case: Custom Expandable Widget:

Expansible is your best buddy if you require a unique layout, design, or animations.

Easy-to-Explain Example

Expansible(
  controller: myController,
  headerBuilder: (context, animation) => MyCustomHeader(animation),
  bodyBuilder: (context, animation) => MyCustomBody(animation),
  expansibleBuilder: (context, header, body, animation) => Column(
    children: [header, body],
  ),
)

What is going on here?

  1. headerBuilder: Specifies the tappable, viewable area.
  2. bodyBuilder: The content that can be expanded.
  3. expansibleBuilder: They are combined, which gives you total layout flexibility.

Implement Code:

Let’s look at a simplified example to see how Expansible functions:

            Expansible(
              controller: _myCustomExpansibleController,
              headerBuilder:
                  (BuildContext context, Animation<double> animation) {
                return GestureDetector(
                  onTap: () {
                    if (_myCustomExpansibleController.isExpanded) {
                      _myCustomExpansibleController.collapse();
                    } else {
                      _myCustomExpansibleController.expand();
                    }
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.deepPurple.shade200,
                      borderRadius: BorderRadius.circular(12.0),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.1),
                          blurRadius: 4,
                          offset: const Offset(0, 2),
                        ),
                      ],
                    ),
                    padding: const EdgeInsets.all(18.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Click Me to Reveal!',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontWeight: FontWeight.w600,
                            color: Colors.deepPurple,
                          ),
                        ),
                        RotationTransition(
                          turns: Tween(
                            begin: 0.0,
                            end: 0.5,
                          ).animate(animation),
                          child: Icon(
                            _myCustomExpansibleController.isExpanded
                                ? Icons.keyboard_arrow_up
                                : Icons.keyboard_arrow_down,
                            color: Colors.deepPurple,
                            size: 28,
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
              bodyBuilder: (BuildContext context, Animation<double> animation) {
                return SizeTransition(
                  sizeFactor: animation,
                  axisAlignment: -1.0,
                  child: FadeTransition(
                    opacity: animation,
                    child: Container(
                      margin: const EdgeInsets.only(top: 10.0),
                      padding: const EdgeInsets.all(20.0),
                      decoration: BoxDecoration(
                        color: Colors.deepPurple.shade50,
                        borderRadius: BorderRadius.circular(12.0),
                        border: Border.all(
                          color: Colors.deepPurple.shade100,
                          width: 2,
                        ),
                      ),
                      child: const Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            '🎉 Voila! Here\'s your secret content.',
                            style: TextStyle(
                              fontSize: 16.0,
                              color: Colors.black87,
                            ),
                          ),
                          SizedBox(height: 10.0),
                          Text(
                            'You can put anything here: forms, images, lists, or even other expandable widgets!',
                            style: TextStyle(
                              fontSize: 14.0,
                              color: Colors.grey,
                            ),
                          ),
                          SizedBox(height: 10.0),
                          Row(
                            children: [
                              Icon(Icons.check_circle, color: Colors.green),
                              SizedBox(width: 8),
                              Text('Fully customizable layout'),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
              expansibleBuilder:
                  (
                  BuildContext context,
                  Widget header,
                  Widget body,
                  Animation<double> animation,
                  ) {
                return Column(children: [header, body]);
              },
              duration: const Duration(milliseconds: 400),
              curve: Curves.easeInOutQuad,
              reverseCurve: Curves.easeOutCubic,
              maintainState: true, // Keep state even when collapsed
            ),

You can change the layout, animate the icons, and customise the widget’s appearance. That’s what makes Expansible so great.

ExpansionTile now runs on Expansible thanks to Flutter 3.32. This indicates that the previous ExpansionTileController is no longer supported. Do you also want to use buttons to control your tiles?

            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () => _myCustomExpansibleController.expand(),
                  child: const Text('Expand'),
                ),
                ElevatedButton(
                  onPressed: () => _myCustomExpansibleController.collapse(),
                  child: const Text('Collapse'),
                ),
                ElevatedButton(
                  onPressed: () {
                    if (_myCustomExpansibleController.isExpanded) {
                      _myCustomExpansibleController.collapse();
                    } else {
                      _myCustomExpansibleController.expand();
                    }
                  },
                  child: const Text('Toggle'),
                ),
              ],
            ),

Using ExpansionTile or your unique Expansible widget does not affect how it functions. 🎯

Code File:

import 'package:flutter/material.dart';

class ExpansibleDemoPage extends StatefulWidget {
  const ExpansibleDemoPage({super.key});

  @override
  State<ExpansibleDemoPage> createState() => _ExpansibleDemoPageState();
}

class _ExpansibleDemoPageState extends State<ExpansibleDemoPage> {
  late final ExpansibleController _myCustomExpansibleController;

  @override
  void initState() {
    super.initState();
    _myCustomExpansibleController = ExpansibleController();
    _myCustomExpansibleController.addListener(_onExpansionStateChanged);
  }

  void _onExpansionStateChanged() {
    print(
      'Custom Expansible state: ${_myCustomExpansibleController.isExpanded}',
    );
  }

  @override
  void dispose() {
    _myCustomExpansibleController.removeListener(_onExpansionStateChanged);
    _myCustomExpansibleController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Expansible Demo')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            Expansible(
              controller: _myCustomExpansibleController,
              headerBuilder:
                  (BuildContext context, Animation<double> animation) {
                return GestureDetector(
                  onTap: () {
                    if (_myCustomExpansibleController.isExpanded) {
                      _myCustomExpansibleController.collapse();
                    } else {
                      _myCustomExpansibleController.expand();
                    }
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.deepPurple.shade200,
                      borderRadius: BorderRadius.circular(12.0),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.1),
                          blurRadius: 4,
                          offset: const Offset(0, 2),
                        ),
                      ],
                    ),
                    padding: const EdgeInsets.all(18.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Click Me to Reveal!',
                          style: TextStyle(
                            fontSize: 18.0,
                            fontWeight: FontWeight.w600,
                            color: Colors.deepPurple,
                          ),
                        ),
                        RotationTransition(
                          turns: Tween(
                            begin: 0.0,
                            end: 0.5,
                          ).animate(animation),
                          child: Icon(
                            _myCustomExpansibleController.isExpanded
                                ? Icons.keyboard_arrow_up
                                : Icons.keyboard_arrow_down,
                            color: Colors.deepPurple,
                            size: 28,
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
              bodyBuilder: (BuildContext context, Animation<double> animation) {
                return SizeTransition(
                  sizeFactor: animation,
                  axisAlignment: -1.0,
                  child: FadeTransition(
                    opacity: animation,
                    child: Container(
                      margin: const EdgeInsets.only(top: 10.0),
                      padding: const EdgeInsets.all(20.0),
                      decoration: BoxDecoration(
                        color: Colors.deepPurple.shade50,
                        borderRadius: BorderRadius.circular(12.0),
                        border: Border.all(
                          color: Colors.deepPurple.shade100,
                          width: 2,
                        ),
                      ),
                      child: const Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            '🎉 Voila! Here\'s your secret content.',
                            style: TextStyle(
                              fontSize: 16.0,
                              color: Colors.black87,
                            ),
                          ),
                          SizedBox(height: 10.0),
                          Text(
                            'You can put anything here: forms, images, lists, or even other expandable widgets!',
                            style: TextStyle(
                              fontSize: 14.0,
                              color: Colors.grey,
                            ),
                          ),
                          SizedBox(height: 10.0),
                          Row(
                            children: [
                              Icon(Icons.check_circle, color: Colors.green),
                              SizedBox(width: 8),
                              Text('Fully customizable layout'),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
              expansibleBuilder:
                  (
                  BuildContext context,
                  Widget header,
                  Widget body,
                  Animation<double> animation,
                  ) {
                return Column(children: [header, body]);
              },
              duration: const Duration(milliseconds: 400),
              curve: Curves.easeInOutQuad,
              reverseCurve: Curves.easeOutCubic,
              maintainState: true, // Keep state even when collapsed
            ),
            const SizedBox(height: 30),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () => _myCustomExpansibleController.expand(),
                  child: const Text('Expand'),
                ),
                ElevatedButton(
                  onPressed: () => _myCustomExpansibleController.collapse(),
                  child: const Text('Collapse'),
                ),
                ElevatedButton(
                  onPressed: () {
                    if (_myCustomExpansibleController.isExpanded) {
                      _myCustomExpansibleController.collapse();
                    } else {
                      _myCustomExpansibleController.expand();
                    }
                  },
                  child: const Text('Toggle'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion:

In the article, I have explained how the Expansible Widget In Flutter; you can modify this code according to your choice. This was a small introduction to Expansible Widget in Flutter User Interaction from my side, and it’s working using Flutter.

I hope this blog will provide you with sufficient information on trying the Expansible Widget in your Flutter projectsMore than just a back-end update, Flutter’s new Expansible widget provides developers with the means to create more dynamic, personalised, and aesthetically pleasing expandable user interfaces.

So go ahead and start creating the precise user interface your app requires by refactoring that outdated ExpansionTileController and investigating the new Expansible. So please try it.

❤ ❤ Thanks for reading this article ❤❤

If I need to correct something? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automation, IoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutter developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on FacebookGitHubTwitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.


How to Reduce App Size in Flutter by 40%: Proven Optimization Techniques 

If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.


Table of Contents

Introduction

Why App Size Matters

Understanding Flutter App Size Components

Proven Optimization Techniques

Implementation Best Practices

Measuring Your Results

Conclusion

References


Introduction

In today’s competitive mobile app landscape, application size plays a critical role in user acquisition and retention. Studies have consistently shown that larger app sizes lead to higher abandonment rates during download, particularly in regions with limited bandwidth or storage constraints. For Flutter developers, optimizing app size is not just a technical consideration but a business imperative that directly impacts user experience and market reach.

Flutter applications, while offering exceptional cross-platform capabilities and performance, can sometimes result in larger binary sizes compared to native applications. This comprehensive guide explores proven optimization techniques that can help you reduce your Flutter app size by up to 40% or more, ensuring your application remains accessible to the widest possible audience while maintaining its functionality and performance.


Why App Size Matters

The importance of app size optimization extends far beyond mere technical metrics. Research indicates that for every 6 MB increase in app size, the conversion rate decreases by approximately 1%. This statistic becomes particularly significant when considering emerging markets where users often rely on limited data plans and devices with restricted storage capacity.

Large app sizes create multiple friction points in the user journey. Users may abandon downloads that take too long, especially on slower connections. Additionally, many users regularly clean up their devices by removing larger applications to free up space, making bloated apps prime candidates for deletion. From a distribution perspective, app stores often highlight app size prominently, and users actively consider this metric when deciding whether to download an application.

Beyond user experience, smaller app sizes reduce bandwidth costs for both developers and users, decrease update times, and improve overall app store optimization rankings. For businesses operating in bandwidth-constrained markets or targeting users with entry-level devices, app size optimization can mean the difference between market success and failure.


Understanding Flutter App Size Components

Before diving into optimization techniques, it is essential to understand what contributes to your Flutter app’s overall size. A typical Flutter application consists of several key components that collectively determine the final binary size.

The Flutter engine itself forms the foundation of your application and includes the Dart runtime, Skia graphics library, and platform-specific embedder code. While this component is necessary for Flutter functionality, its impact can be minimized through proper build configurations.

Your application code, including all Dart files and their compiled representations, constitutes another significant portion. This includes not only your custom code but also all imported packages and dependencies. Each package you add introduces additional code, resources, and potential native dependencies.

Assets such as images, fonts, animations, and other media files often represent the largest portion of app size. Without proper optimization, high-resolution images and unnecessary asset variants can quickly inflate your app size.

Native dependencies and platform-specific code, particularly third-party SDKs and libraries, add their own overhead. Some packages include native implementations for iOS and Android that increase the overall footprint.

Understanding these components allows you to target your optimization efforts effectively and achieve maximum size reduction.


Proven Optimization Techniques

1. Enable Code Shrinking and Obfuscation

Code shrinking and obfuscation serve dual purposes: reducing app size and enhancing security. When you build your Flutter app in release mode with these optimizations enabled, the compiler removes unused code paths, shortens identifier names, and eliminates debugging information.

To enable code shrinking and obfuscation, build your release version using the following command:

flutter build apk --release --obfuscate --split-debug-info=/<project-directory>/debug-info

For iOS builds, use:

flutter build ios --release --obfuscate --split-debug-info=/<project-directory>/debug-info

This approach typically reduces code size by 15–20% while making reverse engineering significantly more difficult. The --split-debug-info flag extracts debugging symbols to a separate directory, which can be used later for crash report analysis without bloating the release binary.

2. Split APKs by ABI

Android devices use different Application Binary Interfaces (ABIs) depending on their processor architecture. By default, Flutter builds a fat APK containing native code for all supported ABIs (armeabi-v7a, arm64-v8a, x86_64). This approach ensures compatibility but significantly increases app size.

Splitting APKs by ABI allows you to generate separate APKs for each architecture, reducing individual file sizes by 30–40%. Users automatically receive the appropriate APK for their device architecture when downloading from the Google Play Store.

Configure ABI splitting in your android/app/build.gradle file:

android {
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk false
}
}
}

Build split APKs using:

flutter build apk --split-per-abi

This technique is particularly effective because most users only need one ABI-specific version, resulting in significant download size reduction without any functionality loss.

3. Optimize Image Assets

Images frequently constitute the largest portion of app size, making image optimization one of the most impactful techniques for size reduction. Several strategies can dramatically reduce image-related overhead.

First, always use appropriate image formats. WebP offers superior compression compared to PNG and JPEG while maintaining quality. For simple graphics and icons, consider using SVG with the flutter_svg package, as vector graphics scale without quality loss and typically have smaller file sizes.

Compress all images before including them in your project. Tools like TinyPNG, ImageOptim, or command-line utilities like pngquant can reduce image file sizes by 50–70% without noticeable quality degradation.

Implement proper asset variants for different screen densities. Flutter’s asset system supports 1x, 2x, and 3x variants. However, in many cases, providing only 2x and 3x variants suffices, as Flutter can scale images appropriately. Remove any unused density variants to reduce package size.

Consider lazy-loading images and implementing caching strategies for remote images rather than bundling everything in the app. For frequently changing content, downloading assets on demand can significantly reduce initial app size.

4. Remove Unused Resources

Over time, Flutter projects accumulate unused assets, dependencies, and code. Regular auditing and cleanup of these unused resources can yield substantial size reductions.

Start by examining your pubspec.yaml dependencies. Remove any packages that are no longer used in your application. Each package adds not only its own code but potentially native dependencies and additional assets.

Use static analysis tools to identify unused assets. While Flutter does not automatically detect unused image files, you can use custom scripts or tools to scan your codebase for asset references and remove unreferenced files.

For Android-specific optimizations, enable resource shrinking in your android/app/build.gradle:

android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}

This configuration automatically removes unused resources from Android dependencies and the Android framework itself.

5. Use Deferred Components

Deferred components, also known as dynamic feature modules, allow you to split your application into smaller downloadable units. Users initially download only the core functionality, with additional features downloaded on demand.

This technique is particularly valuable for apps with distinct feature sets that users may not access immediately. For example, advanced settings, premium features, or specific tool sets can be deferred until needed.

Implementing deferred components requires planning your app’s architecture around feature modules. While the initial setup requires more effort, the payoff in reduced initial download size can be substantial, particularly for feature-rich applications.

Configure deferred components using Flutter’s deferred imports:

import 'package:my_app/advanced_feature.dart' deferred as advanced;
// Later, when needed:
await advanced.loadLibrary();
advanced.someFunction();

This approach can reduce initial app size by 20–30% or more for apps with distinct feature modules.

6. Minimize Dependencies

Every dependency you add to your Flutter project increases app size. While packages provide valuable functionality, judicious dependency management is crucial for maintaining optimal app size.

Before adding a new package, evaluate whether you truly need it. Can you implement the functionality yourself with reasonable effort? Sometimes, a few dozen lines of custom code weigh less than importing an entire package.

When dependencies are necessary, choose lightweight alternatives when available. Some packages offer modular versions or allow you to import only the components you need. Always prefer packages that follow this approach.

Regularly audit your dependencies using flutter pub deps to understand the dependency tree. Sometimes, you may discover that multiple packages depend on different versions of the same underlying dependency, creating unnecessary duplication.

Consider using conditional imports for platform-specific functionality rather than including both iOS and Android implementations when only one is needed.

7. Enable ProGuard and R8

For Android builds, ProGuard and its successor R8 provide powerful code shrinking and optimization capabilities. These tools analyze your code, remove unused classes and methods, and optimize the remaining bytecode.

R8 is enabled by default in recent Flutter versions for release builds, but you can fine-tune its behavior for optimal results. Create or modify android/app/proguard-rules.pro to customize optimization rules:

-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

Enable aggressive optimization in android/app/build.gradle:

android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

R8 optimization typically reduces Android app size by an additional 10–15% beyond basic minification.

8. Optimize Font Files

Custom fonts add character to your application but can significantly increase app size if not managed properly. A single font family with multiple weights can easily add several megabytes to your app.

Use variable fonts when possible, as they combine multiple weights and styles into a single file, typically resulting in smaller overall size compared to individual font files for each weight.

Include only the font weights you actually use. If your design only requires regular and bold weights, do not include light, medium, semi-bold, and other variants.

Consider subsetting fonts to include only the characters you need. If your app only displays Latin characters, remove glyphs for other scripts. Tools like fonttools or online subsetting services can help with this process.

For icon fonts, consider using Flutter’s built-in Icons or a cupertino_icons package instead of custom icon fonts, as these are already optimized and included in Flutter’s core.

9. Use App Bundles Instead of APKs

Android App Bundles represent Google’s modern app distribution format, offering significant advantages over traditional APKs. When you upload an App Bundle to Google Play, the platform generates optimized APKs tailored to each user’s device configuration.

App Bundles automatically handle ABI splitting, screen density resources, and language resources, delivering only what each user needs. This approach can reduce download sizes by 35–50% compared to universal APKs.

Build an App Bundle using:

flutter build appbundle --release

Upload the resulting .aab file to Google Play Console instead of APK files. Google Play handles the rest, automatically generating and serving optimized APKs to users based on their device characteristics.

Additionally, App Bundles enable Google Play’s dynamic delivery features, allowing you to implement on-demand and conditional module delivery for even greater size optimization.

10. Analyze and Monitor App Size

Continuous monitoring and analysis of app size ensures that optimization remains an ongoing priority rather than a one-time effort. Several tools help you understand and track app size metrics.

Flutter provides built-in size analysis tools. Use the --analyze-size flag when building to generate a detailed breakdown:

flutter build apk --release --analyze-size

This command generates a size analysis file that you can inspect to understand which components contribute most to your app size.

For more detailed analysis, use the DevTools app size tool, which provides an interactive treemap visualization of your app’s composition. This visual representation helps identify unexpectedly large components.

Implement continuous integration checks that fail builds exceeding defined size thresholds, ensuring that size optimization remains a priority throughout development.


Implementation Best Practices

Successful app size optimization requires more than just applying individual techniques. Adopting certain best practices ensures sustainable results across your development lifecycle.

Make app size optimization a regular part of your development process rather than a pre-release scramble. Review app size metrics with each significant feature addition to catch size increases early when they are easier to address.

Document your optimization decisions and techniques so that team members understand why certain approaches were chosen. This documentation prevents well-intentioned but size-inflating changes from being introduced later.

Test thoroughly after applying optimization techniques. While these methods are generally safe, some optimizations can occasionally cause issues with specific functionality. Comprehensive testing ensures that size reduction does not come at the cost of functionality or stability.

Establish size budgets for different app components. For example, you might allocate specific size budgets to images, fonts, and dependencies. This approach helps teams make informed decisions about when size trade-offs are worthwhile.


Measuring Your Results

After implementing optimization techniques, accurately measuring results helps you understand the effectiveness of your efforts and identify areas for further improvement.

Compare builds before and after optimization using consistent measurement methods. Note both the download size (what users see in app stores) and the installed size (disk space required on the device).

For Android, analyze both the APK or App Bundle size and the actual download size from Google Play, as these often differ significantly. Google Play’s delivery optimization means users typically download less than the full App Bundle size.

Track size metrics over time to ensure that optimization gains are maintained. As you add features and dependencies, monitor size growth and address increases proactively.

Consider segmenting size analysis by user demographics or device types. Understanding which user segments are most affected by app size can help prioritize optimization efforts.


Conclusion

Reducing Flutter app size by 40% or more is not only achievable but essential for maximizing your application’s reach and user satisfaction. By implementing the proven optimization techniques outlined in this guide, you can significantly reduce your app’s footprint while maintaining all its functionality and performance characteristics.

The techniques covered range from simple configuration changes like enabling code obfuscation to more involved strategies like implementing deferred components and optimizing assets. Most applications will benefit from a combination of these approaches, with the specific mix depending on your app’s architecture and requirements.

Remember that app size optimization is an ongoing process rather than a one-time task. As your application evolves, new features and dependencies will naturally increase size. By maintaining vigilance and regularly applying optimization techniques, you can ensure your Flutter app remains lean and accessible to the broadest possible audience.

Start with the highest-impact techniques like image optimization and ABI splitting, then progressively implement additional strategies. Monitor your results, celebrate your wins, and continue refining your approach. Your users, particularly those in bandwidth-constrained environments or using devices with limited storage, will thank you with better engagement and retention rates.


References

Build and release an Android app
How to prepare for and release an Android app to the Play store.docs.flutter.dev

Build and release an iOS app
How to release a Flutter app to the App Store.docs.flutter.dev

Performance best practices
How to ensure that your Flutter app is performant.docs.flutter.dev

Reduce your app size | App quality | Android Developers
Users often avoid downloading apps that seem too large, particularly in emerging markets where devices connect to…developer.android.com

About Android App Bundles | Other Play guides | Android Developers
The Other Play guides.developer.android.com

Inspect app versions on the Latest releases and bundles page
Starting August 2021, new apps are required to publish with the Android App Bundle on Google Play. New apps larger than…support.google.com


From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automationIoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire a Flutter developer for your cross-platform Flutter mobile app project hourly or full-time as per your requirement! For any flutter-related queries, you can connect with us on Facebook, GitHub, Twitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.