Flutter: From Dart Code to Pixels

You tap a button. A number changes. Between those two moments, Flutter receives an event, changes application state, rebuilds a description of part of the interface, lays it out, paints it, and hands a scene to the device for rasterization.

That entire journey becomes easier once you own one mental model:

You describe the interface for the current state. Flutter works out how to update the screen.

By the end of this chapter, you will build a small Reading Progress app and explain how its Dart code becomes pixels. You do not need prior Dart, Flutter, or mobile-development experience. You only need to recognize basic programming ideas such as variables, functions, and classes.


1. What Flutter Actually Is

Flutter is a cross-platform user-interface toolkit. It gives you a framework, rendering system, developer tools, and platform integrations for building applications from one project.

Dart is the programming language used to write Flutter applications. Saying “Flutter code” usually means Dart code that imports Flutter libraries.

One Dart and Flutter application sharing code across mobile, web, and desktop targets

Figure 1 — Flutter lets the application share UI and business logic while still providing access to platform-specific capabilities.

Flutter can target Android, iOS, browsers, Windows, macOS, and Linux. Shared code does not mean every platform is identical. Your app might still need different navigation conventions, permissions, window layouts, input methods, or native integrations.

Three useful boundaries

  • Flutter is not a language; Dart is the language.
  • Flutter is not a web page inside a mobile shell; normal Flutter widgets are not HTML rendered by a WebView.
  • Flutter is not merely a wrapper around Android and iOS controls; Flutter normally lays out and draws its own widget implementations.

This rendering control gives Flutter consistent visuals and a composable API. The trade-off is responsibility: a polished cross-platform app must deliberately adapt to different screens and platform expectations.

Decision rule: share behavior and design language by default; adapt where the platform changes usability, capabilities, or user expectations.


2. Read the Smallest Useful Flutter App

Start with a complete program:

import 'package:flutter/material.dart';

void main() {
  runApp(const ReadingApp());
}

class ReadingApp extends StatelessWidget {
  const ReadingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('Read one page today.'),
        ),
      ),
    );
  }
}

Read it from the outside inward:

  1. main() is the Dart entry point.
  2. runApp() gives Flutter the root widget.
  3. ReadingApp describes the application and never changes its own fields, so it extends StatelessWidget.
  4. build() returns the widget description for this point in the tree.
  5. MaterialApp supplies Material Design behavior such as navigation, themes, and text direction.
  6. Scaffold provides a standard visual page structure.
  7. Center positions its child, and Text describes the visible label.

The vocabulary you need

TermWorking definition
WidgetAn immutable description of part of the interface.
PropertyConstructor data that configures a widget, such as child or title.
CompositionBuilding a larger interface by nesting small widgets.
Widget treeThe parent-child hierarchy formed by that nesting.
StateData that may change while the app runs and can affect the UI.
BuildContextA handle to a widget’s location in Flutter’s element tree.

BuildContext is not “the screen.” It identifies a location in the tree, which lets code find nearby inherited information such as Theme.of(context). You will study its lifecycle and lookup rules later; for now, read it as “where this widget lives.”


3. Build One App, Not Six Disconnected Examples

Our Reading Progress screen has four meaningful regions.

Annotated phone wireframe of the Reading Progress application

Figure 2 — The screen is a composition: the app bar names the page, the card groups progress, text reflects state, and the button produces an event.

Here is the complete main.dart:

import 'package:flutter/material.dart';

void main() {
  runApp(const ReadingApp());
}

class ReadingApp extends StatelessWidget {
  const ReadingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const ReadingProgressPage(),
    );
  }
}

class ReadingProgressPage extends StatefulWidget {
  const ReadingProgressPage({super.key});

  @override
  State<ReadingProgressPage> createState() => _ReadingProgressPageState();
}

class _ReadingProgressPageState extends State<ReadingProgressPage> {
  static const int readingGoal = 50;
  int pagesRead = 12;

  void _addPage() {
    if (pagesRead >= readingGoal) return;

    setState(() {
      pagesRead++;
    });
  }

