From Dart to Flutter

How widgets consume Dart, what a build method really does, where state lives, and what changes in the Flutter toolchain.

Widgets are Dart values

import 'package:flutter/material.dart';

// a widget is an immutable description, not the thing on screen
class PriceTag extends StatelessWidget {
  const PriceTag({super.key, required this.cents, this.currency = 'EUR'});

  final int cents;
  final String currency;

  @override
  Widget build(BuildContext context) {
    // build runs often and must be cheap and free of side effects
    final label = '${(cents / 100).toStringAsFixed(2)} $currency';

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8),
      child: Text(label, style: Theme.of(context).textTheme.titleMedium),
    );
  }
}

// a stateful widget keeps mutable state in a separate State object
class Counter extends StatefulWidget {
  const Counter({super.key, this.start = 0});
  final int start;

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  late int count = widget.start;

  void _increment() {
    setState(() => count++);       // schedules a rebuild of this subtree
  }

  @override
  void dispose() {
    super.dispose();               // release controllers and listeners here
  }

  @override
  Widget build(BuildContext context) => Row(
        children: [
          Text('$count'),
          IconButton(onPressed: _increment, icon: const Icon(Icons.add)),
        ],
      );
}
  • A build method must not perform I/O, start timers or mutate state. Flutter calls it whenever the framework decides to, which can be many times per second.
  • Everything you learned about closures applies directly: an inline callback captures the surrounding scope, and a stale capture is a stale UI.
  • const constructors let Flutter skip rebuilding a subtree entirely. Marking a widget const is the cheapest optimisation available.
  • setState is only for local state. It rebuilds the subtree under that State object and nothing above it.

Where state lives

ScopeToolUse for
One widgetStatefulWidget plus setStateA counter, a toggle, a form field
A subtreeInheritedWidget or a provider packageTheme, locale, a repository
The whole appA state management packageAuthentication, cart, settings
Persistentshared_preferences, a databaseAnything that survives a restart
Ephemeral UIA FutureBuilder or StreamBuilderLoading states driven by an async call
// a FutureBuilder turns an async value into a widget tree
class OrderList extends StatelessWidget {
  const OrderList({super.key, required this.repository});
  final Future<List<Order>> Function() repository;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Order>>(
      // create the future once, outside build, or it restarts on every rebuild
      future: _future ??= repository(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return Center(child: Text('failed: ${snapshot.error}'));
        }
        final orders = snapshot.data ?? const <Order>[];
        if (orders.isEmpty) return const Center(child: Text('nothing yet'));
        return ListView.builder(
          itemCount: orders.length,
          itemBuilder: (context, i) => ListTile(title: Text(orders[i].sku)),
        );
      },
    );
  }

  static Future<List<Order>>? _future;
}

class Order {
  const Order(this.sku);
  final String sku;
}

The single most common Flutter bug is creating a Future or a controller inside build. Every rebuild starts a new request or a new animation, and the results arrive out of order. Create it once, in initState or outside the widget, and read it in build.

What changes in the toolchain

# pubspec.yaml for a Flutter package
name: shop_app
environment:
  sdk: ^3.5.0

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true
  assets:
    - assets/images/
  fonts:
    - family: Inter
      fonts:
        - asset: assets/fonts/Inter-Regular.ttf
flutter doctor                  # check the SDK, the toolchains and the devices
flutter create my_app
flutter pub get
flutter run -d chrome
flutter test
flutter analyze
flutter build apk --release
flutter build web --release
flutter pub run build_runner build --delete-conflicting-outputs
  • The Dart language, the analyzer, the package manager and the testing idioms are the same. Only the widget layer and the platform toolchain are new.
  • A Flutter test runs on the Dart VM with a test binding, so unit tests of your domain code need no device and no emulator.
  • flutter analyze with flutter_lints is the lint gate. Treat it as a build step, not a suggestion.
  • Almost everything that makes classic Dart slow applies: rebuilding a large subtree, allocating in build, and doing synchronous work on the UI isolate.
💡
Move any heavy computation off the UI isolate. A synchronous loop that takes a hundred milliseconds in a CLI tool takes a hundred milliseconds of dropped frames in an application, so the same compute or Isolate.run advice from plain Dart applies unchanged.

FAQ

Do I need to learn a state management library first?
No. Learn StatefulWidget and setState, then InheritedWidget, so you understand what a library replaces. Most of the confusion around state management comes from adopting a package before understanding the built-in mechanism.
Is Flutter's Dart different from server Dart?
The language is identical. The differences are the library set, the isolation model and the compilation target: Flutter ships AOT-compiled native code or JavaScript, and the UI code must not touch dart:io when it targets the web.

Classes, futures and streams Isolates, compilation and packaging

Last refreshed 2026-09-18.