Introduction
Developers familiar with web frameworks often expect Flutter layout to work like HTML and CSS. There are similarities, but Flutter uses a widget tree and a constraint-based layout system rather than the browser’s document layout engine.
Understanding widgets, constraints, and composition is the foundation for building interfaces that adapt to phones, tablets, and desktop-sized windows.
1. Everything starts with widgets
A Flutter application is built by composing widgets. Some describe visual elements, such as Text and Icon; others arrange children, such as Row, Column, and Stack.
A simple screen might look like this:
import 'package:flutter/material.dart';
class ProductPage extends StatelessWidget {
const ProductPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: const Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Featured products',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 12),
Text('Browse our latest collection.'),
],
),
),
);
}
}
Scaffold provides a standard page structure, while Padding, Column, and Text compose the actual content. This small, composable approach is central to Flutter development.
2. Understand constraints
Flutter layout follows a useful rule of thumb: constraints flow down, sizes flow up, and parents position children.
A parent tells its child the range of permitted sizes. The child chooses a size within that range, and the parent positions it. Many layout bugs occur when developers assume a child can choose any size it wants.
For example, placing an unconstrained Column inside a vertically scrollable area may behave differently from placing it inside a bounded Expanded region. The correct structure depends on which parent provides the constraints.
When debugging a layout, ask:
- What constraints does this widget receive?
- What size does it choose?
- How does its parent position it?
- Is the widget inside a bounded or unbounded axis?
These questions are often more useful than adding arbitrary widths and heights.
3. Use Row, Column, and Expanded appropriately
Row lays out children horizontally; Column lays them out vertically. Expanded tells a flex parent to allocate remaining space to a child.
Row(
children: [
const Icon(Icons.shopping_bag),
const SizedBox(width: 12),
const Expanded(
child: Text(
'A product with a potentially long name',
overflow: TextOverflow.ellipsis,
),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.favorite_border),
),
],
)
Here, Expanded gives the text the available space after accounting for the icon and button. Without it, long content may overflow depending on the surrounding constraints.
Do not use Expanded everywhere. It is intended for flex layouts and should be used where allocating remaining space makes sense.
4. Make layouts responsive
Responsive interfaces should react to available space rather than assume that every device has the same dimensions.
LayoutBuilder lets a widget adapt to the constraints provided by its parent:
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 700;
return isWide
? const Row(
children: [
Expanded(child: NavigationPanel()),
Expanded(flex: 3, child: ContentPanel()),
],
)
: const Column(
children: [
NavigationPanel(),
ContentPanel(),
],
);
},
)
The breakpoint here is illustrative, not universal. Choose breakpoints based on where the design stops working well, not simply on device labels.
Also test text scaling, localization, window resizing, and content with unusually long labels.
5. Build reusable widgets without overengineering
Repeated UI should be extracted into widgets when doing so improves readability, reuse, or testability.
class SectionHeading extends StatelessWidget {
const SectionHeading({
required this.title,
super.key,
});
final String title;
@override
Widget build(BuildContext context) {
return Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
);
}
}
Using Theme.of(context) helps the widget follow the application’s visual theme. Passing data through constructor parameters also makes dependencies explicit.
A widget does not need a separate class merely because it contains a few lines of UI. Extract components where there is a meaningful boundary.
6. Test layouts on more than one screen size
A screen that looks correct on a single emulator may fail on smaller devices or wider windows. Test representative constraints and consider automated widget tests for important responsive behavior.
Pay attention to overflow warnings, keyboard appearance, scrolling, accessibility semantics, and orientation changes. A layout should remain usable when the available space changes.
Conclusion
Flutter becomes easier to reason about when you treat the interface as a composition of widgets governed by constraints. Learn the behavior of common layout widgets, adapt to available space, and create reusable components around meaningful UI responsibilities.