  @override
  Widget build(BuildContext context) {
    final double progress = pagesRead / readingGoal;
    final bool isComplete = pagesRead >= readingGoal;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Reading Progress'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(24),
                  child: Column(
                    children: [
                      const Text('PAGES READ'),
                      Text(
                        '$pagesRead',
                        style: Theme.of(context).textTheme.displayMedium,
                      ),
                      Text('of $readingGoal pages'),
                      const SizedBox(height: 16),
                      LinearProgressIndicator(value: progress),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 20),
              FilledButton.icon(
                onPressed: isComplete ? null : _addPage,
                icon: const Icon(Icons.add),
                label: Text(isComplete ? 'Goal complete' : 'Add a page'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The example uses no third-party packages and can replace lib/main.dart in a new Flutter project.

Why the code is nested

Flutter favors composition over a huge screen object with hundreds of built-in settings. Center centers. Padding adds space. Column arranges children vertically. Card supplies a grouped surface. Each widget contributes one focused behavior.

An indented widget tree mapped to the Reading Progress phone wireframe

Figure 3 — Constructor nesting becomes a tree. Follow child or children to move from a parent to its descendants.

The tree shown in your source is a useful widget tree, but it is not a list of permanent screen objects. Flutter may create new widget instances whenever the description needs to be rebuilt. That is cheap by design.


4. The Core Model: UI = f(state)

The expression

UI = f(state)

means: for a particular state, build() returns the interface that should exist now.

In our app:

  • State: pagesRead == 12
  • UI derived from it: the large label says 12, progress is 12 / 50, and the button remains enabled.
  • New state: pagesRead == 50
  • New UI: the label says 50, progress is full, and the button says Goal complete and is disabled.

Imperative versus declarative thinking

An imperative approach might locate three screen controls and mutate each one:

// Conceptual imperative pseudocode—not Flutter UI code.
counterLabel.text = '13';
progressBar.value = 13 / 50;
button.enabled = true;

That creates several facts you must keep synchronized. Flutter’s declarative approach changes the source of truth and describes the result:

setState(() {
  pagesRead++;
});

// build() later derives every affected widget from pagesRead.
Text('$pagesRead');
LinearProgressIndicator(value: pagesRead / readingGoal);

Before-and-after phone wireframes followed by tap, state, build, and update steps

Figure 4 — setState() changes data and tells Flutter that this State object needs rebuilding; it does not directly repaint the number.

Why StatefulWidget and State are separate

Widgets are immutable descriptions. ReadingProgressPage therefore stays immutable, while its separate _ReadingProgressPageState object holds pagesRead, which changes over time.

  • StatelessWidget is suitable when the widget has no mutable state of its own.
  • StatefulWidget creates a persistent State object for data that changes during that location’s lifetime.
  • setState(callback) performs a synchronous state mutation and marks that State for a future rebuild.

Calling setState() does not rebuild the entire application, and a rebuild is not the same as repainting every pixel. Flutter schedules work and reuses persistent objects where it can.

Keep build() predictable

Flutter may call build() whenever it needs a fresh description. Therefore, build() should be fast and free of side effects.

@override
Widget build(BuildContext context) {
  // ❌ A rebuild could repeat this request.
  // final books = fetchBooksFromNetwork();

  // ✅ Read already-available state and describe the UI.
  return Text('$pagesRead pages read');
}

Fetch data, start subscriptions, or perform expensive computation outside build(), then store the result as state and render it.


5. From Widgets to Pixels

Your widget source is not sent directly to the GPU. Flutter is layered so each part has a focused job.

Layered diagram of the Dart app, Flutter framework, engine, embedder, and operating system or GPU

Figure 5 — The framework turns descriptions into a scene; the engine provides low-level graphics and rasterization; the embedder connects Flutter to its host platform.

Layer by layer

  1. Dart application: your state, business rules, callbacks, and widget descriptions.
  2. Flutter framework: Dart libraries for widgets, gestures, accessibility, animation, layout, painting, and more.
  3. Flutter engine: lower-level runtime and graphics facilities exposed to the framework through dart:ui; it rasterizes composited scenes.
  4. Platform embedder: connects the engine to a platform window, input, lifecycle, rendering surface, and platform messages.
  5. Operating system and graphics hardware: present the resulting pixels and deliver input back to the application.

A preview of the three trees

The framework uses related structures with different lifetimes:

  • The widget tree contains immutable configuration objects produced by your code.
  • The element tree records persistent locations in the UI hierarchy. A BuildContext refers to an element’s location.
  • The render-object tree performs work such as layout, painting, and hit testing for renderable parts of the interface.

A rebuild can create new widgets without throwing away all persistent elements and render objects. Flutter reconciles the new descriptions with what already exists. Chapter 8 covers that mechanism, Widget.canUpdate, and keys in depth.

Does Flutter use native controls?

For its normal widget UI, Flutter provides and draws its own Material, Cupertino, and foundational widgets instead of translating each one into an Android View or iOS UIView. This makes visuals composable and consistent.

Flutter can still call platform APIs through plugins or platform channels, and it can embed native platform views when a feature requires them. “Flutter draws the UI” does not mean “Flutter cannot use native capabilities.”


6. Development and Release Are Different Environments

Flutter optimizes the development loop for iteration and release builds for delivery.

ActionPreserves current app state?What it is for
Hot reloadUsually yesInject Dart source changes and rebuild affected UI quickly.
Hot restartNoRestart the Dart application without rebuilding the full native host.
Full restartNoStop, rebuild, and relaunch everything, including native changes.

Hot reload works in debug mode. Some changes—such as native platform code, application initialization, or certain type-shape changes—need a hot restart or full restart.

During development, Flutter uses a Dart VM to support debugging and stateful hot reload. Release builds produce optimized output for the selected target: native targets compile for their platform, while web targets produce browser-compatible output. Never judge release performance from debug mode alone.


7. Five Mistakes This Mental Model Prevents

Mistake 1: Performing work inside build()

Symptom: repeated network calls, duplicate analytics events, or dropped frames.

Rule: build() reads state and returns widgets. Put side effects and expensive work in the appropriate lifecycle, event, or data layer.

Mistake 2: Changing state without notifying Flutter

void _addPageWrong() {
  pagesRead++; // ❌ The variable changes, but no rebuild is requested.
}

void _addPage() {
  setState(() {
    pagesRead++; // ✅ Change and notification form one operation.
  });
}

Mistake 3: Treating a widget like a mutable view

Do not save a Text widget and try to change its label later. Save the data—pagesRead—and let build() create the appropriate Text description.

Mistake 4: Building one enormous widget

When a subtree represents one idea and can receive clear inputs, extract it into a focused widget. This improves naming, reuse, testing, and rebuild reasoning. Do not split every three lines: extract around responsibility, not arbitrary size.

Mistake 5: Confusing shared code with identical experiences

A desktop window, touch phone, and browser have different sizes, input devices, navigation expectations, and platform services. Reuse is a starting point; adaptability is part of production quality.


8. Guided Practice: Finish the Reading Goal

Add two behaviors without changing the app’s core model:

  1. Put a reset action in the app bar. Tapping it sets pagesRead to 0.
  2. When pagesRead >= readingGoal, show Goal complete!, fill the progress bar, and disable the add button.

Two practice-target wireframes showing a reset control and the completed reading goal

Figure 6 — Implement these two target states from data; do not manually manipulate the visible controls.

Hints

  • Reset and increment are both events that change the same state.
  • Derive isComplete inside build() from pagesRead and readingGoal.
  • A disabled Material button uses onPressed: null.
  • Clamp the progress value if future changes could make pagesRead exceed the goal.

Reference solution

Replace the state-changing methods with:

void _addPage() {
  if (pagesRead >= readingGoal) return;
  setState(() => pagesRead++);
}

void _resetProgress() {
  setState(() => pagesRead = 0);
}

Add the app-bar action:

appBar: AppBar(
  title: const Text('Reading Progress'),
  actions: [
    IconButton(
      onPressed: _resetProgress,
      tooltip: 'Reset progress',
      icon: const Icon(Icons.refresh),
    ),
  ],
),

Derive the completion state and use it in the UI:

final bool isComplete = pagesRead >= readingGoal;
final double progress = (pagesRead / readingGoal).clamp(0.0, 1.0);

Text(isComplete ? 'Goal complete!' : 'Keep reading'),
FilledButton.icon(
  onPressed: isComplete ? null : _addPage,
  icon: const Icon(Icons.add),
  label: Text(isComplete ? 'Goal complete' : 'Add a page'),
),

Where the book goes next

  • Dart foundations: null safety, types, object orientation, asynchronous execution, and isolates.
  • Framework mechanics: declarative UI, BuildContext, lifecycle, the three trees, layout, and painting.
  • Application design: local and shared state, architecture, data, networking, persistence, and security.
  • Production engineering: performance, testing, CI/CD, and observability.

You now have the map. The later chapters zoom into each part without changing the central idea: state changes; Flutter rebuilds a description; the framework efficiently updates what the user sees.

Primary references

Display Options
Appearance
Text Size
100%