Learn Flutter - Theming & Styling
Episode 7 of 23

Learn Flutter - Theming & Styling

This episode transforms your app's appearance: ThemeData and Material Design theming, custom fonts, colors, and typography, dark mode support and adaptive UI, and styling widgets with decoration and responsive design.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

A functional app isn't necessarily pleasant to use. Episode 7 closes that gap: you'll learn to build a consistent visual identity through theming and styling. We cover ThemeData and Material Design theming, custom fonts, colors, and typography, dark mode support and adaptive UI, and styling widgets with decoration and responsive design. After this episode, your app will look like a real product, not a demo.

ThemeData and Material Design Theming

Injecting a Theme Across the Whole App

Instead of setting colors in every widget, define the theme once in MaterialApp:

Theme with ColorScheme from a seed color
MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.indigo,
    ),
    useMaterial3: true,
  ),
  home: const HomeScreen(),
)

ColorScheme.fromSeed(seedColor: Colors.indigo) generates a complete color palette from a single seed color — the pattern recommended by Material 3. useMaterial3: true enables the latest design tokens.

Accessing the Theme Across Widgets

Get colors from the theme
final colorScheme = Theme.of(context).colorScheme;
 
Container(
  color: colorScheme.primary,
  child: Text(
    'Tombol utama',
    style: TextStyle(color: colorScheme.onPrimary),
  ),
);

Theme.of(context).colorScheme retrieves the active palette. With this pattern, colors are never hardcoded inside widgets — theme changes propagate automatically across the whole app.

Custom Fonts, Colors, and Typography

Registering Fonts in pubspec.yaml

Custom fonts are registered in pubspec.yaml under the fonts block:

Register a font family
flutter:
  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-Regular.ttf
        - asset: assets/fonts/Poppins-Bold.ttf
          weight: 700

Once the font is registered in the flutter.fonts block, run flutter pub get and then use that font family.

Setting Up Typography in the Theme

Define a text theme so typography stays consistent:

Custom text theme
theme: ThemeData(
  textTheme: const TextTheme(
    headlineMedium: TextStyle(
      fontSize: 28,
      fontWeight: FontWeight.bold,
    ),
    bodyMedium: TextStyle(
      fontSize: 16,
      height: 1.5,
    ),
  ),
)

TextTheme defines styles for headlineMedium, bodyMedium, and so on. The whole app uses these styles without rewriting TextStyle everywhere.

Dark Mode Support and Adaptive UI

Light and Dark Themes Together

Provide two themes and Flutter will choose based on the system setting:

Automatic dark theme
MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.indigo,
      brightness: Brightness.dark,
    ),
  ),
  home: const HomeScreen(),
)

With theme and darkTheme populated, the app adapts to the device's light or dark mode automatically. Brightness.dark flips the palette so it stays contrasted.

Following the Platform

For platform-dependent decisions, use Theme.of(context).platform or MediaQuery:

Responsive to screen size
final lebar = MediaQuery.sizeOf(context).width;
 
if (lebar >= 600) {
  return const WideLayout();
}
return const NarrowLayout();

MediaQuery.sizeOf(context).width reads the current screen width. This pattern is the foundation of responsive design, which we'll cover in depth in episode 18.

Styling Widgets, Decoration, and Responsive Design

BoxDecoration for Rich Visuals

BoxDecoration gives a Container gradient colors, borders, and shadows:

A card with full decoration
Container(
  width: double.infinity,
  padding: const EdgeInsets.all(16),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.indigo, Colors.blue],
    ),
    borderRadius: BorderRadius.circular(12),
    boxShadow: const [
      BoxShadow(
        color: Colors.black26,
        blurRadius: 8,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: const Text('Kartu bergaya'),
)

BoxShadow adds depth and BorderRadius.circular(12) rounds the corners. double.infinity makes the Container as wide as its parent allows.

Avoid Hardcoding, Use a Scale

Define spacing constants in one place for consistency:

Centralized spacing constants
const double spacingKecil = 8;
const double spacingBesar = 24;
 
Padding(
  padding: EdgeInsets.all(spacingKecil),
  child: Text('Elemen dengan jarak konsisten'),
)

EdgeInsets.all(spacingKecil) uses the same constants across the whole app. Consistent spacing and colors are the hallmark of an app that looks professional.

Conclusion

Key takeaways:

  • Define the theme once in MaterialApp with ColorScheme.fromSeed.
  • Access colors via Theme.of(context) — don't hardcode colors in widgets.
  • Register custom fonts in pubspec.yaml and set a centralized TextTheme.
  • Provide theme and darkTheme for automatic dark mode support.
  • BoxDecoration brings visuals to life with gradients, borders, and shadows.
  • Centralize spacing constants for cross-screen consistency.

In the next episode 8 we discuss networking and data handling — HTTP requests with the http or dio package, JSON deserialization with dart:convert or json_serializable, async programming with Future and Stream, and production-ready loading states, error handling, and retry. Your app starts connecting to the outside world.