Skip to content

Build Process

Understand how Flutter builds widgets and manages the build lifecycle.


What is it?

The build process in Flutter is the mechanism by which widgets are created, configured, and arranged into a widget tree. It involves calling the build() method on widgets, reconciling with the existing element tree, and updating the render tree. The build process is triggered by state changes, parent rebuilds, or other events.


Why does it exist?

The build process exists to:

  • Create and configure widgets based on current state
  • Rebuild parts of the UI when state changes
  • Optimize performance by only rebuilding changed widgets
  • Maintain a consistent UI state
  • Enable declarative UI programming
  • Support hot reload and development features

The Build Pipeline

The build process follows a specific pipeline in Flutter.

1. Trigger Event
   ↓
2. Mark Dirty
   ↓
3. Schedule Build
   ↓
4. Build Widgets
   ↓
5. Update Elements
   ↓
6. Layout Render Tree
   ↓
7. Paint Render Tree
   ↓
8. Display on Screen

What's happening here? - Events trigger the build process - Widgets are marked as dirty - Build is scheduled on the next frame - Widgets are rebuilt and configured - Elements are updated with new configuration - Render tree is laid out and painted


The Build Method

build() is called to create the widget hierarchy.

// The build method
class MyWidget extends StatelessWidget {
  const MyWidget({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    // Called every time the widget needs to rebuild
    // Returns a widget tree

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          children: [
            const Text('Hello'),
            ElevatedButton(
              onPressed: () {
                // Triggers rebuild when state changes
                setState(() {});
              },
              child: const Text('Press'),
            ),
          ],
        ),
      ),
    );
  }
}

// When build() is called:
// 1. Initial build (after mounting)
// 2. State changes (setState)
// 3. Inherited widget changes
// 4. Parent widget rebuilds
// 5. Hot reload

// What build() should NOT do:
// - Heavy computations
// - Network requests
// - File operations
// - Database queries
// - Memory allocations

What's happening here? - build() returns a widget tree - Called when widget needs to update - Should be pure and fast - Avoid heavy operations in build - build() creates new widgets


Types of Build

Different types of builds occur in Flutter.

// 1. Initial Build (mounting)
class InitialBuildWidget extends StatefulWidget {
  const InitialBuildWidget({super.key});

  @override
  State<InitialBuildWidget> createState() => _InitialBuildWidgetState();
}

class _InitialBuildWidgetState extends State<InitialBuildWidget> {
  @override
  void initState() {
    super.initState();
    // Initial build triggered
    // Widget is mounted to tree
    print('Initial build');
  }

  @override
  Widget build(BuildContext context) {
    return const Text('Initial');
  }
}

// 2. State Change Build
class StateChangeWidget extends StatefulWidget {
  const StateChangeWidget({super.key});

  @override
  State<StateChangeWidget> createState() => _StateChangeWidgetState();
}

class _StateChangeWidgetState extends State<StateChangeWidget> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++; // Triggers rebuild
    });
  }

  @override
  Widget build(BuildContext context) {
    // Called every time setState is called
    print('Build: $_counter');
    return Text('$_counter');
  }
}

// 3. Inherited Widget Build
class InheritedBuildWidget extends StatelessWidget {
  const InheritedBuildWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // Builds when inherited data changes
    final data = InheritedData.of(context);
    return Text(data.value);
  }
}

// 4. Parent Rebuild
class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _showChild = true;

  @override
  Widget build(BuildContext context) {
    // Rebuilds child when parent rebuilds
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            setState(() {
              _showChild = !_showChild;
            });
          },
          child: const Text('Toggle'),
        ),
        if (_showChild)
          const ChildWidget(), // Rebuilds with parent
      ],
    );
  }
}

What's happening here? - Initial build happens on mount - State changes trigger rebuilds - Inherited widgets rebuild dependents - Parent rebuilds affect children


Build Scheduling

Builds are scheduled to occur on the next frame.

// Build scheduling
class BuildScheduler extends StatefulWidget {
  const BuildScheduler({super.key});

  @override
  State<BuildScheduler> createState() => _BuildSchedulerState();
}

class _BuildSchedulerState extends State<BuildScheduler> {
  int _counter = 0;

