Skip to content
ExponaBLOG
FLUTTER · DAY 009 / 100

Use const constructors: Flutter best practices explained

A free performance win.

Expona Admin7 min read

Use const constructors: Flutter best practices explained

Const constructors are one of the easiest performance wins you can implement in Flutter, yet many teams leave them on the table. By marking widgets and objects as const, you're telling Flutter's engine to reuse instances instead of recreating them on every rebuild—and the analyzer can guide you to every place where this matters. In this article, we'll walk through why const constructors work, how to apply them systematically, and how to enforce them across your codebase with lint rules.

Understanding const widgets and instance creation

When you create a widget without const, Flutter treats it as a new instance every time the code runs, even if nothing about it has changed. This matters because Flutter's widget tree comparison is identity-based at the const level. Two widget instances with identical properties are still considered different objects in memory.

With const, you're declaring that a widget is immutable and its properties will never change. Flutter then creates that widget instance once and reuses it across rebuilds. The Dart compiler optimizes this further by pooling const instances—multiple const declarations with identical arguments point to the same object in memory.

Here's a practical example:

dart
// Without const: new instance each time parent rebuilds
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Home'),
        ),
        body: Column(
          children: [
            Icon(Icons.star),  // New instance on every build
            SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

// With const: single instance reused across rebuilds
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Home'),
        ),
        body: Column(
          children: [
            Icon(Icons.star),  // Same instance every rebuild
            SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

This might seem like a small detail, but when your build method runs dozens of times per second (during animations, scrolling, or state changes), reusing instances saves allocation time, garbage collection pressure, and CPU cycles.

How Flutter skips rebuilding identical const widgets

Flutter's widget comparison uses a process called "canUpdate." When the framework decides whether to rebuild a widget, it first checks if the old and new widget instances are the same object in memory. If they are, Flutter skips all rebuild logic for that subtree.

This is where const makes the biggest impact. Let's trace through a real scenario:

dart
class CounterScreen extends StatefulWidget {
  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const AppBar(  // const: reused, never rebuilt
        title: Text('Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),  // const
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headlineMedium,  // not const, changes
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => counter++),
        child: const Icon(Icons.add),  // const: reused every rebuild
      ),
    );
  }
}

Each time the counter increments and setState triggers a rebuild, the AppBar and Icon are not recreated. Flutter compares them by identity, sees they're the same const instance, and skips the entire build logic for those subtrees. Only the Text showing the counter value rebuilds, because it depends on mutable state.

Over a typical user session with hundreds or thousands of rebuilds, this compounds into measurable savings in memory allocation and CPU time—especially on lower-end devices.

Adding const wherever the analyzer suggests

The Dart analyzer knows exactly where const is safe to use. It tracks immutability requirements and will highlight every location where const can be applied. Most IDEs (VSCode, Android Studio, IntelliJ) show these suggestions as yellow bulb warnings or inline hints.

The general rule is straightforward: if all constructor arguments are const-eligible (literals, other const objects, or final fields that are themselves const-eligible), the instance can be const.

Here's a checklist for when to apply const:

  • All widget constructor arguments are compile-time constants (strings, numbers, booleans)
  • All arguments are other const objects or final fields initialized with const values
  • No arguments depend on runtime state, context, or mutable data
  • The constructor is declared with const (not all are)

In practice, you should trust your analyzer. If it suggests const, apply it. If a build method argument depends on state or context, you can't use const—but the outer widget tree often still can be.

dart
class ProfileCard extends StatelessWidget {
  final String name;
  final String email;

  // Constructor is const-eligible
  const ProfileCard({
    required this.name,
    required this.email,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),  // const: EdgeInsets is immutable
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(name),  // depends on state, not const
            const SizedBox(height: 8),  // const: fixed spacing
            Text(email),  // depends on state, not const
            const Divider(),  // const: static divider
          ],
        ),
      ),
    );
  }
}

Notice that we mark the constructor itself as const, allowing instances of ProfileCard to be const when passed immutable arguments. The analyzer will flag places where you instantiate ProfileCard with const arguments but forget to use the const keyword.

Enforcing const with lint rules

Manual attention drifts over time. The best approach is to enforce const usage with lint rules in your analysis_options.yaml file. These rules catch const violations during development, preventing regressions.

The most important rules are:

  • prefer_const_constructors: Flag every const-eligible widget construction that's missing const
  • prefer_const_literals_to_create_immutables: Flag collections and properties that could use const literals
  • avoid_unnecessary_containers: Removes unnecessary wrappers that hide const optimization opportunities

Here's a recommended configuration:

dart
linter:
  rules:
    - prefer_const_constructors
    - prefer_const_literals_to_create_immutables
    - prefer_const_declarations
    - avoid_unnecessary_containers

With these rules enabled, your analyzer will warn on every missed opportunity. Your IDE can often auto-fix these with a single action. Over time, your team builds muscle memory—const becomes automatic, not deliberate.

For larger teams, you can enforce these rules as part of continuous integration by failing builds when analyzer violations exceed a threshold, or by requiring zero violations in new code.

Common mistakes

Mistake 1: Applying const to widgets with mutable children. You can't mark a Column const if its children depend on state. Instead, keep children as a non-const field or parameter, and rebuild the parent normally.

dart
// Wrong: children depend on state
const Column(
  children: [
    Text('Count: $count'),  // Error: can't use dynamic data in const
  ],
)

// Right: extract to a variable or method
Column(
  children: [
    Text('Count: $count'),
  ],
)

Mistake 2: Forgetting to mark constructor parameters as const-required. If you want callers to pass const objects, make parameters const-compatible by not using any mutable defaults or runtime operations.

dart
// Weak: allows non-const usage
class Badge extends StatelessWidget {
  final String label;
  Badge({required this.label});  // Can be called with dynamic label

  @override
  Widget build(BuildContext context) {
    return Container(child: Text(label));
  }
}

// Stronger: const constructor, can be used const
class Badge extends StatelessWidget {
  final String label;
  const Badge({required this.label});  // Enables const instantiation

  @override
  Widget build(BuildContext context) {
    return Container(child: Text(label));
  }
}

Mistake 3: Ignoring analyzer warnings as noise. Every analyzer suggestion about const is actionable. Treating them as warnings to ignore means leaving performance on the table and accumulating technical debt. Enforce them with lint rules instead.

Key takeaways

  • Const widgets are instantiated once and reused across rebuilds, avoiding allocation and garbage collection overhead.
  • Flutter's widget comparison skips rebuild logic for const widgets that are identical objects in memory.
  • The analyzer detects every location where const can be safely applied—trust it and apply the fix.
  • Lint rules like prefer_const_constructors enforce const usage systematically and prevent regressions.
  • Common mistakes involve marking non-const-eligible widgets as const, forgetting to declare constructors as const, and ignoring analyzer warnings.
  • Const is a free performance win requiring no architectural changes—apply it everywhere the analyzer suggests.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles