Flutter for Beginners: Styling with Themes, Text, and Assets

Capítulo 5

Estimated reading time: 8 minutes

+ Exercise

Why themes matter (and what they control)

In Flutter, styling can be applied at two levels: globally (with a theme) and locally (per widget). A global theme helps you keep colors, typography, and component styles consistent across screens without repeating the same TextStyle and color values everywhere. Local styling is still useful for one-off tweaks, but the goal is to let the theme do most of the work.

The main entry point for app-wide styling is ThemeData, usually provided to MaterialApp via the theme property. Modern Flutter styling is centered around ColorScheme (a structured set of semantic colors like primary, surface, error) and TextTheme (a set of named text styles like titleLarge, bodyMedium).

Step 1: Define a consistent ThemeData

Create a theme file

Start by creating a dedicated file for your theme so it stays reusable and easy to maintain.

  • Create lib/theme/app_theme.dart
  • Define a light theme (and optionally a dark theme)
import 'package:flutter/material.dart';

class AppTheme {
  static const Color seedColor = Color(0xFF4F46E5); // Indigo-ish

  static ThemeData light() {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: seedColor,
      brightness: Brightness.light,
    );

    return ThemeData(
      useMaterial3: true,
      colorScheme: colorScheme,
      scaffoldBackgroundColor: colorScheme.surface,
      textTheme: _textTheme(colorScheme),
      appBarTheme: AppBarTheme(
        backgroundColor: colorScheme.surface,
        foregroundColor: colorScheme.onSurface,
        elevation: 0,
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        ),
      ),
      inputDecorationTheme: InputDecorationTheme(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      cardTheme: CardTheme(
        elevation: 0,
        color: colorScheme.surfaceContainerHighest,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      ),
    );
  }

  static TextTheme _textTheme(ColorScheme colorScheme) {
    return TextTheme(
      titleLarge: TextStyle(
        fontSize: 22,
        fontWeight: FontWeight.w700,
        color: colorScheme.onSurface,
      ),
      bodyMedium: TextStyle(
        fontSize: 16,
        height: 1.4,
        color: colorScheme.onSurface,
      ),
      labelLarge: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w600,
        color: colorScheme.onPrimary,
      ),
    );
  }
}

Apply the theme in MaterialApp

In your app entry widget, pass the theme to MaterialApp. This makes the theme available through Theme.of(context) and used automatically by many Material widgets.

import 'package:flutter/material.dart';
import 'theme/app_theme.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: AppTheme.light(),
      home: const HomeScreen(),
    );
  }
}

Use semantic colors instead of hard-coded colors

Prefer Theme.of(context).colorScheme over raw hex colors inside widgets. This keeps your UI adaptable (for example, if you later add a dark theme).

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

final cs = Theme.of(context).colorScheme;

Container(
  padding: const EdgeInsets.all(16),
  decoration: BoxDecoration(
    color: cs.primaryContainer,
    borderRadius: BorderRadius.circular(16),
  ),
  child: Text(
    'Welcome',
    style: TextStyle(color: cs.onPrimaryContainer),
  ),
);

Step 2: Apply TextStyle the right way (TextTheme first)

Use TextTheme for consistent typography

Instead of styling every Text widget manually, pull styles from the theme. This gives you consistent font sizes and weights across the app.

Text(
  'Profile',
  style: Theme.of(context).textTheme.titleLarge,
);

Text(
  'Edit your details below.',
  style: Theme.of(context).textTheme.bodyMedium,
);

Override only what you need with copyWith

If a specific widget needs a small variation (like a different color), keep the base style from the theme and override only the changed properties.

final tt = Theme.of(context).textTheme;
final cs = Theme.of(context).colorScheme;

Text(
  'Warning: Unsaved changes',
  style: tt.bodyMedium?.copyWith(
    color: cs.error,
    fontWeight: FontWeight.w600,
  ),
);

Widget-level styling vs. theme-level styling

Use theme-level styling for patterns you want everywhere (buttons, inputs, cards). Use widget-level styling for special cases. A good rule: if you copy/paste the same style twice, it probably belongs in the theme or a reusable component.

Step 3: Create reusable UI components that respect the theme

Build a reusable primary button

Even with ElevatedButtonThemeData, you may want a reusable widget to standardize spacing, loading states, and icons. The key is to rely on the theme for colors and typography.

import 'package:flutter/material.dart';

class PrimaryButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final bool isLoading;

  const PrimaryButton({
    super.key,
    required this.label,
    required this.onPressed,
    this.isLoading = false,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 12),
          child: isLoading
              ? const SizedBox(
                  height: 18,
                  width: 18,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : Text(label),
        ),
      ),
    );
  }
}

Create a themed info card

This component uses CardTheme and the app’s ColorScheme so it automatically matches your design.

import 'package:flutter/material.dart';

class InfoCard extends StatelessWidget {
  final String title;
  final String subtitle;
  final IconData icon;