  void _updateCounter() {
    // setState schedules a build
    setState(() {
      _counter++;
    });
    // Build doesn't happen immediately
    // It's scheduled for the next frame
    print('State updated, build scheduled');
  }

  @override
  Widget build(BuildContext context) {
    // Called on the next frame
    print('Build executing');
    return Text('Counter: $_counter');
  }
}

// Multiple setState calls are batched
void _batchUpdate() {
  // All three setState calls
  // Result in a single build
  setState(() { _counter++; });
  setState(() { _counter++; });
  setState(() { _counter++; });
}

// Manual build triggering
void _triggerBuild() {
  // Rarely needed
  // WidgetsBinding.instance.allowReassemble();
}

// Build phases:
// 1. Mark dirty (setState)
// 2. Schedule build (end of frame)
// 3. Execute build (next frame)
// 4. Perform layout
// 5. Perform painting

What's happening here? - setState schedules a build - Builds are batched together - Builds happen on the next frame - Multiple changes = one build - Efficient update system


Build Context

BuildContext provides information during build.

// Using BuildContext in build method
class ContextWidget extends StatelessWidget {
  const ContextWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // 1. Access theme
    final theme = Theme.of(context);

    // 2. Access media query
    final mediaQuery = MediaQuery.of(context);
    final size = mediaQuery.size;
    final isDark = mediaQuery.platformBrightness == Brightness.dark;

    // 3. Access inherited widgets
    final data = InheritedData.of(context);

    // 4. Access navigator
    final navigator = Navigator.of(context);

    // 5. Access focus scope
    final focusScope = FocusScope.of(context);

    // 6. Access scaffold
    final scaffold = Scaffold.of(context);

    return Container(
      width: size.width * 0.5,
      color: theme.primaryColor,
      child: Text(data?.value ?? 'No data'),
    );
  }
}

// Context limitations:
// 1. Only available during build
// 2. Cannot be used after dispose
// 3. Must be used in current widget
// 4. May not exist at certain times

// Safe context usage:
if (mounted) {
  // Safe to use context
  Navigator.push(context, route);
}

What's happening here? - Context provides tree information - Access theme, media, inherited data - Used for navigation and actions - Only valid during build and events


Build Optimization

Optimizing builds improves performance.

// 1. Use const widgets
class ConstWidget extends StatelessWidget {
  const ConstWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // const widgets are reused
    return const Column(
      children: [
        Text('Hello'),
        Text('World'),
      ],
    );
  }
}

// 2. Use const constructors
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // const children don't rebuild
    return const Padding(
      padding: EdgeInsets.all(8.0),
      child: Text('Hello'),
    );
  }
}

// 3. Split widgets
class SplitWidget extends StatefulWidget {
  const SplitWidget({super.key});

  @override
  State<SplitWidget> createState() => _SplitWidgetState();
}

class _SplitWidgetState extends State<SplitWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // This widget rebuilds when counter changes
        CounterDisplay(counter: _counter),

        // This widget doesn't rebuild (const)
        const ControlsWidget(),

        // This widget is independent
        ElevatedButton(
          onPressed: () {
            setState(() {
              _counter++;
            });
          },
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

// 4. Use child caching
class CachedWidget extends StatelessWidget {
  const CachedWidget({super.key, required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    // Child is cached and not rebuilt
    return Container(
      child: child,
    );
  }
}

// 5. Use keys for stable identity
class ListWidget extends StatelessWidget {
  const ListWidget({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: items.map((item) {
        return Text(
          item,
          key: ValueKey(item), // Stable identity
        );
      }).toList(),
    );
  }
}

What's happening here? - const widgets are reused - Split widgets to isolate rebuilds - Child caching prevents rebuilds - Keys provide stable identity - Smaller widgets rebuild faster


Build Performance

Measuring and improving build performance.

// Debug build performance
class DebugWidget extends StatelessWidget {
  const DebugWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // 1. Track build time
    final stopwatch = Stopwatch()..start();

    // Build widget
    final widget = _buildWidget();

    stopwatch.stop();
    if (stopwatch.elapsedMilliseconds > 16) {
      // Build took too long (exceeds frame budget)
      print('Slow build: ${stopwatch.elapsedMilliseconds}ms');
    }

    return widget;
  }

  Widget _buildWidget() {
    // Heavy build
    return const Text('Hello');
  }
}

// Profile build performance
class ProfileBuild extends StatelessWidget {
  const ProfileBuild({super.key});

  @override
  Widget build(BuildContext context) {
    // Use Flutter DevTools to profile:
    // 1. Open DevTools
    // 2. Select Performance tab
    // 3. Record timeline
    // 4. Check build costs
    // 5. Identify expensive widgets

    return const Text('Profile me');
  }
}

// Common performance issues:
// 1. Building too many widgets
// 2. Heavy computation in build
// 3. Deep widget trees
// 4. Not using const
// 5. Rebuilding unchanged widgets

What's happening here? - Track build time for optimization - Use DevTools for profiling - Keep builds under 16ms - Avoid heavy computations - Rebuild only what changed


Build in Different Scenarios

Build behavior varies based on context.

// 1. Hot Reload Build
class HotReloadWidget extends StatelessWidget {
  const HotReloadWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // During hot reload:
    // 1. Widget rebuilds
    // 2. State is preserved
    // 3. Elements are updated
    // 4. Render tree is updated

    return const Text('Hot reload works!');
  }
}

