Flutter is Google’s open-source UI toolkit for building apps for mobile, web, desktop, and embedded targets from one Dart codebase. It does not wrap native widgets. It draws every pixel itself through its own rendering engine. That engine is now Impeller, and the 2026 releases make it the default on most platforms.
Quick Takeaways
- Flutter is a framework, and Dart is the language it uses. You write widgets in Dart and Flutter renders them on its own canvas.
- Flutter 3.47 reached stable on August 12, 2026 with Dart 3.13, and made Impeller the default renderer on macOS, Windows, and Linux.
- Impeller pre-compiles shaders at build time, which removes first-run shader jank.
- Pick Flutter when you need pixel-identical UI across platforms and one team. Pick native when you need deep, platform-specific APIs on day one.
| Question | Short Answer |
|---|---|
| Language | Dart (AOT in release, JIT in debug) |
| Rendering | Impeller (Metal, Vulkan), Skia-based on web |
| Targets | iOS, Android, web, Windows, macOS, Linux, embedded |
| UI model | Declarative, widget tree, immutable configuration |
| Best for | Brand-heavy UI, MVPs, multi-platform products |
What Flutter Actually Is
Flutter has three layers:
- Framework (Dart): widgets, rendering, animation, gestures.
- Engine (C++): text layout, graphics, the Dart runtime. Impeller lives here.
- Embedder (per platform): the thin native shell that hosts the engine.
Everything on screen is a Widget. A widget is an immutable description of UI. Flutter diffs the widget tree against the element tree and repaints only what changed.
Minimal Flutter App
import 'package:flutter/material.dart';
void main() => runApp(const CounterApp());
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CounterPage());
}
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
// setState marks this subtree dirty; Flutter rebuilds only what changed.
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Count: $_count')),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
Each tap calls setState(), which schedules a rebuild of _CounterPageState. The const constructors let Flutter skip rebuilding unchanged widgets. The output is a centered counter that increments on every tap.
Impeller: The Rendering Engine Explained
Impeller replaces Skia as Flutter’s renderer. Skia compiled shaders at runtime, on first use. That caused visible stutter the first time an animation ran.
Impeller takes a different route:
- It compiles a small, fixed set of shaders ahead of time, during the build.
- It targets modern graphics APIs directly: Metal on Apple platforms and Vulkan on Android, Windows, and Linux.
- It makes frame cost predictable, which makes profiling easier.
Where Impeller Stands in 2026
| Platform | Impeller Status | Notes |
|---|---|---|
| iOS | Default | Default since Flutter 3.29 |
| Android | Default | Vulkan devices, with OpenGLES fallback |
| macOS | Default | Metal backend |
| Windows / Linux | Default | Vulkan backend, new in 3.47 |
| Web | Not applicable | Uses CanvasKit or Skwasm (WebAssembly) |
The Android opt-out was deprecated in Flutter 3.38, and Impeller runs by default on API 29 and above. On desktop, Impeller targets Metal on macOS and Vulkan on Windows and Linux, and the docs say the ability to switch it off will be removed in a future release.
Testing Impeller Regressions on Desktop
# Run with Impeller disabled to compare rendering output
flutter run -d macos --no-enable-impeller
# Run with the default renderer
flutter run -d macos
Use the opt-out flag only to isolate a rendering bug. File the issue, then return to the default. The opt-out path is being removed.
Dart: The Language Behind Flutter
Dart is a statically typed, null-safe, object-oriented language. It compiles ahead-of-time to native ARM or x64 code for release builds, and to JavaScript or WebAssembly for the web. In debug mode it uses a JIT compiler, which enables stateful hot reload.
Dot Shorthands (Dart 3.10)
Dart 3.10 shipped with Flutter 3.38 on November 12, 2025 and introduced dot shorthands. The compiler infers the enum or static member from context.
// Before Dart 3.10
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('Title'), Text('Subtitle')],
);
// Dart 3.10+ with dot shorthands
Column(
mainAxisAlignment: .center,
crossAxisAlignment: .start,
children: [Text('Title'), Text('Subtitle')],
);
The compiled output is identical. You write less boilerplate and keep full type safety.
Sealed Classes and Pattern Matching
Model UI state with sealed classes. The analyzer then enforces exhaustive switch expressions.
sealed class LoadState<T> {
const LoadState();
}
final class Loading<T> extends LoadState<T> {
const Loading();
}
final class Loaded<T> extends LoadState<T> {
const Loaded(this.data);
final T data;
}
final class Failed<T> extends LoadState<T> {
const Failed(this.message);
final String message;
}
Widget render(LoadState<List<String>> state) {
// The compiler errors if a subtype is missing.
return switch (state) {
Loading() => const CircularProgressIndicator(),
Loaded(:final data) => ListView(children: data.map(Text.new).toList()),
Failed(:final message) => Text('Error: $message'),
};
}
If you add a fourth subtype later, every unhandled switch fails to compile. That catches missing UI states before runtime.
Concurrency With Isolates
Dart is single-threaded per isolate. Isolates do not share memory. Move heavy work off the UI isolate with Isolate.run().
import 'dart:convert';
import 'dart:isolate';
Future<List<Map<String, dynamic>>> parseLargeJson(String raw) {
// Parsing a multi-megabyte payload on the UI isolate drops frames.
return Isolate.run(() {
final decoded = jsonDecode(raw) as List<dynamic>;
return decoded.cast<Map<String, dynamic>>();
});
}
The parse runs on a separate isolate, and the result is copied back when it finishes. The UI isolate keeps rendering at 60 or 120 fps in the meantime.
What Changed in the Language Recently
The Dart team stopped work on macros entirely in early 2025. Code generation with build_runner remains the standard route for serialization. Newer Dart releases added primary constructors and build hooks, which let native build logic live in Dart. Check the Dart changelog for your exact SDK version before relying on them.
Flutter vs React Native vs Native
| Criteria | Flutter | React Native | Native (Swift/Kotlin) |
|---|---|---|---|
| Language | Dart | JavaScript / TypeScript | Swift, Kotlin |
| UI rendering | Own engine (Impeller) | Native widgets | Native widgets |
| UI consistency across OS | Pixel-identical | Platform-native look | Platform-native look |
| Runtime performance | Near-native, AOT | Good, depends on bridge/architecture | Best |
| Platform API access | Via plugins / FFI | Via native modules | Direct |
| Desktop support | Stable (Win/macOS/Linux) | Community-driven | Separate codebases |
| Hot reload | Stateful | Fast Refresh | Limited |
| Learning curve | Moderate (new language) | Low for web devs | Steep (two stacks) |
Flutter wins when design fidelity and a single team matter most. React Native wins when you already have a large TypeScript codebase. Native wins when you depend on brand-new OS features.
Why Developers Choose Flutter
- One codebase, many targets. Share UI and business logic across six platforms.
- Stateful hot reload. Edit code and see the change in about a second, with app state preserved. Hot reload on web became stable in Flutter 3.35.
- Predictable rendering. Because Flutter owns the canvas, a widget looks the same on a Pixel and an iPhone.
- Strong tooling. DevTools, widget previews, and the Dart and Flutter MCP server for AI-assisted refactors.
- Open governance direction. Material and Cupertino are scheduled for formal deprecation in the core SDK in the November 2026 stable release, moving to standalone packages, with an automated migration command.
Migrating Material and Cupertino
# Automated fix that rewrites imports to the standalone packages
dart fix --apply --code=migrate_design_widgets
Run this on a clean git branch. Review the diff, then run your test suite.
Best Practices: Anti-Patterns vs Clean Code
Anti-Pattern 1: Rebuilding Everything With a Giant build()
// BAD: one huge build method; every setState rebuilds all of it
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
int _likes = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Image.network('https://example.com/banner.png'),
Text('A long static biography...'),
ElevatedButton(
onPressed: () => setState(() => _likes++),
child: Text('Likes: $_likes'),
),
]);
}
}
// GOOD: isolate the changing part into its own widget
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return const Column(children: [
_Banner(),
_Bio(),
_LikeButton(), // only this subtree rebuilds on tap
]);
}
}
class _LikeButton extends StatefulWidget {
const _LikeButton();
@override
State<_LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<_LikeButton> {
int _likes = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() => _likes++),
child: Text('Likes: $_likes'),
);
}
}
The refactor confines rebuilds to _LikeButton. Static widgets stay const and cost nothing per frame.
Anti-Pattern 2: Using BuildContext Across an Async Gap
// BAD: context may be invalid after the await
Future<void> save(BuildContext context) async {
await repository.save();
Navigator.of(context).pop(); // lint: use_build_context_synchronously
}
// GOOD: check that the widget is still mounted
Future<void> save(BuildContext context) async {
await repository.save();
if (!context.mounted) return;
Navigator.of(context).pop();
}
The context.mounted check prevents errors when the user leaves the screen during the await.
Performance Checklist
| Technique | Benefit | Cost |
|---|---|---|
const constructors |
Skips rebuilds | None |
ListView.builder |
Lazy item creation, O(visible) memory | Slightly more code |
Isolate.run() |
Keeps UI thread free | Data is copied between isolates |
RepaintBoundary |
Limits repaint area | Extra layer memory |
| Profile mode builds | Realistic frame timings | Slower iteration |
Always measure in profile mode on a physical device. Debug builds run on the JIT and do not reflect release performance.
When Flutter Is the Wrong Choice
- Apps that depend on day-one access to new platform APIs, such as the latest widget or camera frameworks.
- Content-heavy sites that need SEO-first server rendering. Flutter web suits app-like experiences better than document-like pages.
- Teams with deep existing native codebases and no appetite for a second UI layer.
Frequently Asked Questions
Is Flutter still worth learning in 2026?
Yes. Flutter ships four stable releases a year and now defaults to Impeller on iOS, Android, and desktop. It remains a strong choice for multi-platform apps, and Dart is easy to pick up for developers who know Java, Kotlin, Swift, or TypeScript.
What is Impeller in Flutter?
Impeller is Flutter’s rendering engine. It replaces Skia on supported platforms and pre-compiles shaders at build time, which removes first-run animation jank. It uses Metal on Apple platforms and Vulkan on Android, Windows, and Linux.
Is Flutter better than React Native?
Neither wins in every case. Flutter offers consistent UI and one rendering engine across platforms. React Native fits teams already invested in JavaScript or TypeScript and wanting native-looking widgets. Evaluate both with a prototype of your hardest screen.
Do I need to learn Dart to use Flutter?
Yes. Flutter apps are written in Dart. The syntax is close to Java, C#, and TypeScript, so most developers become productive within a week or two.