  const InfoCard({
    super.key,
    required this.title,
    required this.subtitle,
    required this.icon,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final tt = Theme.of(context).textTheme;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: cs.primaryContainer,
                borderRadius: BorderRadius.circular(14),
              ),
              child: Icon(icon, color: cs.onPrimaryContainer),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(title, style: tt.titleLarge?.copyWith(fontSize: 18)),
                  const SizedBox(height: 4),
                  Text(subtitle, style: tt.bodyMedium),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Practical usage example

Column(
  children: [
    InfoCard(
      title: 'Storage',
      subtitle: '2.1 GB used of 5 GB',
      icon: Icons.cloud,
    ),
    const SizedBox(height: 12),
    PrimaryButton(
      label: 'Upgrade',
      onPressed: () {},
    ),
  ],
);

Step 4: Load image assets (and configure pubspec.yaml)

Organize your assets folder

Create a predictable structure so assets stay manageable as your app grows.

  • assets/images/ for PNG/JPG/WebP
  • assets/icons/ for icon images (if not using icon fonts)
  • assets/illustrations/ for larger visuals

Configure pubspec.yaml for assets

Flutter only bundles assets that are declared in pubspec.yaml. Indentation matters because YAML is whitespace-sensitive.

flutter:
  uses-material-design: true

  assets:
    - assets/images/
    - assets/illustrations/

With the folder approach above, any file placed inside those directories will be bundled (for example assets/images/logo.png).

Display an asset image

Image.asset(
  'assets/images/logo.png',
  width: 120,
  fit: BoxFit.contain,
);

Use assets in a decoration

For backgrounds or cards, you can use DecorationImage.

Container(
  height: 180,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(16),
    image: const DecorationImage(
      image: AssetImage('assets/illustrations/banner.jpg'),
      fit: BoxFit.cover,
    ),
  ),
);

Step 5: Add and use custom fonts

Place font files in your project

Create a folder like assets/fonts/ and add your font files (commonly .ttf or .otf). Many font families come with multiple weights (Regular, Medium, Bold). Keep the filenames clear.

Configure pubspec.yaml for fonts

Declare a font family and list each font file with its weight. The family name is what you’ll reference in code.

flutter:
  uses-material-design: true

  fonts:
    - family: Inter
      fonts:
        - asset: assets/fonts/Inter-Regular.ttf
          weight: 400
        - asset: assets/fonts/Inter-Medium.ttf
          weight: 500
        - asset: assets/fonts/Inter-Bold.ttf
          weight: 700

Apply the font globally via ThemeData

You can set fontFamily on ThemeData so all text uses it by default.

return ThemeData(
  useMaterial3: true,
  colorScheme: colorScheme,
  fontFamily: 'Inter',
  textTheme: _textTheme(colorScheme),
);

Apply a custom font to a specific widget (optional)

If you don’t want a global font, you can apply it per widget using TextStyle.

Text(
  'Special Heading',
  style: const TextStyle(
    fontFamily: 'Inter',
    fontSize: 24,
    fontWeight: FontWeight.w700,
  ),
);

Troubleshooting: common asset and font loading mistakes

1) “Unable to load asset” at runtime

  • Check the path string exactly matches the file location and filename (case-sensitive on many systems).
  • Confirm the asset is declared in pubspec.yaml under flutter: and that indentation is correct.
  • If you added the asset while the app was running, do a full restart (hot reload may not pick up new assets).

2) Assets declared but still not found

  • Make sure you used a trailing slash when declaring a directory (for example assets/images/).
  • Verify the asset is inside the declared directory (not a sibling folder).
  • Run flutter pub get after editing pubspec.yaml.

3) YAML indentation errors

  • YAML uses spaces, not tabs. A single wrong indentation level can break asset discovery.
  • Ensure assets: and fonts: are aligned under flutter:.
  • Each list item must be indented consistently (for example two spaces under assets:).

4) Custom font doesn’t apply

  • Confirm the family name in pubspec.yaml matches exactly what you use in fontFamily.
  • Ensure the font files exist at the specified paths.
  • If only some weights work, verify you declared the correct weight values and that the files are the intended weights.
  • Do a full restart after adding fonts.

5) Text looks different on iOS vs Android

  • Without a custom font, platforms may render default fonts differently. Setting a custom fontFamily in the theme helps unify appearance.
  • Check that you’re using TextTheme styles consistently rather than mixing many ad-hoc TextStyle definitions.

Practical checklist: make styling consistent across your app

  • Define ColorScheme once and reference it via Theme.of(context).colorScheme.
  • Define a small, intentional TextTheme and use it everywhere.
  • Theme common components (buttons, inputs, cards) in ThemeData to reduce repetition.
  • Create reusable widgets for repeated patterns (cards, list rows, buttons) that rely on the theme.
  • Declare assets and fonts in pubspec.yaml, then full restart after changes.

Now answer the exercise about the content:

In a Flutter app, what is the main benefit of using ThemeData with ColorScheme and TextTheme instead of styling each widget with hard-coded colors and TextStyle values?

You are right! Congratulations, now go to the next page

You missed! Try again.

A global theme centralizes colors and typography via ColorScheme and TextTheme, reducing repeated values and improving consistency and adaptability (like adding dark mode). Widget-level overrides are still possible when needed.

Next chapter

Flutter for Beginners: Handling User Input with Forms and Gestures

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
42%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.