Debugging & DevTools
Every non-trivial app eventually misbehaves, and Flutter gives you a genuinely strong set of tools for figuring out why: simple console logging, a full browser-based suite called Flutter DevTools, a Widget Inspector for visually exploring your running app, and your IDE's regular debugger. This lesson walks through each, plus one of the most common real bugs you'll actually run into — a layout overflow — and how these tools help you fix it.
Quick Console Debugging: print() vs debugPrint()
The simplest debugging tool is printing a value to the console. Dart's built-in print()
works, but Flutter also provides debugPrint(), which is usually the better default.
Example: Logging a Value During Development
final List<String> items = fetchLongItemList();
debugPrint('Fetched ${items.length} items: $items');
The reason debugPrint() is often preferred over plain print() is a
practical one: Android's system log truncates very long single lines of output, silently cutting
off large messages. debugPrint() automatically throttles and batches large output so
it isn't truncated, which matters more than it sounds the first time you print a large JSON object
or a long list and can't figure out why the end of your log line is missing.
Flutter DevTools: A Suite of Tools in Your Browser
Flutter DevTools is a collection of performance and debugging tools that open in a regular browser tab while your app runs on a device or emulator. VS Code and Android Studio launch it automatically the moment you start a debug session — look for a "DevTools" or "Open DevTools" option in the debug toolbar. It can also be launched manually from a terminal:
flutter pub global activate devtools
flutter pub global run devtools
DevTools includes panes for the widget tree, performance timing, memory usage, network requests, and logging — all connected live to your running app, updating as you interact with it.
The Widget Inspector: Exploring Your Running App Visually
The Widget Inspector pane inside DevTools shows the entire widget tree of your
currently running app, and lets you click any widget directly on the device screen to jump straight
to its position in that tree — showing its type, its properties, and its exact layout constraints.
This is genuinely useful for the layout questions covered in
Layout Widgets — instead of guessing why a
Row or Column is sized the way it is, you can select it live and read its
actual constraints and size.
Debug Paint: Visualizing Every Widget's Boundaries
A related toggle, "debug paint" (available as a button in DevTools, or by setting
debugPaintSizeEnabled = true in code), overlays a colored border and padding
indicator around every widget currently on screen. It turns an otherwise invisible layout
into something you can literally see — useful for spotting unexpected padding, a widget that's
larger or smaller than you assumed, or overlapping elements that are hard to reason about from code
alone.
Breakpoints and Stepping Through Code
Beyond Flutter-specific tools, ordinary debugger usage still applies: set a breakpoint on a line in VS Code or Android Studio by clicking the gutter next to it, then run your app in debug mode. Execution pauses at that line, letting you inspect variable values, step line by line, and step into or over function calls — the same debugging workflow you'd use in most other languages, now connected to your live Flutter app.
A Common Real Bug: RenderFlex Overflowed
One of the most common errors a Flutter beginner runs into is a bright yellow-and-black striped banner in the app, paired with a console error like:
A RenderFlex overflowed by 42 pixels on the right.
This happens when a Row (or Column) tries to lay out children whose
combined natural width is wider than the space actually available — commonly a Row
containing a long piece of text plus an icon, where the text alone is already wider than the
screen. The Widget Inspector immediately identifies which specific Row or
Column is overflowing by highlighting it in the tree. The two most common fixes,
both covered in Layout Widgets, are wrapping the
oversized child in Expanded (so it shrinks to fit the available space) or wrapping
the whole row in a SingleChildScrollView (so it scrolls instead of overflowing) when
the content genuinely doesn't fit on screen at once.
Example: Fixing an Overflowing Row With Expanded
// Before: long text can overflow a narrow Row
Row(
children: [
Icon(Icons.info),
Text('A surprisingly long label that does not fit'),
],
)
// After: Expanded lets the text wrap/shrink within the remaining space
Row(
children: [
Icon(Icons.info),
Expanded(
child: Text('A surprisingly long label that does not fit'),
),
],
)