Flutter Widget Basics
Everything you can see in a Flutter application is a widget. There is no separate “view” layer, no XML layout file, and no way to reach into the screen and change a pixel: you describe what should be on screen as a tree of immutable objects, and Flutter rebuilds the parts of that tree that need to change. That sounds abstract until you see how small the pieces are - a Text is a widget, a Padding around it is a widget, a Row that places them side by side is a widget - and how few of them you need for a complete screen.
A Flutter application is a tree of widgets rooted at your runApp call.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
home: Scaffold(
appBar: AppBar(title: const Text('Demo')),
body: const Center(child: Text('Hello, Flutter!')),
),
);
}
}
MaterialApp is the application root: it installs the navigator, the theme, and the directionality that every widget depends on. Inside it, Scaffold supplies the page furniture - an app bar, a body, a floating action button - and the body holds your own widget tree.
The key mental model is this: a widget is a description, not a thing. It is an immutable configuration object that says “put a text here, with this padding, in this alignment”. The element tree that Flutter builds from those descriptions is what actually occupies space and paints pixels.
StatelessWidget
A stateless widget has no state of its own. It rebuilds whenever its parent rebuilds, and its build method is a pure function of its configuration.
class Greeting extends StatelessWidget {
const Greeting({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) {
return Text('Hello, $name');
}
}
Use stateless widgets for anything that is fully determined by the arguments it is given. Most of your UI will be stateless widgets.
StatefulWidget
A stateful widget owns a State object that survives rebuilds. When the state changes, the framework calls setState, which marks the element dirty and rebuilds that subtree.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Tapped $_count times'),
ElevatedButton(onPressed: _increment, child: const Text('Increment')),
],
);
}
}
The rule of thumb: if a value has to survive a rebuild, it belongs in State, not in a local variable of build.
Flutter offers two families of layout widgets, and choosing the right one is most of layout work.
Flex family
Row and Column are flex widgets: they lay out their children along the main axis. mainAxisAlignment controls distribution along that axis, crossAxisAlignment controls alignment across it, and mainAxisSize decides whether the flex takes all the space it is offered or only what it needs.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
Icon(Icons.star),
Text('12 reviews'),
Icon(Icons.star),
],
)
Expanded and Flexible let a child take a share of the remaining space, and Spacer is an Expanded with no child.
SizedBox, Container, Padding
SizedBox(width: 40, height: 20)fixes a size.Padding(padding: const EdgeInsets.all(16), child: ...)adds space around a child.Containeris a convenience widget that combines padding, margin, decoration, constraints, and alignment.
Stack, Positioned, and Align
Stack layers children on top of each other; Positioned places a child at an offset inside the stack, and Align positions it by alignment instead.
Stack(
children: [
Image.asset('assets/photo.png'),
Positioned(
left: 12,
bottom: 12,
child: Chip(label: const Text('Sunset')),
),
],
)
ListView and GridView
ListView builds a scrollable list. ListView.builder creates children lazily, which is what you want for long or unbounded data.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
leading: const Icon(Icons.circle),
title: Text(items[index].name),
onTap: () => select(items[index]),
),
)
| Widget | Purpose |
|---|---|
Text / RichText | Display text; Text is the usual choice |
TextField | Accept user text input |
Image | Display an image from assets, network, or memory |
Icon | Display a material icon |
ElevatedButton / OutlinedButton / TextButton | The three button emphasis levels |
Checkbox / Switch / Slider | Capture a boolean or a bounded value |
Card / ListTile | Material surfaces with a title and subtitle |
AppBar | The top bar of a Scaffold |
CircularProgressIndicator | Show that something is in flight |
AlertDialog / BottomSheet / SnackBar | Show information and collect decisions |
Widgets compose by nesting, and each level adds one constraint to the one below it. A Column imposes a vertical constraint on its children; a Padding insets its child; a Expanded asks for a share of the space its parent has left.
Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_outline, size: 48),
const SizedBox(height: 12),
Text('All done', style: Theme.of(context).textTheme.titleLarge),
],
),
),
)
Theme.of(context) is the idiomatic way to read the current theme: colours, text styles, and spacing all come from the theme rather than from hard-coded values, so a widget built this way looks right in both light and dark mode.
Because widgets are immutable descriptions, Flutter can skip rebuilding a subtree when the new widget is identical to the old one. A const constructor tells the compiler that a widget is canonical, so two const Text('x') widgets are the same object and the subtree is not rebuilt.
const Center(child: Text('Static'));
Make constructors const wherever the arguments allow it. It is not a micro-optimisation; it is the mechanism that keeps large widget trees cheap.
Approach: The question asks you to predict what the framework does with a specific widget tree, so reason from the rebuild rules rather than from a general knowledge of Flutter: a widget is a description, only a change of description triggers a rebuild, and const canonicalisation is what makes “no change” detectable.
Steps:
const Center(child: Text('Static'))is canonical, so theCenterwidget instance is identical on every build and the framework can skip rebuilding the subtree entirely.Centerpasses loose constraints to its child and sizes itself to the child, so theTextis laid out at the centre of whatever space theCenterreceives.Centerpositions its child by alignment (0.5, 0.5) of the available space, so the text ends up in the middle, not at the top-left.- If the
Texthad not beenconst, a new widget instance would be created on each build, the subtree would be rebuilt, and the result would look identical but cost more.
Answer: The text is rendered in the centre of the available space, the subtree is not rebuilt because the const widget is canonical, and the Center takes whatever space its parent offers while positioning the child at the alignment centre.
Now that you can describe a screen as a tree of widgets, the next question is where the data lives: how you hold changing values, and how a change in one part of the tree is allowed to update another part without the two being entangled.
