Skip to main content

Theme | Artigo | Flutter

  • Theme | Artigo | Flutter

alt text

Você já quis criar um aplicativo Flutter capaz de alternar entre diferentes temas, como Brasil, Estados Unidos e Índia, com apenas um toque? Neste artigo, você vai aprender passo a passo como estruturar seu projeto Flutter para oferecer suporte a múltiplos temas dinâmicos, mantendo seu código organizado, reativo e persistente.

🧠 Conceito: Por que usar temas dinâmicos?

Criar temas dinâmicos permite ao usuário personalizar a aparência do aplicativo de forma visual e interativa. Além de melhorar a experiência, facilita a internacionalização e branding da sua aplicação.

🧱 Arquitetura do Projeto

A estrutura do projeto é baseada em MVVM (Model-View-ViewModel) e separa as responsabilidades em camadas bem definidas:

/bootstrap

└── app_initializer.dart

/core

└── theme/

├── theme_controller.dart

├── theme_context.dart

├── theme_model.dart

├── theme_presets.dart

└── theme_type.dart

/view

└── home_view.dart

/view_model

└── theme_view_model.dart

main.dart

my_app.dart

🔨 Passo a Passo

1. Crie o modelo de tema

Arquivo: theme_model.dart

/// Este arquivo define a classe [ThemeModel], que representa um modelo de tema personalizado.
/// A responsabilidade da classe é encapsular as principais cores usadas na aplicação,
/// permitindo a criação de múltiplos temas com nomes distintos.
///
/// Essa abordagem é útil para aplicar temas dinâmicos ou alternar entre diferentes paletas
/// de cores na interface do aplicativo, mantendo o código mais organizado e reutilizável.
library;

import 'package:flutter/material.dart';

/// Modelo de dados para representar um tema personalizado da aplicação.
class ThemeModel {
/// Nome identificador do tema. Pode ser usado para seleção ou exibição.
final String name;

/// Cor primária do tema. Geralmente usada como cor principal da UI.
final Color primary;

/// Cor secundária do tema. Pode ser usada para botões, destaques ou outros elementos de suporte.
final Color secondary;

/// Cor terciária do tema. Utilizada como cor complementar ou decorativa.
final Color tertiary;

/// Cor de texto do tema. Geralmente usada para textos e elementos de interface.
final Color text;

/// Emoji associado ao tema. Pode ser usado para representar visualmente o tema.
final String emoji;

/// Construtor constante que permite a criação de um tema com nome e cores definidas.
const ThemeModel({
required this.name,
required this.primary,
required this.secondary,
required this.tertiary,
required this.text,
required this.emoji,
});
}

2. Defina os tipos de temas disponíveis

Arquivo: theme_type.dart

/// Este arquivo define a enumeração [ThemeType], que representa os diferentes tipos
/// de temas disponíveis na aplicação.
///
/// Cada valor da enum corresponde a um país específico e pode ser utilizado para
/// aplicar um conjunto de cores temáticas personalizado, associado ao país selecionado.
///
/// Essa enumeração é útil para identificar e alternar entre temas de forma clara e segura.
library;

/// Enumeração que define os tipos de temas disponíveis por país.
enum ThemeType {
/// Tema com cores e estilo inspirados no Brasil.
brazil,

/// Tema com cores e estilo inspirados nos Estados Unidos.
unitedStates,

/// Tema com cores e estilo inspirados na Índia.
india,
}

3. Crie os temas predefinidos

Arquivo: theme_presets.dart

/// Este arquivo define a classe [ThemePresets], responsável por armazenar os temas
/// predefinidos da aplicação.
///
/// Cada tema está associado a um valor da enum [ThemeType] e implementado como um
/// [ThemeModel], contendo cores específicas inspiradas em diferentes países.
///
/// A estrutura permite acessar rapidamente os temas disponíveis de forma organizada e segura,
/// promovendo reutilização e centralização da configuração de temas.
library;

import 'package:flutter/material.dart';
import 'theme_model.dart';
import 'theme_type.dart';

/// Classe utilitária que agrupa os temas predefinidos da aplicação.
class ThemePresets {
/// Mapa contendo todos os temas disponíveis, associados ao respectivo [ThemeType].
///
/// Essa estrutura facilita a seleção de temas com base em uma chave clara e semântica.
static final Map<ThemeType, ThemeModel> all = {
/// Tema do Brasil: verde, amarelo e azul.
ThemeType.brazil: const ThemeModel(
name: 'Brasil',
primary: Color(0xFF009739),
secondary: Color(0xFFFFCC29),
tertiary: Color(0xFF002776),
text: Color(0xFFFFFFFF),
emoji: '🇧🇷',
),

/// Tema dos Estados Unidos: azul escuro, branco e vermelho.
ThemeType.unitedStates: const ThemeModel(
name: 'Estados Unidos',
primary: Color(0xFF3C3B6E),
secondary: Color(0xFFFFFFFF),
tertiary: Color(0xFFB22234),
text: Color(0xFF000000),
emoji: '🇺🇸',
),

/// Tema da Índia: laranja, branco e verde.
ThemeType.india: const ThemeModel(
name: 'Índia',
primary: Color(0xFFFF9933),
secondary: Color(0xFFFFFFFF),
tertiary: Color(0xFF138808),
text: Color(0xFF000000),
emoji: '🇮🇳',
),
};
}

4. Gerencie o estado do tema

Arquivo: theme_controller.dart

/// Este arquivo define a classe [ThemeController], responsável pelo gerenciamento
/// dinâmico de temas da aplicação, incluindo a seleção, persistência e notificação de mudanças.
///
/// Ele usa o padrão de gerenciamento de estado com `ChangeNotifier`
/// e persiste a seleção de tema com `SharedPreferences`, garantindo que o tema
/// escolhido pelo usuário seja mantido entre sessões da aplicação.
library;

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'theme_model.dart';
import 'theme_type.dart';
import 'theme_presets.dart';

/// Controlador de temas da aplicação.
///
/// Responsável por:
/// - Gerenciar o tema atual selecionado.
/// - Persistir a escolha do usuário.
/// - Notificar widgets sobre mudanças de tema.
class ThemeController with ChangeNotifier {
/// Instância atual do tema em uso.
late ThemeModel _colors;

/// Tipo do tema atualmente selecionado.
ThemeType _currentType = ThemeType.brazil;

/// Chave usada para armazenar a seleção do tema localmente.
static const _storageKey = 'app_theme_type';

/// Mapa contendo todos os temas disponíveis.
final Map<ThemeType, ThemeModel> _themes = ThemePresets.all;

/// Inicializa o controlador, carregando o tema salvo no armazenamento local.
/// Caso não haja valor salvo, define o tema padrão como o do Brasil.
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString(_storageKey);

final type = ThemeType.values.firstWhere(
(e) => e.name == stored,
orElse: () => ThemeType.brazil,
);

// Garante que o tema existe no mapa antes de usar
if (_themes.containsKey(type)) {
_colors = _themes[type]!;
} else {
_colors = _themes[ThemeType.brazil]!;
}

_currentType = type;
notifyListeners(); // Notifica a UI para reagir à alteração
}

// Getters para expor as propriedades do tema atual
Color get primaryColor => _colors.primary;
Color get secondaryColor => _colors.secondary;
Color get tertiaryColor => _colors.tertiary;
Color get textColor => _colors.text;
String get name => _colors.name;
String get emoji => _colors.emoji;
ThemeType get currentType => _currentType;

/// Getter auxiliar para inspeção dos temas disponíveis (útil para testes ou seletores de UI)
Map<ThemeType, ThemeModel> get themes => _themes;

/// Define um novo tema e persiste a escolha do usuário localmente.
void setTheme(ThemeType type) async {
if (_themes.containsKey(type)) {
_currentType = type;
_colors = _themes[type]!;
notifyListeners(); // Notifica a UI para atualizar com o novo tema
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_storageKey, type.name);
}
}
}

5. Acesse o tema com extensões no contexto

Arquivo: theme_context.dart

/// Este arquivo define uma extensão sobre [BuildContext] chamada [ThemeContext].
///
/// A responsabilidade da extensão é facilitar o acesso ao [ThemeController] por meio
/// do contexto do Flutter, utilizando o pacote `provider`.
///
/// Ela fornece dois métodos de acesso: um reativo (`watch`) e um estático (`read`),
/// permitindo ao desenvolvedor escolher entre escutar mudanças no tema ou apenas
/// acessar o valor atual, sem escutar atualizações.
///
/// Isso torna o código mais limpo, legível e reduz a repetição ao lidar com temas dinâmicos.
library;

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_controller.dart';

/// Extensão em [BuildContext] que facilita o acesso ao [ThemeController].
extension ThemeContext on BuildContext {
/// Acesso reativo ao [ThemeController], atualiza o widget ao detectar mudanças.
ThemeController get theme => watch<ThemeController>();

/// Acesso não reativo ao [ThemeController], útil quando não é necessário escutar mudanças.
ThemeController get themeStatic => read<ThemeController>();
}

