Flutter
02 / 04

Widgets: Stateless, Stateful & Layout

Flutter: Widgets & Layout

Everything in Flutter is a widget. The widget tree is rebuilt efficiently by Flutter's rendering engine. StatelessWidget for static UI; StatefulWidget for UI that changes over time.

StatelessWidget & StatefulWidget

// StatelessWidget — no mutable state
class GreetingCard extends StatelessWidget {
  final String name;
  final VoidCallback onTap;

  const GreetingCard({
    super.key,
    required this.name,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Text('Hello, $name!',
            style: Theme.of(context).textTheme.headlineSmall),
        ),
      ),
    );
  }
}

// StatefulWidget — mutable state with setState
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++; });  // triggers rebuild
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$_count', style: const TextStyle(fontSize: 48)),
        ElevatedButton(onPressed: _increment, child: const Text('+')),
      ],
    );
  }

  @override
  void initState() {
    super.initState();
    // called once when widget is inserted into tree
  }

  @override
  void dispose() {
    // cleanup: cancel timers, close streams
    super.dispose();
  }
}

Layout Widgets

// Column and Row — flex containers
Column(
  mainAxisAlignment: MainAxisAlignment.center,   // vertical axis
  crossAxisAlignment: CrossAxisAlignment.start,  // horizontal axis
  children: [
    Text('Title'),
    const SizedBox(height: 8),                   // spacer
    Text('Subtitle'),
  ],
)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text('Left'),
    Expanded(child: Text('Takes remaining space')),
    Text('Right'),
  ],
)

// Stack — overlapping widgets
Stack(
  alignment: Alignment.bottomCenter,
  children: [
    Image.network(imageUrl),
    Container(
      color: Colors.black54,
      padding: const EdgeInsets.all(8),
      child: const Text('Caption', style: TextStyle(color: Colors.white)),
    ),
  ],
)

// ListView — scrollable list
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(
    leading: const Icon(Icons.person),
    title: Text(items[index].name),
    subtitle: Text(items[index].email),
    trailing: const Icon(Icons.chevron_right),
    onTap: () => Navigator.pushNamed(context, '/detail', arguments: items[index]),
  ),
)

// GridView
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 1.5,
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) => ProductCard(product: products[index]),
)

Common Widgets

// Scaffold — basic screen structure
Scaffold(
  appBar: AppBar(
    title: const Text('Home'),
    actions: [IconButton(icon: const Icon(Icons.search), onPressed: () {})],
  ),
  body: const Center(child: Text('Content')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
  bottomNavigationBar: BottomNavigationBar(
    items: const [
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
      BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
    ],
    currentIndex: _selectedIndex,
    onTap: (index) => setState(() => _selectedIndex = index),
  ),
)

// Input
TextField(
  controller: _emailController,
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email',
    hintText: 'alice@example.com',
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(),
  ),
  validator: (v) => v?.contains('@') == true ? null : 'Invalid email',
)

// Async image loading
Image.network(
  imageUrl,
  loadingBuilder: (context, child, progress) =>
    progress == null ? child : const CircularProgressIndicator(),
  errorBuilder: (context, error, stack) => const Icon(Icons.broken_image),
)

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free