UI Framework #

In the world of user interface engineering, the programming paradigm has shifted revolutionarily in recent years. The traditional approach that demands manually manipulating UI objects one by one has been replaced by a structured declarative approach. Flutter is one of the most mature implementations of a declarative UI framework today. To master Flutter properly, you have to change how you think about building interfaces: how widgets are composed, how data changes redefine the visual structure, and how Flutter’s internal architecture turns code descriptions into real pixels drawn on screen.


Paradigm: Imperative vs Declarative #

The fundamental difference between traditional UI frameworks (like the old Android XML/View system or old iOS UIKit) and Flutter lies in the programming paradigm used: Imperative versus Declarative.

1. The Imperative Approach #

In the imperative paradigm, you as the developer hold direct references to UI objects already existing in memory, then call mutation methods to change those objects’ visual properties one by one whenever state changes.

// Example of the IMPERATIVE approach (Old Native Style)
// You have to find the object, then mutate it manually
TextView labelNama = findViewById(R.id.label_nama);
ImageView fotoProfil = findViewById(R.id.foto_profil);

// When user data finishes loading:
labelNama.setText("Budi Santoso");
labelNama.setTextColor(Color.BLUE);
fotoProfil.setImageResource(R.drawable.budi_avatar);

The weakness of this approach is fragile state synchronization. If your app has dozens of interrelated states (e.g., loading button active, empty data, network error, admin access rights), your imperative mutation code gets filled with complex if-else branches to manually hide or show UI elements. This is prone to triggering visual bugs where the display doesn’t match the actual data in memory.

2. The Declarative Approach #

In the declarative paradigm, you don’t manipulate UI object instances directly. Instead, you describe what the visual interface should look like based on the current data state. This relationship is formulated in the popular mathematical equation:

$$\text{UI} = f(\text{state})$$

Where the user interface ($\text{UI}$) is purely the result of a function ($f$) of the current data state ($\text{state}$).

// Example of the DECLARATIVE approach (Flutter Style)
// We define the UI structure based on the current state values
final String name = 'Budi Santoso';
final bool isAdmin = true;

@override
Widget build(BuildContext context) {
  return Row(
    children: [
      Text(
        name,
        style: TextStyle(
          color: isAdmin ? Colors.blue : Colors.black, // Follows the state
        ),
      ),
      if (isAdmin) const Icon(Icons.verified), // Declarative conditional
    ],
  );
}

When the isAdmin value changes from true to false, you don’t go hunting for the Icon widget to remove it manually. You simply trigger a state update, and Flutter re-invokes the build function to reconstruct the new UI structure matching the latest state values.

💡 The Restaurant Chef Analogy

  • Imperative is like walking into the kitchen and giving the chef step-by-step instructions: “Take a plate, put rice on it, grab a spoon, place it on table number 3.” If the order changes, you have to go back in and move the plate manually.
  • Declarative is like writing your order on a slip of paper: “Table 3 orders a plate of fried rice.” The chef in the kitchen (in this case, the Flutter Engine) is the one responsible for figuring out how to cook, serve, and efficiently clear the dirty dishes at that table.

Widgets: Immutable UI Blueprints #

In Flutter, every visual element is a widget. This concept is often called “Everything is a widget”.

  • Structural page elements (Scaffold, AppBar).
  • Layout elements (Row, Column, Stack, Padding).
  • Visual styling elements (Theme, MediaQuery).
  • Interaction detection elements (GestureDetector, InkWell).

Widgets in Flutter are Immutable (cannot be changed). Once a widget is created with certain parameters, those parameter values can’t be changed mid-way. Properties inside a widget class must be declared with the final keyword.

If widgets are immutable, how do you change what’s on screen?

  • You destroy the old widget and replace it with a new one that has different parameter configurations.
  • Because widgets in Flutter are just lightweight configuration objects (a blueprint), creating and destroying thousands of widget objects every second is very cheap and fast, putting no burden on the device’s CPU performance.

Widget Composition #

Flutter avoids deep class inheritance to create new UIs. Instead, Flutter adopts the principle of Composition — building complex components by combining simple widgets that each have a specific responsibility.

// Building a complex profile card component through basic widget composition
Widget buildProfileCard() {
  return Card(
    elevation: 4,
    child: Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        children: [
          const CircleAvatar(
            radius: 30,
            backgroundImage: NetworkImage('https://example.com/budi.jpg'),
          ),
          const SizedBox(width: 16),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                'Budi Santoso',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
              ),
              Text(
                'Flutter Specialist',
                style: TextStyle(color: Colors.grey[600]),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

You don’t inherit from the Card class to make a ProfileCard. You simply put Padding, Row, CircleAvatar, SizedBox, Column, and Text as children of Card. The advantage is absolute flexibility: you can easily change the layout just by rearranging the widget tree structure without fear of breaking the class hierarchy.


The Three Trees Architecture #

One of the biggest secrets behind why Flutter can continuously rebuild its UI at very high performance lies in the Three Trees Architecture. Behind the scenes, Flutter maintains three separate object tree structures in parallel:

flowchart TD
    subgraph WTree["1. Widget Tree (Blueprint)"]
        W1["Scaffold"] --> W2["Padding"]
        W2 --> W3["Text"]
    end

    subgraph ETree["2. Element Tree (Lifecycle Controller)"]
        E1["ComponentElement"] --> E2["SingleChildRenderObjectElement"]
        E2 --> E3["MultiChildRenderObjectElement"]
    end

    subgraph RTree["3. RenderObject Tree (Layout & Paint)"]
        R1["RenderPadding"] --> R2["RenderParagraph"]
    end

    WTree -->|"1. Triggers Instantiation"| ETree
    ETree -->|"2. Manages & Updates"| RTree

    style WTree stroke:#0288d1,stroke-width:2px
    style ETree stroke:#388e3c,stroke-width:2px
    style RTree stroke:#f57c00,stroke-width:2px

Let’s analyze the role and characteristics of each tree above:

1. Widget Tree #

  • Nature: Immutable, very short-lived, very lightweight.
  • Role: Acts as a blueprint or UI configuration spec written by the developer. This tree is destroyed and rebuilt from scratch every time the build() method is called due to a state change.

2. Element Tree #

  • Nature: Mutable, long-lived, acts as an intermediary.
  • Role: An Element represents the real instance of a widget at a specific location on screen. It manages the widget’s lifecycle, stores memory state (the State object in a StatefulWidget), and performs the reconciliation process.
  • Reconciliation Process: When a new widget is sent to the Element Tree after a rebuild, the Element compares the new widget with the old one. If the widget’s class type (runtimeType) and unique key are the same, the Element is not destroyed. It just updates its internal configuration with the new values from the widget, then forwards the update to the RenderObject.

3. RenderObject Tree #

  • Nature: Mutable, long-lived, does the heavy calculations.
  • Role: Contains pure graphics objects (RenderObject) responsible for computing visual coordinate layouts (layout) and drawing the actual graphic elements (paint) onto the canvas surface. RenderObjects are only created or replaced when there’s a drastic structural change in the widget tree (e.g., a widget is removed from the tree).

With this separation, Flutter guarantees high efficiency: the expensive layout computation process in the RenderObject Tree is protected from the constant Widget Tree recreation process.


Layout Mechanism: Constraints Go Down, Sizes Go Up #

The layout system in Flutter is very simple but absolute. It’s governed by one main principle:

“Constraints Go Down, Sizes Go Up, Parent Sets Position”

This layout determination flow runs recursively through the following chart:

flowchart LR
    Parent["Parent Widget (Scaffold/Row)"] -->|"1. Send Constraints (Min & Max Width/Height)"| Child["Child Widget (Padding/Card)"]
    Child -->|"2. Send Actual Size (Width & Height)"| Parent
    Parent -->|"3. Determine Child Position (X, Y Coordinates)"| Display["Device Screen"]

    style Parent stroke:#0288d1,stroke-width:2px
    style Child stroke:#388e3c,stroke-width:2px

Here’s a detailed explanation of the three phases above:

  1. Constraints Go Down: The parent widget sends a BoxConstraints object to its child widget. These constraints define the minimum and maximum allowed values for the child’s width and height.
    • Example: “You may have a width between 100px and 300px, and your height must be exactly 200px.”
  2. Sizes Go Up: The child widget calculates its own size needs based on its internal content, provided that size must comply with the constraints given by its parent. After calculating, the child reports its actual size back to the parent.
    • Example: The child reports: “I’ve decided to have a width of 250px and a height of 200px.”
  3. Parent Sets Position: After receiving the size report from the child, the parent widget is responsible for placing that child on the $X$ and $Y$ coordinate system within its own screen area. The child has no right to decide where it draws itself; only the parent knows its coordinate position.

Breaking Down the Five Rendering Pipeline Phases #

After the widget build process completes, Flutter runs a series of rendering pipeline stages in sequence to process the tree structure into lit-up pixels on the device screen. This process runs in 5 main phases:

flowchart TD
    StateChange["State Changes (setState)"] --> Build["1. BUILD Phase: Widget Tree Reconstruction"]
    Build --> Layout["2. LAYOUT Phase: Constraints & Sizes Transmission"]
    Layout --> Paint["3. PAINT Phase: DisplayList Recording (Canvas)"]
    Paint --> Composite["4. COMPOSITE Phase: Graphics Layer Merging"]
    Composite --> Rasterize["5. RASTERIZE Phase: GPU Draws Pixels (Impeller/Skia)"]

    style Build stroke:#0288d1,stroke-width:2px
    style Layout stroke:#0288d1,stroke-width:2px
    style Paint stroke:#388e3c,stroke-width:2px
    style Composite stroke:#388e3c,stroke-width:2px
    style Rasterize stroke:#f57c00,stroke-width:2px

1. Build (Tree Construction) #

This phase executes entirely in the Dart layer. Flutter calls the build() method on widgets marked dirty (dirty widgets due to setState calls). The result is a new Widget Tree that will be matched against the Element Tree to update the Elements’ configuration properties.

2. Layout (Dimension Calculation) #

The RenderObject Tree traverses the tree using depth-first search (DFS). Constraints go down, and sizes come back up. Each object calculates its position on screen. The final result of this phase is that every RenderObject has a definite Size and absolute Offset.

3. Paint (Creating Drawing Instructions) #

RenderObjects generate a series of graphics painting instructions.

  • Example: “Draw a red circle with a 10px radius at coordinates (50,50), then draw a black line 20px long.” These drawing instructions are recorded in a DisplayList object (in the modern architecture) or stored as separate layers, rather than as direct pixels.

4. Composite (Layer Compositing) #

Because modern apps have many visual effects (like opacity transitions, background blur effects, or curved clipping), Flutter splits drawing instructions into several graphics layers (Compositing Layers). In this phase, those layers are re-arranged and merged into a unified layer tree (Layer Tree) ready to be sent to the GPU.

5. Rasterize (GPU Rasterization) #

The Layer Tree is sent to the C++ Engine (Impeller or Skia). The Engine uses GPU hardware acceleration to rasterize (converting abstract drawing instructions into sequences of physical pixel colors) and present them on the user’s device screen. This entire pipeline is targeted to complete in under 8–16 milliseconds to guarantee smooth 60–120 FPS.


System Design Comparison: Material vs Cupertino #

As a cross-platform UI framework that draws its own UI independently, Flutter provides two complete design system component libraries to mimic the native visual styles of each operating system:

Component CriteriaMaterial Design (Android/Google)Cupertino (iOS/Apple)
Import Libraryimport 'package:flutter/material.dart';import 'package:flutter/cupertino.dart';
Page ScaffoldScaffoldCupertinoPageScaffold
Top Navigation BarAppBarCupertinoNavigationBar
Primary Button StyleElevatedButton / FilledButtonCupertinoButton
Switch ComponentSwitchCupertinoSwitch
Alert DialogAlertDialogCupertinoAlertDialog
Aesthetic CharacteristicsDynamic Material 3 design with unified seed colors.Minimalist iOS design with frosted glass blur effects (backdrop filter).

Here’s a code comparison for implementing both design styles:

// EXAMPLE 1: Implementing a Material Design styled page
Widget buildMaterialPage() {
  return MaterialApp(
    theme: ThemeData(useMaterial3: true),
    home: Scaffold(
      appBar: AppBar(title: const Text('Material 3 App')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {},
          child: const Text('Material Button'),
        ),
      ),
    ),
  );
}

// EXAMPLE 2: Implementing a Cupertino (iOS) styled page
Widget buildCupertinoPage() {
  return const CupertinoApp(
    home: CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('iOS Cupertino App'),
      ),
      child: Center(
        child: CupertinoButton(
          color: CupertinoColors.activeBlue,
          onPressed: null,
          child: Text('Cupertino Button'),
        ),
      ),
    ),
  );
}

The Declarative Paradigm Across the Industry #

The declarative UI paradigm used by Flutter isn’t something foreign. In fact, all modern UI frameworks in the industry today have converged toward the same paradigm:

flowchart TD
    subgraph Frameworks["Cross-Framework UI Paradigm"]
        direction TB
        React["React (Web) <br> JSX -> Virtual DOM -> Real DOM"]
        Flutter["Flutter (Cross-Platform) <br> Widget Tree -> Element Tree -> GPU Canvas"]
        SwiftUI["SwiftUI (iOS Native) <br> View Structure -> Attribute Graph -> UIKit/Metal"]
        Compose["Jetpack Compose (Android Native) <br> Composable -> Slot Table -> Android Canvas"]
    end

    style React stroke:#0288d1,stroke-width:2px
    style Flutter stroke:#388e3c,stroke-width:2px
    style SwiftUI stroke:#f57c00,stroke-width:2px
    style Compose stroke:#4caf50,stroke-width:2px

Although conceptually they share a similar way of thinking (UI as a function of state), Flutter has one absolute advantage over the rest: pure portability.

  • SwiftUI is tied to Apple’s operating systems.
  • Jetpack Compose is designed primarily for the Android ecosystem.
  • React Native must translate its code to each OS’s native rendering.

Flutter is the only framework that brings its own rendering engine, so it can run identically on all of those operating systems without depending on the target OS vendor’s rendering implementation.


Summary #

  • A Function of State — The UI is defined declaratively as a pure function of data state ($\text{UI} = f(\text{state})$), eliminating the risk of visual-data desynchronization.
  • Immutable Widgets — Widgets are just lightweight configuration objects that are immutable and cheap to destroy and rebuild.
  • Three Trees Architecture — Tactical separation between the Widget Tree (blueprint), Element Tree (intermediary & state), and RenderObject Tree (layout & paint computation) for maximum performance.
  • Layout Workflow — Follows the principle of constraints going down, sizes going up, and position set by the parent.
  • Ordered Rendering Pipeline — The visualization process flows through 5 structured stages: Build → Layout → Paint → Composite → Rasterize in under 16ms.
  • Complete Design Libraries — Ships Material Design and Cupertino components out of the box to satisfy the visual preferences of users across operating systems.

← Previous: AOT vs JIT   Next: Dart Language →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact