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
buildmethod 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.
constconstructors let Flutter skip rebuilding a subtree entirely. Marking a widgetconstis the cheapest optimisation available.setStateis only for local state. It rebuilds the subtree under thatStateobject and nothing above it.
Where state lives
| Scope | Tool | Use for |
|---|---|---|
| One widget | StatefulWidget plus setState | A counter, a toggle, a form field |
| A subtree | InheritedWidget or a provider package | Theme, locale, a repository |
| The whole app | A state management package | Authentication, cart, settings |
| Persistent | shared_preferences, a database | Anything that survives a restart |
| Ephemeral UI | A FutureBuilder or StreamBuilder | Loading 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.ttfflutter 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 analyzewithflutter_lintsis 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.Related
Classes, futures and streams Isolates, compilation and packaging
Last refreshed 2026-09-18.