Back to blog

Flutter: How Dart Paints Every Pixel

flutterdartmobilecross-platformgoogle
Flutter: How Dart Paints Every Pixel

Series: Cross-Platform Mobile Development
Previous: React Native: How JavaScript Renders Native UIs
Next: Ionic vs React Native vs Flutter: When to Use What


Flutter takes the most radical approach of the three frameworks: don't use the platform's UI components at all. No WebView. No native buttons, text fields, or scroll views. Flutter draws every single pixel itself, using its own rendering engine.

This sounds like reinventing the wheel. But this architectural decision gives Flutter something no other cross-platform framework has: pixel-perfect consistency across every platform and complete control over every visual detail.

Why Dart? The Controversial Choice

When Google announced Flutter, the biggest pushback was: "Why Dart? Nobody uses Dart."

The answer lies in Flutter's technical requirements. They needed a language that could do something no mainstream language could:

Both AOT and JIT Compilation

Development (JIT):

Production (AOT):

  • During development: Dart uses JIT (Just-In-Time) compilation, enabling Flutter's famous stateful hot reload — change code, see the result in under a second, with app state preserved
  • In production: Dart uses AOT (Ahead-Of-Time) compilation to native ARM machine code — no interpreter, no VM, no JIT warmup, just raw native performance

JavaScript can't do AOT compilation to native code (it needs a VM). Swift/Kotlin can do AOT but don't have JIT for hot reload. Dart does both.

No Lock Contention

Dart is single-threaded with an event loop (like JavaScript), but it also supports isolates — true parallel execution without shared memory.

Why does this matter for a UI framework?

  • Predictable frame timing — no garbage collection pauses caused by lock contention between threads
  • Smooth 60/120fps — the UI thread is never blocked by locks
  • Parallel computation — heavy tasks run in isolates without affecting the UI

Object-Oriented with Sound Null Safety

class User {
  final String name;
  final String? email;  // nullable — must handle null explicitly
  final int age;
 
  const User({required this.name, this.email, required this.age});
}
 
// The compiler enforces null safety
void greet(User user) {
  print('Hello, ${user.name}');
 
  // This would be a compile error:
  // print(user.email.length);  // Error: email might be null
 
  // Must handle null:
  if (user.email != null) {
    print('Email: ${user.email!.length}');
  }
}

Dart's syntax is familiar to Java/C#/TypeScript developers, making the learning curve manageable despite being a "new" language.

The Rendering Pipeline: Three Trees

Flutter's most important architectural concept is the three-tree system: Widget tree → Element tree → RenderObject tree.

Widget Tree (What You Write)

Widgets are immutable descriptions of UI. They're cheap to create and discard:

class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
 
  const ProfileCard({required this.name, required this.role});
 
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Text(role, style: const TextStyle(color: Colors.grey)),
          ],
        ),
      ),
    );
  }
}

Key insight: Widgets don't render anything. They're just configuration objects. Flutter creates and destroys thousands of widgets per frame — they're designed to be disposable.

Element Tree (The Glue)

Elements are the persistent objects that manage the widget-to-render relationship. When Flutter rebuilds the widget tree (e.g., after setState()), it doesn't destroy and recreate everything. Instead, it walks the element tree and asks: "Can I reuse you with this new widget?"

RenderObject Tree (The Painters)

RenderObjects do the actual work: layout and painting. Each RenderObject knows:

  • How to calculate its own size given constraints from its parent
  • How to paint itself to a canvas
  • How to handle hit testing (touch/click detection)

How a Frame Renders

When something changes (user taps a button, data arrives), here's what happens in a single frame (~16ms for 60fps):

The Rendering Engine: Skia → Impeller

This is where Flutter fundamentally differs from every other framework. While Ionic uses the browser engine and React Native uses native UI components, Flutter paints every pixel itself.

Skia (The Original Engine)

Skia is Google's open-source 2D graphics library — the same engine that powers:

  • Chrome (browser rendering)
  • Android (system UI)
  • Firefox (partially)

Flutter used Skia from the beginning. Skia takes high-level drawing commands ("draw a rectangle with rounded corners at these coordinates with this gradient") and turns them into GPU instructions.

Impeller (The New Engine)

Flutter's team built Impeller specifically for Flutter to solve Skia's biggest mobile problem: shader compilation jank.

The problem with Skia on mobile:

Skia compiles GPU shaders at runtime — the first time a visual effect appears, there's a brief stutter while the shader compiles. This caused a "first-time jank" that was noticeable and hard to fix.

How Impeller solves it:

Impeller pre-compiles all shaders at build time. When your app runs, every visual effect is already compiled and ready — zero shader compilation jank.

Results:

  • Worst-frame performance improved dramatically (no more random stutters)
  • Consistent 120fps on modern devices
  • Lower power consumption — more efficient GPU usage

Impeller is the default on iOS since Flutter 3.16 and on Android since Flutter 3.22.

Platform Channels: Talking to Native Code

Flutter can't draw everything. Sometimes you need platform-specific APIs — camera, Bluetooth, biometrics, payments. Platform channels are the bridge:

// Dart side
import 'package:flutter/services.dart';
 
class BatteryService {
  static const platform = MethodChannel('com.example.app/battery');
 
  static Future<int> getBatteryLevel() async {
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      return result;
    } on PlatformException catch (e) {
      throw Exception('Failed to get battery level: ${e.message}');
    }
  }
}
// iOS side (Swift)
import Flutter
 
