Engine, Framework & Embedder #
When developing apps with Flutter, we often marvel at how the Dart code we write runs so smoothly and consistently across very different operating systems like Android, iOS, Windows, macOS, Linux, and even the Web. This magic isn’t the result of a simple code translation trick — it’s the product of a very carefully designed, structured software architecture. Flutter’s architecture is built on three main layers that collaborate closely: the Framework (written in Dart), the Engine (written in C++), and the Platform Embedder (written in each platform’s native language). By understanding the roles, boundaries, and inner workings of these three layers, you’ll be able to diagnose performance problems more accurately, design optimal native integrations, and appreciate the efficiency behind Flutter’s remarkably fast visual rendering.
The Big Picture: Flutter’s “Layer Cake” #
Flutter’s architecture is often compared to a layer cake, where each layer has a clearly defined responsibility and only directly depends on the layer beneath it. This design principle is crucial because it ensures a change in one layer won’t accidentally break another layer.
Unlike traditional cross-platform frameworks that act as wrappers or bridges for the OS’s native widgets, Flutter takes a radical approach: Direct Rendering. Flutter bypasses all native widgets and draws every UI pixel itself on a blank canvas provided by the platform. Therefore, the architecture below is designed to support that self-contained rendering consistently across all operating system environments.
flowchart TD
subgraph AppLayer["Application Layer (Dart)"]
App["Your App"]
end
subgraph FrameworkLayer["Flutter Framework (Dart)"]
direction TB
Material["Material & Cupertino (UI Kit)"]
Widgets["Widgets (UI Elements)"]
Rendering["Rendering (Layout & Paint)"]
Services["Services (Animation, Painting, Gestures)"]
Foundation["Foundation (Utilities & Binding)"]
Material --> Widgets
Widgets --> Rendering
Rendering --> Services
Services --> Foundation
end
subgraph EngineLayer["Flutter Engine (C++)"]
direction TB
DartRun["Dart Runtime & VM Isolate"]
RenderEngine["Rendering Engine (Impeller / Skia)"]
TextLayout["Text Layout (SkParagraph / HarfBuzz)"]
PlatformChannels["Platform Channels Binding"]
IOServices["System Services (File & Network I/O)"]
end
subgraph EmbedderLayer["Platform Embedder (Native)"]
direction TB
AndroidEmb["Android Embedder (Java / Kotlin + C++)"]
IOSEmb["iOS Embedder (Swift / Obj-C)"]
DesktopEmb["Desktop Embedder (C++)"]
WebEmb["Web Embedder (JS / WebAssembly)"]
end
AppLayer --> FrameworkLayer
FrameworkLayer -->|"dart:ui API"| EngineLayer
EngineLayer --> EmbedderLayer
EmbedderLayer -->|"Operating Systems (Android, iOS, macOS, Windows, Linux, Web)"| OS["OS & Hardware"]
style FrameworkLayer stroke:#0288d1,stroke-width:2px
style EngineLayer stroke:#388e3c,stroke-width:2px
style EmbedderLayer stroke:#f57c00,stroke-width:2pxThis layered architecture provides extraordinary flexibility. For example, if you want to run a Flutter app on a Tizen-based smartwatch or an embedded system in a car dashboard, you don’t need to modify the Framework (Dart) or the Engine (C++). You only need to write a new Platform Embedder capable of initializing the Engine and providing a suitable rendering surface for that device.
Layer 1: The Flutter Framework (Dart) #
The Flutter Framework is the topmost layer containing the libraries you use every day to build your app’s interface. This layer is written 100% in Dart and runs entirely on the Dart VM (in debug mode) or on AOT-compiled machine binaries (in release mode). Because it’s written in Dart, you can easily browse the framework’s internal source code directly from your IDE to learn how it works, subclass it, or even swap out specific components with custom implementations if needed.
The Framework is divided into several important sub-layers, from bottom to top:
Foundation #
This is the bottommost foundation of the framework, providing utilities, base classes, and fundamental abstractions for all libraries above it. Inside Foundation, you’ll find important classes like ChangeNotifier and ValueNotifier for change-notification-based state management, Listenable for observable objects, and DiagnosticsNode, which lets the Flutter Inspector expose the widget tree structure visually to make debugging easier. Foundation also provides service bindings like WidgetsBinding that connect the framework’s lifecycle to the engine.
Animation, Painting & Gestures #
One level above Foundation is the collection of core system services handling animation, graphics drawing, and user interaction:
- Animation: Provides the
AnimationController,Tween, andCurveclasses. The animation value update cycle is driven by theTickerobject, which synchronizes itself precisely with the device’s screen refresh rate signal through VSync. - Painting: Provides high-level abstractions for drawing on screen, including the
Canvas,Paint,Border,Decoration, andTextStyleclass definitions. These classes simplify drawing UI elements without dealing with raw pixel instructions. - Gestures: Handles converting raw touch coordinates into interactive events. This is where the Gesture Arena system lives — a gesture pattern-matching protocol that resolves conflicts when multiple gestures compete simultaneously (for example, determining whether a user’s finger touch is a simple tap, a vertical swipe to scroll the page, or a pinch to zoom an image).
Rendering #
The Rendering layer is fully responsible for the layout and painting of visual elements. This is where the render object tree (RenderObject Tree) is built and managed. Every RenderObject is tasked with computing its own physical size based on the layout constraints given by its parent, determining its coordinate position, and drawing itself onto the canvas. This layer is managed by PipelineOwner, which orchestrates the rendering execution flow from the layout phase, through layer composition, to the actual painting.
Widgets #
Widgets is the declarative layer that simplifies your interaction with the Rendering layer. As a developer, you rarely interact with RenderObject directly because its code is very imperative and complex. Instead, you use Widget — an immutable, very lightweight UI configuration description. The Widgets layer is responsible for managing widget lifecycles and translating them into an Element Tree, which acts as the logical bridge before RenderObject instances are created or updated in memory.
Material & Cupertino #
This is the topmost layer of the framework, providing ready-to-use widget libraries with specific visual design guidelines. Material implements Google’s Material Design language, while Cupertino implements Apple’s Human Interface Guidelines. Here you can use complex widgets like Scaffold, AppBar, CupertinoNavigationBar, and ElevatedButton instantly, complete with visual transitions and animation effects that match platform standards.
Here’s an example of using CustomPainter, which leverages APIs from the painting and rendering layers directly to draw custom visual elements:
import 'package:flutter/widgets.dart';
// CORRECT: Using CustomPainter for low-level drawing with Canvas
class CustomCirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF0288D1)
..style = PaintingStyle.fill;
// Drawing a circle in the middle of the canvas
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
size.width / 4,
paint,
);
}
@override
bool shouldRepaint(covariant CustomCirclePainter oldDelegate) => false;
}
All Framework Layers Are Optional As a developer, you’re free to replace or modify any part of this framework. If you don’t like the Material or Cupertino widget libraries, you can create your own widget library directly on top of theWidgetslayer. You can even bypass theWidgetslayer entirely and draw the UI directly using theRenderingorPaintinglayers if needed.
Layer 2: The Flutter Engine (C++) #
If the Flutter Framework is the brain processing the app’s visual structure, then the Flutter Engine is the mechanical heart pumping those instructions into real pixels on hardware. The Engine is written almost entirely in C++ (with a few small parts in Rust, Assembly, and Objective-C) and compiled specifically for each device CPU’s target architecture (like ARMv7 or ARM64 for smartphones, and x64 or ARM64 for desktop computers).
The Engine wraps and manages the following vital sub-systems:
Dart Runtime & Compile Toolchain #
The Engine is responsible for wrapping and running the Dart VM (Virtual Machine) that executes your app’s Dart code. The Dart VM provides a complete runtime including dynamic memory allocation and a very fast next-generation Garbage Collector (GC), specifically optimized to handle the creation and destruction of thousands of short-lived widget objects happening every second in Flutter’s render cycle. Additionally, the JIT (Just-In-Time) compiler runs inside the engine during development to power Hot Reload, while the AOT (Ahead-Of-Time) runtime executes statically compiled machine binaries when the app runs in production mode.
Rendering Subsystem (Skia vs Impeller) #
The Engine’s heaviest task is translating high-level visual instructions from the Framework into actual pixels on the GPU screen. For years, Flutter used Skia as its default 2D graphics engine. However, starting with modern Flutter versions, this rendering architecture has been replaced by Impeller, designed from scratch specifically to address Skia’s fundamental weaknesses.
Skia’s Main Problem: Shader Compilation Jank #
Skia uses an Immediate Mode Rendering approach and relies on OpenGL or Vulkan through a dynamic abstraction layer. When an app first displays a certain animation (like a new page transition), Skia has to write and compile a small GPU program called a Shader dynamically while the app runs (runtime compilation). This compilation process takes about 10 to 50 milliseconds. Since our rendering target for 60 FPS is 16.6 milliseconds per frame (and only 8.3 milliseconds for 120 FPS), this shader compilation pause exceeds the frame time budget, causing very disruptive screen stuttering (stuttering or jank).
Impeller’s Revolutionary Solution: AOT Shader Compilation #
Impeller solves this problem by moving the entire shader compilation process from the runtime phase to the app build phase (build time). Using a dedicated shader compiler called impellerc, Impeller translates all GLSL shaders into GPU shader binaries (MSL for iOS/Metal, SPIR-V for Android/Vulkan) before the app package is distributed. When the app runs, Impeller just loads those ready-made shader binaries without any further dynamic compilation.
Additionally, Impeller uses modern graphics APIs directly (like Metal on iOS and Vulkan on Android) to minimize GPU driver overhead, supports efficient multi-threaded rendering, and adopts a Retained Mode Rendering model that intelligently tracks and updates visual state changes.
| Comparison Parameter | Skia | Impeller |
|---|---|---|
| Shader Compilation Phase | Dynamic at runtime (triggers jank) | Ahead-Of-Time (AOT) at build time (jank-free) |
| Primary Graphics API | OpenGL (Legacy), Vulkan, Metal | Metal (iOS/macOS), Vulkan & GLES (Android) |
| Rendering Model | Immediate Mode Rendering | Retained Mode Rendering |
| Multi-threading Utilization | Limited to a single raster thread | Scalable across GPU threads |
| Default Platform Support | Web (CanvasKit) | iOS (Flutter 3.29+) & Android API 29+ (Flutter 3.27+) |
flowchart TD
subgraph SkiaPipeline["Skia Rendering Pipeline (Runtime Compilation)"]
direction TB
SkiaShader["GLSL Shader (Runtime)"] -->|"Dynamic GPU Compilation"| GPUCompile["On-Device Compilation (Runtime)"]
GPUCompile -->|"Frame Pause (Jank)"| GPUExecSkia["GPU Execution"]
end
subgraph ImpellerPipeline["Impeller Rendering Pipeline (Build-time Compilation)"]
direction TB
ImpellerShader["GLSL Shader (Build Time)"] -->|"AOT Compilation (Impellerc)"| Precompiled["Pre-compiled Shader (MSL/SPIR-V)"]
Precompiled -->|"Distributed in APK/IPA"| LoadReady["Instant GPU Loading"]
LoadReady -->|"Smooth Execution (Jank-Free)"| GPUExecImpeller["GPU Execution"]
end
style SkiaPipeline stroke:#f44336,stroke-width:2px
style ImpellerPipeline stroke:#4caf50,stroke-width:2pxOther Engine Components #
- Text Layout & Shaping: Uses a modern text sub-system called
SkParagraphthat integrates the HarfBuzz library for Unicode text shaping and ICU for orthography analysis. This sub-system ensures complex fonts and scripts (like Arabic, Japanese, or Devanagari) render correctly. - File & Network I/O: Provides low-level async I/O APIs to Dart through internal C++ libraries.
- Accessibility (A11y): Provides a bridge to expose the Framework’s semantic tree structure (Semantic Tree) to native platform screen readers like TalkBack on Android or VoiceOver on iOS.
dart:ui: The Bridge to the Framework #
The Engine exposes all of its C++ functionality to Dart code through a built-in library called dart:ui. This library contains low-level bindings for Canvas, Paragraph, and PictureRecorder. Although you as an app developer are advised to always use the Framework’s built-in Widgets, you can access dart:ui directly if you want to build experimental UI systems or do extreme graphics manipulation beyond normal limits.
Let’s look at how dart:ui can be used directly without relying on the widget system at all:
import 'dart:ui' as ui;
// CORRECT: Using the dart:ui API directly to draw onto a Scene object
void drawRawScene(ui.Canvas canvas) {
final paint = ui.Paint()
..color = const ui.Color(0xFFE91E63)
..style = ui.PaintingStyle.fill;
// Drawing a pink rectangle directly at raw screen coordinates
canvas.drawRect(
const ui.Rect.fromLTWH(50.0, 50.0, 200.0, 150.0),
paint,
);
}
// Note: This is the lowest-level interaction between Dart and the C++ Engine.
// The Framework wraps this kind of code inside CustomPainter for safer management.
Layer 3: The Platform Embedder (Native) #
Every time a Flutter app runs, the device’s operating system doesn’t interact directly with the Dart VM or the C++ Engine. Instead, the OS launches a standard native app acting as the app’s host. This host is called the Platform Embedder.
The Platform Embedder is written in the target platform’s native programming language:
- Android: written in Java or Kotlin, with C++ (JNI) glue.
- iOS & macOS: written in Objective-C or Swift, with a Metal API layer.
- Windows: written in C++ (Win32 API).
- Linux: written in C++ (GTK+ API).
- Web: written in JavaScript and WebAssembly (Wasm).
The Embedder acts as the physical bridge between the Flutter Engine and the hardware, with the following main responsibilities:
Providing the Rendering Surface #
The Embedder is responsible for creating a hardware-accelerated visual area (Metal layer on iOS, SurfaceView or TextureView on Android, GLFW/Win32 Window on desktop) and handing it to the Flutter Engine. The pixels rasterized by the Engine are presented to the screen through this visual area.
Managing Threading & Task Runners #
To keep rendering performance at 60 FPS or above without lag, the Flutter Engine relies on a very strict threading model. The Platform Embedder allocates and manages the following four types of Task Runner (execution threads):
- UI Task Runner: Used to execute your main Dart VM isolate. This is where the widget tree is built, layout is computed, state is updated, and app logic runs. This thread produces the Layer Tree (graphics scene) describing what needs to be drawn.
- Raster Task Runner (GPU Runner): This thread takes the Layer Tree from the UI Task Runner and translates it into a series of GPU instructions (Metal/Vulkan commands) using Impeller/Skia. This thread runs in parallel with the UI Task Runner so the next UI computation isn’t blocked by the previous frame’s rasterization process.
- IO Task Runner: Used for heavy background operations, like loading and decoding images from internal storage, reading large files, or downloading assets from the internet. After this thread decodes assets into raw GPU textures, they’re sent directly to the Raster Task Runner for rendering without burdening the UI thread.
- Platform Task Runner: The main thread of the native operating system (Android/iOS native UI thread). This thread handles interactions with OS APIs, processes Platform Channel messages, manages lifecycle events, and receives physical input events (like touches or keyboard presses).
flowchart TD
subgraph Embedder["Platform Embedder (Thread Manager)"]
direction TB
PlatformRunner["Platform Task Runner (Main Native Thread)"]
UIRunner["UI Task Runner (Dart VM Isolate)"]
RasterRunner["Raster Task Runner (GPU Commands)"]
IORunner["IO Task Runner (Asset Loading & I/O)"]
end
PlatformRunner <-->|"Send Touches / Platform Channels"| UIRunner
UIRunner -->|"Generate Layer Tree (Scene)"| RasterRunner
RasterRunner -->|"Send to GPU"| GPU["GPU Hardware"]
IORunner -->|"Upload Ready-Made Textures"| RasterRunner
UIRunner <-->|"Delegate Heavy Tasks"| IORunner
style PlatformRunner stroke:#0288d1,stroke-width:2px
style UIRunner stroke:#4caf50,stroke-width:2px
style RasterRunner stroke:#e91e63,stroke-width:2px
style IORunner stroke:#ff9800,stroke-width:2pxTranslating Physical Input (Input Handling) #
The Embedder captures finger touch events, mouse coordinates, scroll movements, and keyboard input from the native operating system. These events are packaged into a uniform data packet called PointerDataPacket and sent to the Flutter Engine via dart:ui, which then distributes them to the Gestures layer in the Framework.
Managing the App Lifecycle #
When the operating system tells the host that the app has moved to the background (backgrounded), the Platform Embedder forwards this lifecycle signal to the Flutter Engine, allowing the Dart VM to do extra memory cleanup (garbage collection) or automatically pause app timers.
Module Integration: Add-to-App & FlutterEngineGroup #
Because the Platform Embedder is designed as a self-contained container, you don’t have to migrate your entire native app to Flutter at once. You can package a Flutter screen as a library module (.aar or framework) and add it to an existing native Android/iOS app.
For scenarios where a native app has several separate Flutter modules (e.g., the profile page and payment page use Flutter, while the main pages are native), instantiating multiple Flutter Engines raw would consume a lot of RAM (about 30-40 MB per engine). To solve this, Flutter provides FlutterEngineGroup. Through FlutterEngineGroup, multiple embedder instances can share the same Dart VM, internal caches, and runtime isolates, so memory usage for the second and subsequent engines is only around 1-2 MB. This enables very efficient native integration.
Cross-Layer Workflow and Interaction #
To understand how these three big layers dance together in synchronized harmony, let’s trace the lifecycle of a single screen tap event, from the moment a user’s finger touches the physical screen until new UI pixels are drawn:
- Physical Touch: The user taps a button on the smartphone screen.
- Hardware Detection: The screen’s digitizer panel detects the touch and sends a hardware interrupt signal to the Operating System.
- Embedder Delivery: The native Operating System (Android/iOS) forwards the touch event to the Platform Embedder (as a
MotionEventon Android orUITouchon iOS). - Engine Packaging: The Platform Embedder translates the raw touch coordinates into a standardized
PointerDataPacketobject, then sends it to the Flutter Engine. - Gesture Resolution: The Engine forwards the touch packet to the Flutter Framework through the
dart:uibinding. The Gestures layer runs hit-testing and activates the Gesture Arena to determine that the user is performing a tap gesture at the button’s coordinates, then triggers theonPressedcallback you defined. - State Change: Inside the
onPressedcallback, your Dart code changes a state variable (e.g., changing the button color) and triggers a state update by callingsetState(). - Frame Scheduling: The Framework marks that widget element as dirty and calls
WidgetsBinding.scheduleFrame()to tell the Engine there’s a visual change that needs redrawing. - VSync Synchronization: The Engine requests a VSync signal from the Platform Embedder. The Embedder forwards this request to the OS. When the next screen VSync cycle is triggered by the OS (e.g., at 16.6ms intervals for a 60Hz screen), the OS sends a signal back to the Embedder, which then triggers a render event on the Engine.
- Drawing Callbacks: The Engine calls the
onBeginFrameandonDrawFramefunctions on the Framework. The Framework executes the widget rebuild process, runs the top-down layout calculation to compute the button’s new dimensions, and records new visual painting instructions into a Layer Tree object. - Scene Delivery: The Framework sends the Layer Tree containing the button’s new coordinates and drawing instructions to the Engine using the
ui.SceneBuilderclass. - GPU Rasterization: The Engine takes the Layer Tree, references the shader binaries previously compiled by Impeller, and rasterizes those visual instructions into raw GPU instructions (Metal/Vulkan/OpenGL commands) inside the Raster Task Runner.
- Screen Presentation: The GPU instructions are executed by the device’s graphics card to write the new frame into the rendering surface’s framebuffer. The Platform Embedder asks the operating system to swap the visual buffers, instantly presenting the button’s new, color-changed pixels to the user’s physical screen.
sequenceDiagram
autonumber
actor User as User
participant OS as Operating System
participant Emb as Platform Embedder
participant Eng as Flutter Engine
participant FW as Flutter Framework
User->>OS: Tap screen
OS->>Emb: Forward coordinate input event
Emb->>Eng: Convert coordinates & send PointerDataPacket
Eng->>FW: Send event to Gesture Arena (dart:ui)
FW->>FW: Detect gesture & call onTap() callback
FW->>FW: Update State (setState)
FW->>FW: Mark Widget dirty & schedule new frame
FW->>Eng: Request new VSync frame
Eng->>Emb: Request VSync from OS
OS-->>Emb: VSync Signal (Frame Trigger)
Emb-->>Eng: Trigger new frame render
Eng-->>FW: Drawing callbacks (onBeginFrame & onDrawFrame)
FW->>FW: Rebuild, Layout, & Paint (Generate Layer Tree)
FW->>Eng: Send Scene/Layer Tree via SceneBuilder
Eng->>Eng: Rasterize Scene with Impeller/Skia
Eng->>Emb: Render frame to platform surface buffer
Emb->>OS: Present buffer to GPU screen
OS-->>User: UI updates (~16.6ms / 60 FPS)Summary #
- Three Main Layers — Flutter’s architecture consists of the Framework (Dart), Engine (C++), and Platform Embedder (Native), each with neatly isolated responsibility boundaries.
- Direct Rendering Approach — Unlike traditional cross-platform frameworks, Flutter draws every user interface pixel itself on a blank canvas without converting its code into the OS’s built-in native widgets.
- Framework Sub-systems — The Framework layer flows top to bottom: Material/Cupertino (Design System), Widgets (Declarative Elements), Rendering (Layout & Paint), Animation/Painting/Gestures, and Foundation (Base Utilities).
- Engine Sub-systems — The Engine layer is written in C++ to manage Dart VM runtime execution, pixel rendering (Impeller/Skia), text shaping (SkParagraph), and low-level I/O operations.
- Impeller vs Skia — Impeller replaces Skia by moving the shader compilation process from runtime to build time (AOT), completely solving the shader compilation jank (stuttering) problem on modern devices.
- dart:ui Binding — The Engine exposes its low-level graphics functionality to the framework through the
dart:uilibrary, which acts as the main communication bridge.- Platform Embedder — The native layer acting as the app’s host, managing thread allocation (UI, Raster, IO, Platform), handling physical touch input, and app lifecycle.
- Portability & Modules — Through the Platform Embedder, Flutter can be integrated as a module (Add-to-App) into existing native apps and have its memory usage optimized using
FlutterEngineGroup.