Flutter: Sistema de Temas e Estilos
O tema é o guarda-roupa de um app — um estilo unificado permite que os usuários reconheçam sua marca de relance.
📋 Pré-requisitos: Você já deve estar familiarizado com
- Aula 15: Sistema de Animação
1. O Que Você Vai Aprender
- Configuração completa do ThemeData: colorScheme, textTheme, appBarTheme, cardTheme
- Alternância dinâmica de tema: Claro / Escuro / temas de marca personalizados (gerenciado por Riverpod)
- ThemeExtension
<T>extensões de tokens de tema personalizados - Substituições de tema em nível de componente: Theme(data: ..., child: ...)
- ShopApp: sistema de tema de marca (cor de marca + modo escuro + acessibilidade de texto grande)
2. Uma História Real de Caos de Marca
(1) O Problema: Cem Páginas, Cem Azuis
O ShopApp do Bob é desenvolvido por 3 desenvolvedores, cada um definindo suas próprias cores: Colors.blue, Color(0xFF2196F3), Color(0xFF1976D2). Em 100 páginas existem 12 azuis diferentes, e os raios de canto dos botões vêm em 4px, 8px e 12px. O modo escuro é ainda mais caótico — alguns usam texto Colors.white em fundos escuros, outros usam Colors.grey[300]. O reconhecimento de marca é praticamente inexistente.
(2) A Solução com ThemeData
ThemeData define uniformemente todas as cores, fontes e formas — os componentes derivam estilos automaticamente do tema, e uma única mudança tem efeito global.
import 'package:flutter/material.dart';
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF0066CC)),
appBarTheme: const AppBarTheme(centerTitle: true),
cardTheme: CardThemeData(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
),
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: Unidade de Marca + Alternância Global
Após Bob definir o tema de marca do ShopApp, todas as páginas compartilharam automaticamente um estilo unificado. O modo escuro exigiu apenas uma troca de ThemeData — sem mudanças por página.
3. Sistema de Configuração do ThemeData
graph TD
MT[MaterialApp.theme] --> TD[ThemeData]
TD --> CS[ColorScheme]
TD --> TM[TextTheme]
TD --> AT[AppBarTheme]
TD --> CT[CardTheme]
TD --> TE[ThemeExtension]
TE --> Brand[BrandTokens]
CS --> |light| Light[Light Scheme]
CS --> |dark| Dark[Dark Scheme]
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(1) Sistema de Cores ColorScheme
| Papel de Cor | Uso | Exemplo |
|---|---|---|
primary |
Cor principal da marca (botões/tabs) | #0066CC |
onPrimary |
Texto sobre a cor primary | #FFFFFF |
secondary |
Cor de destaque | #FF6B35 |
surface |
Card/fundo | #FFFFFF/#1C1C1E |
onSurface |
Texto sobre surface | #1C1C1E/#FFFFFF |
error |
Cor de erro | #B3261E |
outline |
Bordas/divisores | #79747E |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Definições de cores de marca do ShopApp
import 'package:flutter/material.dart';
class ShopAppColors {
static const primary = Color(0xFF0066CC);
static const secondary = Color(0xFFFF6B35);
static const surface = Color(0xFFFFFFFF);
static const onSurface = Color(0xFF1C1C1E);
static final light = ColorScheme.light(
primary: primary,
secondary: secondary,
surface: surface,
onSurface: onSurface,
error: const Color(0xFFB3261E),
);
static final dark = ColorScheme.dark(
primary: const Color(0xFF80B3FF),
secondary: const Color(0xFFFF9B75),
surface: const Color(0xFF1C1C1E),
onSurface: const Color(0xFFE6E6E6),
error: const Color(0xFFF2B8B5),
);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
4. Configuração Completa do ThemeData
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Tema completo do ShopApp
import 'package:flutter/material.dart';
// Custom class definition source:
// - ShopAppColors: see Section 3 brand color definitions in this lesson
ThemeData shopAppLightTheme(ColorScheme colorScheme) => ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// AppBar
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
),
// Card
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
),
// Elevated/Filled Button
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
// Input
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
// Bottom Nav
navigationBarTheme: NavigationBarThemeData(
indicatorColor: colorScheme.primary.withOpacity(0.1),
),
// Text
textTheme: ThemeData.light().textTheme.apply(
fontFamily: 'Roboto',
),
);
ThemeData shopAppDarkTheme(ColorScheme colorScheme) => shopAppLightTheme(colorScheme).copyWith(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0D0D0F),
);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
5. Alternância Dinâmica de Tema
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Gerenciamento de tema com Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod riverpod_annotation shared_preferences
// ⚙️ Dev dependencies: flutter pub add --dev riverpod_generator build_runner
// Custom class definition source:
// - shopAppLightTheme/shopAppDarkTheme: see Section 4 in this lesson
// - ShopAppColors: see Section 3 brand color definitions in this lesson
enum AppThemeMode { light, dark, system }
@riverpod
class ThemeModeNotifier extends _$ThemeModeNotifier {
@override
AppThemeMode build() => AppThemeMode.system;
void setMode(AppThemeMode mode) {
state = mode;
SharedPreferences.getInstance().then((prefs) => prefs.setString('theme_mode', mode.name));
}
ThemeMode get flutterThemeMode => switch (state) {
AppThemeMode.light => ThemeMode.light,
AppThemeMode.dark => ThemeMode.dark,
AppThemeMode.system => ThemeMode.system,
};
}
// In MaterialApp
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeNotifierProvider).flutterThemeMode;
return MaterialApp(
theme: shopAppLightTheme(ShopAppColors.light),
darkTheme: shopAppDarkTheme(ShopAppColors.dark),
themeMode: themeMode,
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Página de configurações de alternância de tema
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod
// Custom class definition source:
// - AppThemeMode/themeModeNotifierProvider: see Section 5 Riverpod theme management in this lesson
class ThemeSettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(themeModeNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Appearance')),
body: ListView(children: AppThemeMode.values.map((mode) {
final label = switch (mode) {
AppThemeMode.light => 'Light',
AppThemeMode.dark => 'Dark',
AppThemeMode.system => 'System',
};
return RadioListTile<AppThemeMode>(
title: Text(label),
value: mode,
groupValue: current,
onChanged: (v) => ref.read(themeModeNotifierProvider.notifier).setMode(v!),
);
}).toList()),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Extensões Personalizadas com ThemeExtension
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Extensão de tokens de marca
import 'package:flutter/material.dart';
import 'dart:ui';
// Custom class definition source: BrandTokens is a custom ThemeExtension in this lesson
class BrandTokens extends ThemeExtension<BrandTokens> {
final Color salePrice;
final Color discount;
final Color rating;
final double cardRadius;
final double productImageRatio;
const BrandTokens({
this.salePrice = Colors.green,
this.discount = Colors.red,
this.rating = Colors.amber,
this.cardRadius = 12,
this.productImageRatio = 0.75,
});
@override
BrandTokens copyWith({Color? salePrice, Color? discount, Color? rating,
double? cardRadius, double? productImageRatio}) {
return BrandTokens(
salePrice: salePrice ?? this.salePrice,
discount: discount ?? this.discount,
rating: rating ?? this.rating,
cardRadius: cardRadius ?? this.cardRadius,
productImageRatio: productImageRatio ?? this.productImageRatio,
);
}
@override
BrandTokens lerp(covariant BrandTokens? other, double t) {
if (other == null) return this;
return BrandTokens(
salePrice: Color.lerp(salePrice, other.salePrice, t)!,
discount: Color.lerp(discount, other.discount, t)!,
rating: Color.lerp(rating, other.rating, t)!,
cardRadius: lerpDouble(cardRadius, other.cardRadius, t)!,
productImageRatio: lerpDouble(productImageRatio, other.productImageRatio, t)!,
);
}
}
// Use in ThemeData
ThemeData(extensions: const [BrandTokens()])
// Access in widget
final brand = Theme.of(context).extension<BrandTokens>()!;
Text('\$99.99', style: TextStyle(color: brand.salePrice));
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
7. Modo de Acessibilidade de Texto Grande
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Escalonamento de texto responsivo
import 'package:flutter/material.dart';
class AccessibleText extends StatelessWidget {
final String text;
final TextStyle? style;
const AccessibleText(this.text, {super.key, this.style});
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
// Clamp text scale factor for accessibility
final scaleFactor = mediaQuery.textScaleFactor.clamp(0.8, 2.0);
return MediaQuery(
data: mediaQuery.copyWith(textScaleFactor: scaleFactor),
child: Text(text, style: style),
);
}
}
// Global text scale limit in MaterialApp
builder: (context, child) {
final mq = MediaQuery.of(context);
return MediaQuery(
data: mq.copyWith(textScaleFactor: mq.textScaleFactor.clamp(0.8, 1.5)),
child: child!,
);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
8. Exemplo Completo: Sistema de Tema do ShopApp
import 'package:flutter/material.dart';
// Custom class definition source:
// - ShopAppColors: see Section 3 brand color definitions in this lesson
// - BrandTokens: see Section 6 ThemeExtension in this lesson
class ShopAppTheme {
static ThemeData light() => ThemeData(
useMaterial3: true,
colorScheme: ShopAppColors.light,
extensions: const [BrandTokens()],
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
cardTheme: CardThemeData(elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
inputDecorationTheme: InputDecorationTheme(filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8))),
);
static ThemeData dark() => ThemeData(
useMaterial3: true,
colorScheme: ShopAppColors.dark,
extensions: const [BrandTokens(
salePrice: Color(0xFF4CAF50),
discount: Color(0xFFEF5350),
)],
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
scaffoldBackgroundColor: const Color(0xFF0D0D0F),
cardTheme: CardThemeData(elevation: 1, color: const Color(0xFF1C1C1E),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Qual a diferença entre ColorScheme.fromSeed e definição manual? R: fromSeed gera automaticamente uma paleta harmônica completa (12 cores); a definição manual é mais flexível mas você deve garantir a harmonia. Recomenda-se fromSeed como ponto de partida, depois ajustar.
P: Theme.of(context) tem custo de desempenho? R: Sim, cada chamada dispara um rebuild. Armazene em cache fora do build:
final theme = Theme.of(context);, depois reutilize a variável theme.
P: Como substituir o tema global para um componente? R: Envolva a subárvore que precisa de substituição com
Theme(data: Theme.of(context).copyWith(...), child: widget).
P: Como lidar com cores de imagens/ícones no modo escuro? R: Use cores de papel do ColorScheme (como onSurface), evite cores fixas no código. Use colorFilter para colorir ícones SVG.
P: O que o método lerp do ThemeExtension faz? R: lerp implementa animação de interpolação durante a troca de tema — transições Claro→Escuro são suaves em vez de saltitantes.
P: Como fazer componentes de pacotes de terceiros seguirem temas personalizados? R: A maioria dos pacotes Material segue automaticamente o ThemeData. Componentes não-Material precisam de passagem manual de parâmetros de tema.
📖 Resumo
- ThemeData gerencia uniformemente cores, fontes e formas — uma mudança, efeito global
- ColorScheme define papéis semânticos de cor, auto-adaptando-se a Claro/Escuro
- Riverpod gerencia o estado de alternância de tema, SharedPreferences persiste a escolha
- ThemeExtension estende tokens de marca personalizados (cores de promoção/raios de canto/proporções)
- MediaQuery.textScaleFactor limita o escalonamento de texto, garantindo acessibilidade
📝 Exercícios
- Básico (dificuldade ⭐): Use ColorScheme.fromSeed para criar um tema de cor de marca e aplicá-lo ao MaterialApp.
- Intermediário (dificuldade ⭐⭐): Implemente alternância de três modos Claro/Escuro/Sistema usando Riverpod + persistência com SharedPreferences.
- Desafio (dificuldade ⭐⭐⭐): Crie um sistema de tema completo do ShopApp: extensão BrandTokens + temas duplos Claro/Escuro + modo de acessibilidade de texto grande + substituições de tema em nível de componente.