class BatteryPlugin {
    static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(
            name: "com.example.app/battery",
            binaryMessenger: registrar.messenger()
        )
        channel.setMethodCallHandler { call, result in
            if call.method == "getBatteryLevel" {
                let device = UIDevice.current
                device.isBatteryMonitoringEnabled = true
                let level = Int(device.batteryLevel * 100)
                result(level)
            } else {
                result(FlutterMethodNotImplemented)
            }
        }
    }
}

Platform channels use binary message passing — more efficient than JSON serialization (used by old React Native bridge) but still asynchronous. For most use cases, the latency is negligible.

Flutter also supports FFI (Foreign Function Interface) for calling C/C++ code directly from Dart — useful for performance-critical native operations.

State Management: The Ecosystem

Flutter doesn't prescribe a state management solution. The community has developed several approaches:

// 1. setState (built-in, simple)
class CounterWidget extends StatefulWidget {
  @override
  State<CounterWidget> createState() => _CounterState();
}
 
class _CounterState extends State<CounterWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => setState(() => count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}
 
// 2. Riverpod (most popular in 2026)
final counterProvider = StateProvider<int>((ref) => 0);
 
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('Count: $count');
  }
}
 
// 3. BLoC (popular in enterprise)
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

The most popular options in 2026:

  • Riverpod — compile-safe, flexible, excellent testing support
  • BLoC — event-driven, great for complex business logic and enterprise apps
  • Provider — simple, built on InheritedWidget
  • GetX — controversial but popular for rapid prototyping

Beyond Mobile: Flutter Everywhere

Flutter now targets six platforms from a single codebase:

Flutter Web

Flutter on the web is interesting — it renders to a <canvas> element (CanvasKit mode) or generates DOM elements (HTML mode). This means:

  • CanvasKit: Pixel-perfect rendering, same as mobile. But large download size (~2MB WASM) and no SEO
  • HTML mode: Smaller download, better SEO, but not pixel-perfect

Flutter Web is best for web apps (dashboards, tools, admin panels), not websites (where SEO and content matter).

Flutter Desktop

Flutter desktop apps are production-ready on Windows, macOS, and Linux. Notable examples:

  • Canonical uses Flutter for the Ubuntu installer
  • Google uses Flutter for internal tools
  • Superlist — task management app (macOS, Windows, mobile)

When Flutter Is the Right Choice

Perfect Fit ✅

  • Custom, branded UIs — your app should look identical on every platform, not "native" to each
  • Animation-heavy apps — complex transitions, physics-based interactions, custom painting
  • Startup/MVP with design vision — ship a polished, unique product fast
  • Multi-platform (beyond mobile) — target mobile, web, and desktop from one codebase
  • Teams willing to learn Dart — the investment pays off in productivity

Think Twice ⚠️

  • You need native platform look-and-feel — Flutter can mimic iOS/Material, but it's still Flutter drawing it, not the OS
  • Heavy web content in the app — embedding WebViews in Flutter works but isn't as seamless as Ionic
  • Your team is heavily invested in React/TypeScript — React Native would be a smoother transition
  • Simple wrapper around a web app — use Ionic/Capacitor
  • Apps that must use OS-native UI controls — accessibility and platform conventions matter more than consistency
  • Teams that can't afford Dart learning curve — if time-to-market is critical and the team knows JavaScript, consider alternatives

Who Uses Flutter?

  • Google Pay — the entire app rebuilt in Flutter
  • BMW — My BMW app
  • Alibaba — Xianyu (used goods marketplace, 200M+ users)
  • Nubank — Brazil's largest digital bank (80M+ customers)
  • eBay Motors — vehicle marketplace
  • PUBG Mobile — companion app
  • Toyota — infotainment system
  • Canonical — Ubuntu desktop installer

The 2026 State of Flutter

Flutter continues to evolve rapidly:

  • Impeller is now the default on both iOS and Android — shader jank is eliminated
  • Dart 3.x — pattern matching, sealed classes, records, enhanced enums
  • Flutter 3.x — improved web rendering, desktop stability, platform views
  • pub.dev — 50,000+ packages in the Dart/Flutter ecosystem
  • Firebase integration — first-class support for Google's backend services
  • Flame — a mature 2D game engine built on Flutter

Key Takeaways

  1. Flutter draws every pixel itself — no WebView, no native components. Complete control over rendering.
  2. Dart was chosen for technical reasons — AOT + JIT compilation, sound null safety, and single-threaded predictability
  3. Three trees (Widget → Element → RenderObject) power the rendering pipeline, enabling efficient diffing and minimal repaints
  4. Impeller replaces Skia as the rendering engine, eliminating shader compilation jank for consistent 120fps
  5. Platform channels connect Dart to native APIs (camera, Bluetooth, etc.) via asynchronous binary message passing
  6. Flutter targets 6 platforms from one codebase — but mobile remains the strongest target
  7. The trade-off is clear: pixel-perfect consistency and custom UIs, at the cost of learning Dart and losing native platform look-and-feel

Flutter's bet is that owning the rendering pipeline is worth the cost of reimplementing platform UI conventions. For apps where brand consistency and visual polish matter more than native platform fidelity — that bet pays off handsomely.


Series: Cross-Platform Mobile Development
Previous: React Native: How JavaScript Renders Native UIs
Next: Ionic vs React Native vs Flutter: When to Use What

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.