// 2. Release Build
class ReleaseWidget extends StatelessWidget {
  const ReleaseWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // In release mode:
    // 1. Assertions are removed
    // 2. Debug flags are disabled
    // 3. Optimized build
    // 4. Faster performance

    return const Text('Release mode');
  }
}

// 3. Profile Build
class ProfileWidget extends StatelessWidget {
  const ProfileWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // In profile mode:
    // 1. Debugging disabled
    // 2. Performance tracking
    // 3. Tracing enabled
    // 4. Best for performance testing

    return const Text('Profile mode');
  }
}

// 4. Debug Build
class DebugBuildWidget extends StatelessWidget {
  const DebugBuildWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // In debug mode:
    // 1. Assertions enabled
    // 2. Debug flags on
    // 3. Slower performance
    // 4. Better error messages

    assert(true, 'Debug mode');
    return const Text('Debug mode');
  }
}

What's happening here? - Different builds for different purposes - Hot reload preserves state - Release mode is optimized - Profile mode for performance - Debug mode for development


Best Practices

Keep Build Methods Pure

// Good - Pure build method
@override
Widget build(BuildContext context) {
  return Text('Hello');
}

// Bad - Side effects in build
@override
Widget build(BuildContext context) {
  fetchData(); // Side effect - DON'T DO THIS
  return Text('Hello');
}

Use const Where Possible

// Good - const widgets
@override
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(8.0),
    child: Text('Hello'),
  );
}

// Bad - Not using const
@override
Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(8.0), // Creates new object every time
    child: Text('Hello'),
  );
}

Split Large Widgets

// Good - Split widgets
class ParentWidget extends StatelessWidget {
  const ParentWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const HeaderWidget(),
        const ContentWidget(),
        const FooterWidget(),
      ],
    );
  }
}

// Bad - One large widget
class LargeWidget extends StatelessWidget {
  const LargeWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // Everything in one widget
    return Column(
      children: [
        // Header
        // Content
        // Footer
        // All in one place
      ],
    );
  }
}

Common Mistakes

Doing Heavy Work in Build

Wrong:

@override
Widget build(BuildContext context) {
  final data = heavyComputation(); // Runs every build
  return Text(data);
}

Correct:

final data = heavyComputation(); // Runs once

@override
Widget build(BuildContext context) {
  return Text(data);
}

Not Using Keys

Wrong:

// No keys for list items
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Text(items[index]);
  },
)

Correct:

// Keys for list items
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Text(
      items[index],
      key: ValueKey(index),
    );
  },
)


Summary

The build process creates and updates widgets based on state changes. Understanding when and how builds occur helps optimize performance. Keep build methods pure, use const widgets, split large widgets, and avoid heavy computations.


Next Steps


Did You Know?

  • Build methods are called every frame for dirty widgets
  • Const widgets are pre-compiled and never rebuild
  • Builds are batched together for efficiency
  • Widgets can rebuild multiple times per frame
  • The build process is optimized for performance
  • Hot reload creates a fresh build
  • Release builds skip debug checks