6. Inicialize o app com o tema salvo

Arquivo: app_initializer.dart

/// Este arquivo define o widget [AppInitializer], responsável por inicializar
/// as configurações essenciais da aplicação antes de exibir a interface principal.
///
/// Ele cuida da carga assíncrona do [ThemeController], garantindo que o tema salvo
/// pelo usuário (persistido localmente) seja carregado corretamente antes da renderização
/// da aplicação. Após a inicialização, o controller e sua ViewModel são injetados
/// globalmente usando `MultiProvider`.

library;

import 'package:flutter/material.dart';
import 'package:flutter_exemplo_temas/view_model/theme_view_model.dart';
import 'package:provider/provider.dart';
import 'package:flutter_exemplo_temas/my_app.dart';
import 'package:flutter_exemplo_temas/core/theme/theme_controller.dart';

/// Widget responsável por carregar configurações iniciais da aplicação, como o tema.
class AppInitializer extends StatelessWidget {
const AppInitializer({super.key});

/// Carrega e inicializa o [ThemeController], buscando o tema salvo no armazenamento local.
Future<ThemeController> _loadTheme() async {
final controller = ThemeController();
await controller.init();
return controller;
}


Widget build(BuildContext context) {
return FutureBuilder<ThemeController>(
future: _loadTheme(),
builder: (context, snapshot) {
// Enquanto o tema ainda está sendo carregado, exibe um indicador de progresso.
if (!snapshot.hasData) return const CircularProgressIndicator();

final themeController = snapshot.data!;

// Injeta o ThemeController e a ThemeViewModel globalmente com MultiProvider.
return MultiProvider(
providers: [
ChangeNotifierProvider<ThemeController>.value(
value: themeController,
),
ChangeNotifierProvider<ThemeViewModel>(
create: (_) => ThemeViewModel(themeController),
),
],
child: const MyApp(),
);
},
);
}
}

7. Configure o widget principal

Arquivo: my_app.dart

/// Este arquivo define o widget [MyApp], que representa a raiz da árvore de widgets
/// da aplicação após a fase de inicialização.
///
/// A responsabilidade do [MyApp] é configurar o `MaterialApp`, definindo o título
/// da aplicação e a tela inicial a ser exibida.
///
/// Este widget pode ser estendido para configurar temas dinâmicos e rotas nominais
/// no futuro, tornando-o um ponto central para configurações globais da interface.

library;

import 'package:flutter/material.dart';
import 'package:flutter_exemplo_temas/view/home_view.dart';

/// Widget principal da aplicação que configura o `MaterialApp`.
class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
/// Desativa o banner de modo de depuração.
debugShowCheckedModeBanner: false,

/// Define o título da aplicação.
title: 'Flutter Exemplo Temas',

/// Página inicial exibida ao iniciar o app.
home: HomeView(),
);
}
}

8. Ponto de entrada

Arquivo: main.dart

/// Este é o ponto de entrada da aplicação Flutter.
///
/// Sua responsabilidade principal é iniciar a execução do app chamando `runApp()`
/// com o widget raiz da aplicação, neste caso o [AppInitializer].
///
/// O [AppInitializer] é responsável por configurar dependências iniciais, como
/// o tema da aplicação, provedores de estado e qualquer outra configuração necessária
/// antes de renderizar a interface principal do usuário.

library;

import 'package:flutter/material.dart';
import 'package:flutter_exemplo_temas/bootstrap/app_initializer.dart';

/// Função principal que inicializa a aplicação.
void main() => runApp(const AppInitializer());

🎨 Como Usar no UI

1. Mostrar a cor primária como fundo de Container

Container(
color: context.theme.primaryColor,
padding: const EdgeInsets.all(16),
child: const Text(
'Tema Ativo',
style: TextStyle(fontSize: 20),
),
)

2. Mudar o tema ao clicar em um botão

context.theme.setTheme(ThemeType.india);

📦 Benefícios

  • ✅ Suporte a múltiplos temas
  • ✅ Persistência com SharedPreferences
  • ✅ Extensível para temas escuros ou branding

🔗 Código Fonte e App de Exemplo

Acesse o repositório com o projeto completo e um app de exemplo funcional:

👉 Ver Código no GitHub

alt text

📌 Conclusão

Com essa estrutura, seu app Flutter estará pronto para oferecer múltiplos temas personalizados e persistentes. Esse tipo de experiência eleva o profissionalismo do app e mostra atenção aos detalhes visuais.

--