This episode builds the foundation of Flutter UI: the difference between StatelessWidget and StatefulWidget, basic layout with Row, Column, Stack, and Container, managing spacing, alignment, and constraints, and widget composition and builder patterns for building reusable UI.

Your first project is already running from episode 3. Now it's time to understand the building blocks of Flutter UI: widgets. Almost everything in Flutter is a widget — text, buttons, spacing, even layout itself. Episode 4 dissects four fundamentals: when to use StatelessWidget and StatefulWidget, basic layout with Row, Column, Stack, and Container, how to manage spacing, alignment, and constraints, and widget composition and builder patterns for building clean, reusable UI.
Flutter divides widgets into two families:
class JudulStatis extends StatelessWidget {
const JudulStatis({super.key});
@override
Widget build(BuildContext context) {
return const Text('Ini tidak berubah');
}
}
class TombolBerubah extends StatefulWidget {
const TombolBerubah({super.key});
@override
State<TombolBerubah> createState() => _TombolBerubahState();
}
class _TombolBerubahState extends State<TombolBerubah> {
bool _ditekan = false;
@override
Widget build(BuildContext context) {
return Text(_ditekan ? 'Ditekan' : 'Belum');
}
}The rule of thumb: start with StatelessWidget. Switch to StatefulWidget only when there's data that must change and trigger a rebuild — a decision that keeps rebuild costs low from the start.
The two most frequently used layout widgets:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Baris pertama'),
SizedBox(height: 8),
Text('Baris kedua'),
],
)SizedBox(height: 8) inserts vertical spacing between the texts. crossAxisAlignment: CrossAxisAlignment.start aligns the children to the left on the cross axis.
Stack(
children: [
Container(
width: 200,
height: 200,
color: Colors.blue,
),
Positioned(
left: 16,
top: 16,
child: Container(
padding: EdgeInsets.all(8),
color: Colors.white,
child: const Text('Badge'),
),
),
],
)Positioned places a child at a position relative to the Stack. EdgeInsets.all(8) adds uniform padding inside the Container, and the combination of the two is a common pattern for overlay badges.
Inside a Row or Column, SizedBox provides fixed spacing, while Spacer absorbs the remaining space:
Row(
children: [
const Text('Kiri'),
Spacer(),
const Text('Kanan'),
],
)A Spacer without parameters uses the default flex, so it pushes Text('Kanan') to the right edge. This is a quick pattern for two-sided rows.
Every widget receives constraints from its parent and returns its resulting size to the parent. Understanding this flow helps debug strange layouts:
mainAxisAlignment — positions children along the main axis.crossAxisAlignment — positions children along the cross axis.MainAxisAlignment.spaceEvenly distributes empty space evenly between and around children, whereas spaceBetween centers the distribution in the middle so the ends stick to the edges.
Flutter encourages composition: build complex widgets from simple ones rather than deriving subclasses. A habit to plant from the start is breaking a large build into small, reusable widgets, like this example:
class AvatarCard extends StatelessWidget {
const AvatarCard({super.key, required this.nama});
final String nama;
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(nama),
),
);
}
}AvatarCard packages Card, ListTile, and CircleAvatar into a single named widget. The parameter required this.nama makes this widget feel like a clear API — a pattern you'll use throughout the series.
Some widgets use builder functions instead of fixed children, for example ListView.builder for long lists:
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
)ListView.builder only builds the items visible on screen — efficient for lists with hundreds or thousands of entries, and the foundation we'll use when building real applications.
Key takeaways:
SizedBox provides fixed spacing; Spacer absorbs leftover space.ListView.builder builds items lazily for long lists.In the next episode 5 we discuss input, forms, and interaction — handling user input with TextField, buttons, and gestures, form validation and controllers, simple navigation and routing, and using SnackBars, dialogs, and modal bottom sheets. You start building truly interactive apps.