Skip to content

Instantly share code, notes, and snippets.

@tiagolpadua
Last active March 16, 2026 00:26
Show Gist options
  • Select an option

  • Save tiagolpadua/71ad9e78a54e86de2f964d8fee081ff4 to your computer and use it in GitHub Desktop.

Select an option

Save tiagolpadua/71ad9e78a54e86de2f964d8fee081ff4 to your computer and use it in GitHub Desktop.
Flutter Cheatsheet - Banco Douro App

Flutter Tests — Cheatsheet Rápido

Comandos Essenciais

flutter test                              # Rodar todos os testes
flutter test test/helpers/               # Rodar pasta específica
flutter test test/helpers/taxes_test.dart # Rodar arquivo específico
flutter test --coverage                   # Com coverage
flutter test --reporter expanded          # Output detalhado
dart run build_runner build               # Gerar mocks (mockito)
dart run build_runner watch               # Gerar mocks em modo watch

Estrutura de Pastas

banco_douro_app/
  lib/
    helpers/helper_taxes.dart
    models/account.dart
    providers/account_provider.dart
    ui/screens/login_screen.dart
  test/
    helpers/
      helper_taxes_test.dart     ← Testes de unidade
    models/
      account_test.dart          ← Testes de unidade
    providers/
      account_provider_test.dart ← Testes com mock
    ui/
      login_screen_test.dart     ← Testes de widget
    mocks/
      mocks.dart                 ← @GenerateMocks (fonte)
      mocks.mocks.dart           ← Gerado por build_runner

Teste de Unidade

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('NomeDaClasse', () {
    test('deve [resultado] quando [condição]', () {
      // Arrange
      final input = 5000.0;

      // Act
      final result = calcularImposto(input);

      // Assert
      expect(result, 12.5);
    });
  });
}

Setup e Teardown

group('MinhaClasse', () {
  late MinhaClasse instancia;

  setUp(() {           // Antes de CADA teste
    instancia = MinhaClasse();
  });

  tearDown(() {        // Depois de CADA teste
    instancia.dispose();
  });

  setUpAll(() async {  // UMA VEZ antes de todos
    await initDB();
  });

  tearDownAll(() async { // UMA VEZ depois de todos
    await closeDB();
  });

  test('...', () { ... });
});

Matchers Principais

// Valores
expect(x, equals(42));
expect(x, 42);               // shorthand
expect(x, isNull);
expect(x, isNotNull);
expect(x, isTrue);
expect(x, isFalse);

// Números
expect(x, greaterThan(0));
expect(x, lessThan(100));
expect(x, closeTo(3.14, 0.01)); // tolerância para doubles

// Strings
expect(s, contains('texto'));
expect(s, startsWith('prefix'));
expect(s, endsWith('suffix'));

// Coleções
expect(lista, isEmpty);
expect(lista, isNotEmpty);
expect(lista, hasLength(3));
expect(lista, contains('item'));

// Tipos
expect(obj, isA<Account>());
expect(obj, isA<List<Account>>());

// Exceções
expect(() => fn(), throwsException);
expect(() => fn(), throwsA(isA<MinhaException>()));
expect(() => fn(), throwsArgumentError);

Mocks com Mockito

pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.4
  build_runner: ^2.4.12

Definir mocks

// test/mocks/mocks.dart
import 'package:mockito/annotations.dart';
import 'package:banco_douro_app/services/account_service.dart';

@GenerateMocks([AccountService])
void main() {}
dart run build_runner build

Usar mocks

import 'package:mockito/mockito.dart';
import '../mocks/mocks.mocks.dart';

void main() {
  late MockAccountService mock;

  setUp(() {
    mock = MockAccountService();
  });

  test('...', () async {
    // Configurar retorno
    when(mock.getAll()).thenAnswer((_) async => []);
    when(mock.addAccount(any)).thenAnswer((_) async => true);
    when(mock.getAll()).thenThrow(Exception('erro'));

    // Verificar chamadas
    verify(mock.getAll()).called(1);
    verifyNever(mock.deleteAccount(any));
  });
}

Injeção de Dependência (para testabilidade)

// ❌ Difícil de testar
class AccountProvider extends ChangeNotifier {
  final AccountService _service = AccountService();
}

// ✅ Testável
class AccountProvider extends ChangeNotifier {
  final AccountService _service;

  AccountProvider({AccountService? service})
      : _service = service ?? AccountService();
}

// Produção
AccountProvider()

// Teste
AccountProvider(service: MockAccountService())

Teste de Widget

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

void main() {
  testWidgets('descrição do teste', (WidgetTester tester) async {
    // Renderizar o widget
    await tester.pumpWidget(MaterialApp(home: MeuWidget()));

    // Encontrar elementos
    expect(find.text('Olá'), findsOneWidget);
    expect(find.byType(TextField), findsNWidgets(2));
    expect(find.byKey(Key('meuBotao')), findsOneWidget);

    // Interagir
    await tester.tap(find.text('Entrar'));
    await tester.enterText(find.byType(TextField).first, 'valor');
    await tester.drag(find.byType(ListView), Offset(0, -300));

    // Atualizar (processar eventos pendentes)
    await tester.pump();            // Um frame
    await tester.pumpAndSettle();   // Até animações terminarem
  });
}

Teste de Widget com Provider

testWidgets('HomeScreen lista contas', (tester) async {
  final mockProvider = MockAccountProvider();
  when(mockProvider.accounts).thenReturn([fakeAccount]);
  when(mockProvider.isLoading).thenReturn(false);

  await tester.pumpWidget(
    ChangeNotifierProvider<AccountProvider>.value(
      value: mockProvider,
      child: MaterialApp(home: HomeScreen()),
    ),
  );

  expect(find.text('João Silva'), findsOneWidget);
});

Finders (find.xxx)

find.text('Olá')                    // Por texto
find.byType(TextField)              // Por tipo de widget
find.byKey(Key('minha-chave'))      // Por Key
find.byIcon(Icons.add)              // Por ícone
find.byWidget(meuWidget)            // Por instância
find.ancestor(of: x, matching: y)  // Ancestral
find.descendant(of: x, matching: y) // Descendente

async em testes

// Função assíncrona
test('carrega dados', () async {
  await provider.loadAll();
  expect(provider.accounts, isNotEmpty);
});

// Widget com Future
testWidgets('exibe loading e depois dados', (tester) async {
  await tester.pumpWidget(MeuWidget());

  expect(find.byType(CircularProgressIndicator), findsOneWidget);

  await tester.pumpAndSettle(); // espera o Future completar

  expect(find.text('Dados carregados'), findsOneWidget);
});

Tipos de Teste — Resumo

Tipo Velocidade Custo Quando usar
Unidade Muito rápido Baixo Lógica de negócio, helpers, models
Widget Rápido Médio UI, interações, navegação local
Integração Lento Alto Fluxos completos, E2E
Mock Muito rápido Baixo Isolar dependências externas

Pirâmide de Testes

    /\
   /UI\       ← Poucos (lentos, caros)
  /----\
 /Widget\     ← Médios
/--------\
/ Unidade \   ← Muitos (rápidos, baratos)
/__________\

Cobertura de Testes (Coverage)

# Gerar coverage
flutter test --coverage

# Ver resumo (requer lcov)
lcov --summary coverage/lcov.info

# Gerar HTML (macOS: brew install lcov)
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

Meta realista: 70-80% de coverage nas regras de negócio.

Flutter Cheatsheet - APIs e Autenticação

Consumo de APIs REST com Autenticação

Objetivo: Referência rápida para acompanhamento da aula e consulta posterior.


Índice

  1. PUT - Atualizar Recursos
  2. DELETE - Remover Recursos
  3. Dialogs de Confirmação
  4. Autenticação com Login e Senha
  5. Token de Autenticação (JWT)
  6. Tratamento de Erros de APIs
  7. Recursos e Documentação

PUT - Atualizar Recursos

Requisição PUT Básica

import 'package:http/http.dart' as http;
import 'dart:convert';

Future<bool> updateAccount(Account account) async {
  final response = await http.put(
    Uri.parse('http://localhost:3000/accounts/${account.id}'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode(account.toMap()),
  );

  return response.statusCode == 200;
}

Documentação: http package

PUT com Autenticação

Future<bool> updateAccount(Account account, String token) async {
  final response = await http.put(
    Uri.parse('$baseUrl/accounts/${account.id}'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $token',
    },
    body: json.encode(account.toMap()),
  );

  if (response.statusCode != 200) {
    throw Exception('Falha ao atualizar: ${response.body}');
  }

  return true;
}

Diferença PUT vs PATCH

Método Uso Corpo da Requisição
PUT Substituição completa Objeto inteiro
PATCH Atualização parcial Apenas campos alterados
// PUT - envia objeto completo
body: json.encode(account.toMap())

// PATCH - envia apenas o que mudou
body: json.encode({'name': 'Novo Nome'})

DELETE - Remover Recursos

Requisição DELETE Básica

Future<bool> deleteAccount(String id) async {
  final response = await http.delete(
    Uri.parse('http://localhost:3000/accounts/$id'),
    headers: {'Content-Type': 'application/json'},
  );

  return response.statusCode == 200;
}

DELETE com Autenticação

Future<bool> deleteAccount(String id, String token) async {
  final response = await http.delete(
    Uri.parse('$baseUrl/accounts/$id'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $token',
    },
  );

  if (response.statusCode != 200) {
    throw Exception('Falha ao excluir');
  }

  return true;
}

Códigos de Status DELETE

Código Significado
200 Sucesso com corpo na resposta
204 Sucesso sem corpo (No Content)
404 Recurso não encontrado
401 Não autorizado

Dialogs de Confirmação

AlertDialog Básico

Future<bool?> showConfirmationDialog(BuildContext context) {
  return showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('Confirmação'),
      content: Text('Deseja realizar esta ação?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: Text('Cancelar'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: Text('Confirmar'),
        ),
      ],
    ),
  );
}

Documentação: AlertDialog

Dialog Reutilizável

Future<bool?> showConfirmationDialog(
  BuildContext context, {
  String title = 'Atenção!',
  String content = 'Deseja realizar esta operação?',
  String confirmText = 'Confirmar',
  String cancelText = 'Cancelar',
}) {
  return showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: Text(title),
      content: Text(content),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: Text(cancelText),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: Text(
            confirmText,
            style: TextStyle(
              color: Colors.red,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ],
    ),
  );
}

Uso do Dialog

void _onDeletePressed() async {
  final confirmed = await showConfirmationDialog(
    context,
    title: 'Excluir Conta',
    content: 'Deseja realmente excluir esta conta?',
    confirmText: 'Excluir',
  );

  if (confirmed == true) {
    await deleteAccount(account.id);
    // Atualizar UI
  }
}

Padrão: Sempre Confirmar Ações Destrutivas

// BOM - Pede confirmação antes de excluir
onPressed: () async {
  final confirmed = await showConfirmationDialog(context);
  if (confirmed == true) {
    await service.delete(id);
  }
}

// RUIM - Exclui direto sem confirmar
onPressed: () async {
  await service.delete(id);
}

Autenticação com Login e Senha

Estrutura do AuthService

class AuthService {
  static const String _tokenKey = 'accessToken';

  Future<String> login(String email, String password) async {
    final response = await http.post(
      Uri.parse('$baseUrl/login'),
      body: {'email': email, 'password': password},
    );

    if (response.statusCode != 200) {
      throw Exception('Falha no login');
    }

    final data = json.decode(response.body);
    return data['accessToken'];
  }

  Future<String> register(String email, String password) async {
    final response = await http.post(
      Uri.parse('$baseUrl/register'),
      body: {'email': email, 'password': password},
    );

    if (response.statusCode != 201) {
      throw Exception('Falha no registro');
    }

    final data = json.decode(response.body);
    return data['accessToken'];
  }
}

Endpoints de Autenticação (json-server-auth)

Método Endpoint Body Resposta
POST /register {email, password} {accessToken, user}
POST /login {email, password} {accessToken, user}

Tela de Login

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _authService = AuthService();
  bool _isLoading = false;

  Future<void> _onLoginPressed() async {
    setState(() => _isLoading = true);

    try {
      await _authService.login(
        _emailController.text,
        _passwordController.text,
      );
      Navigator.pushReplacementNamed(context, 'home');
    } catch (e) {
      // Mostrar erro
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TextFormField(controller: _emailController),
          TextFormField(
            controller: _passwordController,
            obscureText: true,
          ),
          ElevatedButton(
            onPressed: _isLoading ? null : _onLoginPressed,
            child: _isLoading
                ? CircularProgressIndicator()
                : Text('Entrar'),
          ),
        ],
      ),
    );
  }
}

Token de Autenticação (JWT)

Instalação do SharedPreferences

# pubspec.yaml
dependencies:
  shared_preferences: ^2.2.2
flutter pub add shared_preferences

Pacote: pub.dev/packages/shared_preferences

Salvar Token

import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveToken(String token) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('accessToken', token);
}

Recuperar Token

Future<String?> getToken() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('accessToken');
}

Verificar se Está Logado

Future<bool> isLoggedIn() async {
  final token = await getToken();
  return token != null && token.isNotEmpty;
}

Logout (Remover Token)

Future<void> logout() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove('accessToken');
}

Usar Token nas Requisições

Future<Map<String, String>> _getHeaders() async {
  final token = await getToken();
  return {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $token',
  };
}

Future<List<Account>> getAccounts() async {
  final headers = await _getHeaders();
  final response = await http.get(
    Uri.parse('$baseUrl/accounts'),
    headers: headers,
  );
  // ...
}

Verificar Token no Startup

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final authService = AuthService();
  final isLoggedIn = await authService.isLoggedIn();

  runApp(MyApp(isLoggedIn: isLoggedIn));
}

class MyApp extends StatelessWidget {
  final bool isLoggedIn;

  const MyApp({required this.isLoggedIn});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: isLoggedIn ? 'home' : 'login',
      routes: {
        'login': (_) => LoginScreen(),
        'home': (_) => HomeScreen(),
      },
    );
  }
}

Tratamento de Erros de APIs

Exceções Customizadas

class UserNotFoundException implements Exception {
  final String message;
  UserNotFoundException([this.message = 'Usuário não encontrado']);
}

class TokenExpiredException implements Exception {
  final String message;
  TokenExpiredException([this.message = 'Token expirado']);
}

class UnauthorizedException implements Exception {
  final String message;
  UnauthorizedException([this.message = 'Não autorizado']);
}

class ServerException implements Exception {
  final String message;
  ServerException([this.message = 'Erro no servidor']);
}

Verificar Erros da API

void _verifyException(String responseBody) {
  final errorMessage = responseBody.toLowerCase();

  if (errorMessage.contains('jwt expired')) {
    throw TokenExpiredException();
  }
  if (errorMessage.contains('cannot find user')) {
    throw UserNotFoundException();
  }
  if (errorMessage.contains('unauthorized')) {
    throw UnauthorizedException();
  }

  throw ServerException(responseBody);
}

Try-Catch com Múltiplas Exceções

Future<void> _onLoginPressed() async {
  try {
    await authService.login(email, password);
    Navigator.pushReplacementNamed(context, 'home');
  } on UserNotFoundException {
    // Usuário não existe - oferecer registro
    _offerRegistration();
  } on UnauthorizedException {
    // Senha incorreta
    _showError('Senha incorreta');
  } on SocketException {
    // Sem internet
    _showError('Verifique sua conexão');
  } catch (e) {
    // Erro genérico
    _showError('Erro inesperado');
  }
}

Tratamento com catchError (Futures)

authService.login(email, password)
  .then((token) {
    Navigator.pushReplacementNamed(context, 'home');
  })
  .catchError((e) {
    _offerRegistration();
  }, test: (e) => e is UserNotFoundException)
  .catchError((e) {
    _showError(e.message);
  }, test: (e) => e is ServerException);

Dialog de Erro

void showExceptionDialog(
  BuildContext context, {
  required String content,
  String title = 'Ocorreu um problema',
}) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(Icons.warning, color: Colors.orange),
          SizedBox(width: 8),
          Text(title),
        ],
      ),
      content: Text(content),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('OK'),
        ),
      ],
    ),
  );
}

Códigos HTTP Comuns

Código Significado Ação Sugerida
200 Sucesso Processar resposta
201 Criado Processar resposta
400 Bad Request Validar dados enviados
401 Não Autorizado Redirecionar para login
403 Proibido Mostrar mensagem
404 Não Encontrado Mostrar mensagem
500 Erro Servidor Tentar novamente

Tratamento de Token Expirado

try {
  final accounts = await accountService.getAll();
} on TokenExpiredException {
  await authService.logout();
  Navigator.pushNamedAndRemoveUntil(
    context,
    'login',
    (route) => false,
  );
}

json-server-auth

Instalação

npm install -g json-server-auth

Estrutura do db.json

{
  "users": [],
  "accounts": [],
  "transactions": [],
  "accountTypes": []
}

Arquivo routes.json (Permissões)

{
  "users": 600,
  "accounts": 660,
  "transactions": 660,
  "accountTypes": 444
}

Códigos de Permissão

Código Leitura Escrita Descrição
444 Público - Somente leitura pública
600 Privado Privado Apenas dono acessa
640 Público Privado Lê público, escreve autenticado
660 Autenticado Autenticado Requer token

Executar Servidor

json-server-auth db.json --routes routes.json --port 3000

Recursos e Documentação

Documentação Oficial

Recurso Link
http package pub.dev/packages/http
shared_preferences pub.dev/packages/shared_preferences
json-server-auth npmjs.com/package/json-server-auth

API Reference

Classe Link
AlertDialog api.flutter.dev/flutter/material/AlertDialog
showDialog api.flutter.dev/flutter/material/showDialog
Navigator api.flutter.dev/flutter/widgets/Navigator

HTTP Status Codes

Recurso Link
MDN Web Docs developer.mozilla.org/en-US/docs/Web/HTTP/Status
HTTP Cats http.cat

Quick Reference

Ação Código
Requisição PUT http.put(uri, headers: {...}, body: json)
Requisição DELETE http.delete(uri, headers: {...})
Mostrar dialog showDialog(context: ctx, builder: ...)
Fechar dialog com valor Navigator.pop(context, value)
Salvar token prefs.setString('key', token)
Recuperar token prefs.getString('key')
Remover token prefs.remove('key')
Header de auth 'Authorization': 'Bearer $token'
Lançar exceção throw MinhaException()
Capturar específica on MinhaException catch (e) {...}

Fluxo de Autenticação

App Inicia
    |
    v
Verifica token no SharedPreferences
    |
    +---> Token existe? ---> HomeScreen
    |
    +---> Não existe? ---> LoginScreen
                              |
                              v
                         Usuário digita email/senha
                              |
                              v
                         POST /login
                              |
              +---------------+---------------+
              |               |               |
              v               v               v
           Sucesso       Usuário não      Erro
              |          encontrado         |
              v               |             v
         Salvar token    Oferecer      Mostrar erro
              |          registro
              v               |
         HomeScreen      POST /register
                              |
                              v
                         Salvar token
                              |
                              v
                         HomeScreen

Fluxo de Requisição Autenticada

Ação do Usuário (ex: editar conta)
         |
         v
Recuperar token do SharedPreferences
         |
         v
Montar headers com Authorization
         |
         v
Fazer requisição (PUT/DELETE)
         |
         +---> 200: Sucesso --> Atualizar UI
         |
         +---> 401: Não autorizado --> Verificar token
         |                                   |
         |                           Token expirado?
         |                                   |
         |                           Logout e ir para Login
         |
         +---> 4xx/5xx: Erro --> Mostrar mensagem

Checklist da Aula

  • Fazer requisição PUT para atualizar recurso
  • Fazer requisição DELETE para remover recurso
  • Criar dialog de confirmação reutilizável
  • Usar dialog antes de ações destrutivas
  • Implementar login com email e senha
  • Implementar registro de novo usuário
  • Salvar token com SharedPreferences
  • Recuperar e verificar token no startup
  • Enviar token no header Authorization
  • Implementar logout (remover token)
  • Criar exceções customizadas
  • Tratar erros específicos da API
  • Mostrar mensagens de erro amigáveis
  • Redirecionar para login quando token expira
  • Configurar json-server-auth

Flutter Cheatsheet - Estilizacao e Layouts

Estilizando e Reproduzindo Layouts do Figma

Objetivo: Referencia rapida para acompanhamento da aula e consulta posterior.


Indice

  1. BoxDecoration e Gradientes
  2. TextStyle e Text.Rich
  3. InkWell e GestureDetector
  4. Widgets Nativos vs Material Design
  5. ThemeData e Temas
  6. Fontes Personalizadas
  7. Organizacao de Codigo
  8. Recursos e Documentacao

BoxDecoration e Gradientes

BoxDecoration Basico

Container(
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 10,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Text('Conteudo'),
)

Documentacao: BoxDecoration

Propriedades do BoxDecoration

Propriedade Tipo Descricao
color Color Cor de fundo solida
gradient Gradient Gradiente (nao usar com color)
borderRadius BorderRadius Cantos arredondados
boxShadow List Sombra(s)
border Border Borda
image DecorationImage Imagem de fundo
shape BoxShape Formato (rectangle, circle)

LinearGradient

Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [
        Color(0xFF0A1F3D),
        Color(0xFF1B4F72),
      ],
    ),
  ),
)

Direcoes do Gradiente

Direcao begin end
Cima para baixo topCenter bottomCenter
Esquerda para direita centerLeft centerRight
Diagonal topLeft bottomRight
Diagonal inversa topRight bottomLeft

Gradiente com Stops

LinearGradient(
  colors: [cor1, cor2, cor3],
  stops: [0.0, 0.5, 1.0],
)

BoxShadow

BoxShadow(
  color: Colors.black.withOpacity(0.1),
  blurRadius: 10,
  spreadRadius: 2,
  offset: Offset(0, 4),
)
Parametro Descricao
color Cor com opacidade
blurRadius Quanto desfoca
spreadRadius Quanto se expande
offset Deslocamento (x, y)

BorderRadius

// Todos iguais
BorderRadius.circular(16)

// Individuais
BorderRadius.only(
  topLeft: Radius.circular(16),
  topRight: Radius.circular(16),
)

// Vertical
BorderRadius.vertical(
  top: Radius.circular(16),
)

Border

// Todas as bordas
Border.all(color: Colors.grey, width: 1)

// Bordas especificas
Border(
  bottom: BorderSide(color: Colors.grey, width: 1),
)

TextStyle e Text.Rich

TextStyle

Text(
  'Texto estilizado',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Color(0xFF2C3E50),
    letterSpacing: 0.5,
    height: 1.5,
    fontFamily: 'Montserrat',
  ),
)

Documentacao: TextStyle

FontWeight

Constante Valor Nome
FontWeight.w300 300 Light
FontWeight.w400 400 Regular
FontWeight.w500 500 Medium
FontWeight.w600 600 SemiBold
FontWeight.w700 700 Bold

Text.Rich com TextSpan

Text.rich(
  TextSpan(
    text: 'Saldo: ',
    style: TextStyle(color: Colors.grey),
    children: [
      TextSpan(
        text: 'R\$ 5.230,45',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.black,
        ),
      ),
    ],
  ),
)

Documentacao: Text.rich

Text.Rich com Multiplos Trechos

Text.rich(
  TextSpan(
    children: [
      TextSpan(text: 'Texto ', style: TextStyle(color: Colors.grey)),
      TextSpan(text: 'destaque', style: TextStyle(fontWeight: FontWeight.bold)),
      TextSpan(text: ' normal.', style: TextStyle(color: Colors.grey)),
    ],
  ),
)

TextOverflow

Text(
  'Texto longo...',
  overflow: TextOverflow.ellipsis,
  maxLines: 1,
)
Valor Efeito
clip Corta
ellipsis "..."
fade Desfoca
visible Extravasa

InkWell e GestureDetector

InkWell (com efeito ripple)

InkWell(
  onTap: () => print('tap'),
  onLongPress: () => print('segurar'),
  borderRadius: BorderRadius.circular(12),
  splashColor: Colors.blue.withOpacity(0.2),
  child: Container(
    padding: EdgeInsets.all(16),
    child: Text('Clique'),
  ),
)

Documentacao: InkWell

GestureDetector (sem efeito visual)

GestureDetector(
  onTap: () => print('tap'),
  onDoubleTap: () => print('duplo'),
  onLongPress: () => print('segurar'),
  child: Container(
    child: Text('Toque'),
  ),
)

Comparacao

InkWell GestureDetector
Efeito Ripple Nenhum
Material Necessario Nao necessario
Gestos Basicos Basicos + complexos
Uso Botoes Gestos complexos

Widgets Nativos vs Material Design

Widgets Nativos

Container()    // Caixa com decoracao
Column()       // Vertical
Row()          // Horizontal
Stack()        // Sobreposicao
Padding()      // Espacamento
SizedBox()     // Espaco fixo
Expanded()     // Preenche espaco
Text()         // Texto
Image()        // Imagem
Icon()         // Icone

Widgets Material Design

Scaffold()              // Estrutura da tela
AppBar()                // Barra superior
Card()                  // Cartao com elevacao
ElevatedButton()        // Botao elevado
TextButton()            // Botao de texto
IconButton()            // Botao de icone
ListTile()              // Item de lista
Drawer()                // Menu lateral
BottomNavigationBar()   // Barra inferior
FloatingActionButton()  // Botao flutuante

Quando Usar

Necessidade Escolha
Layout customizado Nativos (Container, Row, Column)
Botao padrao Material (ElevatedButton)
Botao customizado InkWell + Container
Cartao customizado Container + BoxDecoration
Cartao simples Card

ThemeData e Temas

ThemeData Basico

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Color(0xFF0A1F3D),
    ),
    fontFamily: 'Montserrat',
    scaffoldBackgroundColor: Color(0xFFF5F5F5),
  ),
)

Documentacao: ThemeData

Acessando o Tema

final theme = Theme.of(context);
final colors = theme.colorScheme;
final textTheme = theme.textTheme;

Text('Titulo', style: theme.textTheme.headlineLarge);

AppBarTheme

appBarTheme: AppBarTheme(
  backgroundColor: Color(0xFF0A1F3D),
  foregroundColor: Colors.white,
  elevation: 0,
  centerTitle: false,
)

CardTheme

cardTheme: CardTheme(
  elevation: 2,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
)

ElevatedButtonTheme

elevatedButtonTheme: ElevatedButtonThemeData(
  style: ElevatedButton.styleFrom(
    backgroundColor: Color(0xFF0A1F3D),
    foregroundColor: Colors.white,
    padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

TextTheme

textTheme: TextTheme(
  headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
  titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
  bodyMedium: TextStyle(fontSize: 14, color: Colors.grey),
)

Dark Theme

MaterialApp(
  theme: AppTheme.lightTheme,
  darkTheme: AppTheme.darkTheme,
  themeMode: ThemeMode.system,
)

Fontes Personalizadas

Passo 1: Estrutura de Arquivos

assets/
  fonts/
    Montserrat-Regular.ttf
    Montserrat-Medium.ttf
    Montserrat-SemiBold.ttf
    Montserrat-Bold.ttf

Passo 2: pubspec.yaml

flutter:
  fonts:
    - family: Montserrat
      fonts:
        - asset: assets/fonts/Montserrat-Regular.ttf
          weight: 400
        - asset: assets/fonts/Montserrat-Medium.ttf
          weight: 500
        - asset: assets/fonts/Montserrat-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Montserrat-Bold.ttf
          weight: 700

Passo 3: Usar no Codigo

// Em um widget
Text('Texto', style: TextStyle(fontFamily: 'Montserrat'))

// No tema global
ThemeData(fontFamily: 'Montserrat')

Fontes Populares para Apps

Fonte Estilo Uso
Montserrat Geometrica Titulos e UI
Inter Neutra Corpo de texto
Poppins Geometrica UI moderna
Roboto Padrao Android Consistencia
SF Pro Padrao iOS Consistencia

Organizacao de Codigo

Estrutura de Pastas

lib/
  main.dart
  theme/
    app_colors.dart
    app_theme.dart
    app_text_styles.dart
  screens/
    home_screen.dart
  widgets/
    header_widget.dart
    balance_card.dart
    action_button.dart
    transaction_tile.dart

Constantes de Cores

class AppColors {
  static const Color primary = Color(0xFF0A1F3D);
  static const Color primaryLight = Color(0xFF1B4F72);
  static const Color background = Color(0xFFF5F5F5);
  static const Color textPrimary = Color(0xFF2C3E50);
  static const Color textSecondary = Color(0xFF7F8C8D);
  static const Color positive = Color(0xFF27AE60);
  static const Color negative = Color(0xFFE74C3C);
}

Boas Praticas

Pratica Exemplo
Centralizar cores AppColors.primary
Centralizar estilos AppTheme.lightTheme
Componentes reutilizaveis ActionButton(icon, label, onTap)
Nomes descritivos HeaderWidget, BalanceCard
Separar por responsabilidade theme/, screens/, widgets/

Quick Reference

Acao Codigo
Cor hexadecimal Color(0xFF0A1F3D)
Gradiente linear LinearGradient(colors: [c1, c2])
Bordas arredondadas BorderRadius.circular(16)
Sombra BoxShadow(blurRadius: 10, offset: Offset(0, 4))
Texto em negrito FontWeight.bold
Texto com estilos mistos Text.rich(TextSpan(children: [...]))
Area clicavel com ripple InkWell(onTap: ..., child: ...)
Acessar tema Theme.of(context)
Estilo de texto do tema theme.textTheme.headlineLarge
Fonte no tema ThemeData(fontFamily: 'Montserrat')
Cor com opacidade color.withOpacity(0.1)
Avatar circular CircleAvatar(radius: 24, child: Icon(...))

Mapeamento Figma para Flutter

Elemento do Figma Widget Flutter
Retangulo com cor Container(color: ...)
Retangulo com gradiente Container(decoration: BoxDecoration(gradient: ...))
Retangulo arredondado Container(decoration: BoxDecoration(borderRadius: ...))
Sombra BoxDecoration(boxShadow: [...])
Texto Text(style: TextStyle(...))
Texto misto Text.rich(TextSpan(children: [...]))
Imagem circular CircleAvatar ou ClipRRect
Grupo horizontal Row(children: [...])
Grupo vertical Column(children: [...])
Sobreposicao Stack(children: [...])
Espaco entre SizedBox(height/width: ...)
Botao customizado InkWell + Container
Barra de navegacao BottomNavigationBar

Checklist da Aula

  • Analisar layout do Figma e identificar componentes
  • Mapear componentes para Widgets Flutter
  • Extrair cores, fontes e espacamentos
  • Criar constantes de cores centralizadas
  • Usar BoxDecoration para estilizar containers
  • Criar gradientes com LinearGradient
  • Adicionar sombras com BoxShadow
  • Usar BorderRadius para cantos arredondados
  • Estilizar textos com TextStyle
  • Misturar estilos com Text.Rich e TextSpan
  • Criar areas clicaveis com InkWell
  • Diferenciar InkWell de GestureDetector
  • Configurar ThemeData global
  • Adicionar fontes personalizadas no pubspec.yaml
  • Organizar codigo em pastas (theme, screens, widgets)

Proxima Aula: Navegacao e Animacoes

Flutter Cheatsheet - Constraints e Responsividade (Banco Douro)

Delimitando Widgets e Construindo Layouts Adaptativos

Objetivo: Referencia rapida para acompanhar a aula e consultar no desenvolvimento do banco_douro_app.


Indice

  1. BoxConstraints
  2. Widgets de Delimitacao
  3. MediaQuery e Breakpoints
  4. Orientacao e Layout Adaptativo
  5. CustomScrollView e Slivers
  6. Tipos de Lista no App Bancario
  7. Recursos e Documentacao

BoxConstraints

const BoxConstraints(
  minWidth: 260,
  maxWidth: 720,
  minHeight: 140,
)

Tight vs Loose

BoxConstraints.tightFor(width: 320, height: 140)
BoxConstraints.loose(const Size(720, 220))

Widgets de Delimitacao

SizedBox

SizedBox(height: 52, child: ElevatedButton(...))

ConstrainedBox

ConstrainedBox(
  constraints: const BoxConstraints(maxWidth: 680),
  child: AccountWidget(...),
)

LimitedBox

LimitedBox(maxWidth: 260, child: QuickActionCard(...))

Expanded e Flexible

Row(
  children: [
    Expanded(child: _leftPanel()),
    Flexible(child: _rightPanel()),
  ],
)

MediaQuery e Breakpoints

final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
int columnsFor(double width) {
  if (width >= 1200) return 4;
  if (width >= 900) return 3;
  if (width >= 600) return 2;
  return 1;
}

Orientacao e Layout Adaptativo

final isLandscape =
    MediaQuery.of(context).orientation == Orientation.landscape;
OrientationBuilder(
  builder: (context, orientation) {
    return orientation == Orientation.landscape
        ? _buildLandscape()
        : _buildPortrait();
  },
)

CustomScrollView e Slivers

CustomScrollView(
  slivers: [
    SliverAppBar(...),
    SliverToBoxAdapter(child: _summary()),
    SliverGrid(...),
    SliverList(...),
  ],
)

Slivers mais usados

  • SliverAppBar: cabecalho expansivel
  • SliverToBoxAdapter: adapter para widget comum
  • SliverGrid: grade responsiva
  • SliverList: lista longa eficiente

Tipos de Lista no App Bancario

Horizontal (acoes rapidas)

ListView.separated(
  scrollDirection: Axis.horizontal,
  itemBuilder: ...,
)

Grid (atalhos financeiros)

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: columnsFor(width),
  ),
)

Vertical (contas/transacoes)

ListView.builder(itemCount: accounts.length, itemBuilder: ...)

Recursos e Documentacao

Flutter Cheatsheet - Provider e Gerenciamento de Estado (Banco Douro)

Estado Centralizado com ChangeNotifier e Provider

Objetivo: Referencia rapida para acompanhar a aula e consultar no desenvolvimento do banco_douro_app.


Indice

  1. Dependencia e Setup
  2. ChangeNotifier
  3. Tipos de Provider
  4. Consumindo o Provider
  5. Consumer vs watch vs read
  6. Patterns do Banco Douro
  7. Comparativo de Gerenciadores
  8. Armadilhas Comuns
  9. Recursos e Documentacao

Dependencia e Setup

# pubspec.yaml
dependencies:
  provider: ^6.1.2
flutter pub get
// Em qualquer arquivo que usa Provider
import 'package:provider/provider.dart';

ChangeNotifier

Template Base

class MeuProvider extends ChangeNotifier {
  // Estado privado
  List<Item> _items = [];
  bool _isLoading = false;
  String? _error;

  // Getters publicos (somente leitura)
  List<Item> get items => List.unmodifiable(_items);
  bool get isLoading => _isLoading;
  String? get error => _error;

  // Acao
  Future<void> load() async {
    _isLoading = true;
    _error = null;
    notifyListeners(); // UI mostra loading

    try {
      _items = await service.getAll();
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners(); // UI atualiza
    }
  }
}

Regras de Ouro

Regra Correto Errado
Campos _items (privado) items (publico mutavel)
Mutacao _items = newList; notifyListeners() items.add(x) sem notificar
Getter get items => List.unmodifiable(_items) get items => _items
Async try/catch/finally com loading Sem tratamento de erro

Tipos de Provider

ChangeNotifierProvider

Para providers baseados em ChangeNotifier:

ChangeNotifierProvider(
  create: (_) => AccountProvider(),
  child: MyApp(),
)

ChangeNotifierProvider.value

Quando a instancia ja foi criada:

final provider = AccountProvider();
await provider.loadAll(); // inicializa antes

ChangeNotifierProvider.value(
  value: provider,
  child: MyApp(),
)

MultiProvider

Para registrar varios providers:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => AuthProvider()),
    ChangeNotifierProvider(create: (_) => AccountProvider()),
  ],
  child: MaterialApp(...),
)

ProxyProvider

Quando um provider depende de outro:

ProxyProvider<AuthProvider, ApiService>(
  update: (_, auth, prev) => ApiService(token: auth.token),
)

Consumindo o Provider

Consumer

Consumer<AccountProvider>(
  builder: (context, provider, child) {
    return ListView.builder(
      itemCount: provider.accounts.length,
      itemBuilder: (_, i) => AccountWidget(account: provider.accounts[i]),
    );
  },
)

Consumer com child otimizado

Consumer<AccountProvider>(
  child: const Text('Contas', style: TextStyle(fontSize: 20)),
  builder: (context, provider, header) {
    return Column(
      children: [
        header!, // nao reconstroi
        ...provider.accounts.map((a) => AccountWidget(account: a)),
      ],
    );
  },
)

context.watch (inline no build)

@override
Widget build(BuildContext context) {
  final provider = context.watch<AccountProvider>();
  return Text('${provider.accounts.length} contas');
}

context.read (em callbacks)

ElevatedButton(
  onPressed: () => context.read<AccountProvider>().loadAll(),
  child: const Text('Recarregar'),
)

context.select (propriedade especifica)

final count = context.select<AccountProvider, int>(
  (p) => p.accounts.length,
);
Text('Total: $count')

Consumer vs watch vs read

Consumer context.watch context.read
Reconstroi ao mudar Sim (parte do builder) Sim (widget inteiro) Nao
Onde usar Na arvore de widgets Dentro do build Em callbacks
Otimizacao child Sim Nao N/A
Seguro fora do build Nao Nao Sim

Regra pratica

// Leitura reativa (reconstrucao) → watch ou Consumer
final accounts = context.watch<AccountProvider>().accounts;

// Acao (sem reconstrucao) → read
context.read<AccountProvider>().deleteAccount(id);

// Subconjunto reativo → select
final isLoading = context.select<AccountProvider, bool>(
  (p) => p.isLoading,
);

Patterns do Banco Douro

main.dart com MultiProvider

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final authProvider = AuthProvider();
  await authProvider.init();

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider.value(value: authProvider),
        ChangeNotifierProvider(create: (_) => AccountProvider()),
      ],
      child: const BancoDouroApp(),
    ),
  );
}

Tela com 3 estados (loading / erro / dados)

Consumer<AccountProvider>(
  builder: (context, p, _) {
    if (p.isLoading) return const CircularProgressIndicator();
    if (p.error != null) return Text(p.error!);
    if (p.accounts.isEmpty) return const Text('Sem contas');
    return ListView.builder(
      itemCount: p.accounts.length,
      itemBuilder: (_, i) => AccountWidget(account: p.accounts[i]),
    );
  },
)

Acao no modal sem callback

// Modal NAO precisa de onSaved callback
ElevatedButton(
  onPressed: () async {
    await context.read<AccountProvider>().addAccount(newAccount);
    if (context.mounted) Navigator.pop(context);
    // Consumer no pai ja atualiza automaticamente
  },
)

Logout com AuthProvider

IconButton(
  icon: const Icon(Icons.logout),
  onPressed: () async {
    await context.read<AuthProvider>().logout();
    if (context.mounted) {
      Navigator.pushReplacementNamed(context, 'login');
    }
  },
)

Carregar dados ao entrar na tela

// Opção A: StatefulWidget com initState (HomeScreen)
class _HomeScreenState extends State<HomeScreen> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<AccountProvider>().initialize();
    });
  }
}

// Opção B: disparar no LoginScreen antes de navegar (DashboardScreen)
if (success) {
  context.read<AccountProvider>().initialize(); // dispara sem bloquear
  Navigator.pushReplacementNamed(context, 'dashboard');
}

Comparativo de Gerenciadores

Gerenciador Pkg Curva Quando usar
setState - Baixa Estado local, 1 widget
Provider provider Baixa Apps medios, equipes pequenas
Riverpod flutter_riverpod Media Provider moderno, testavel
Cubit/BLoC flutter_bloc Alta Enterprise, auditoria de estados
Redux flutter_redux Alta Background JS/Redux, auditoria total
MobX flutter_mobx Media Background JS/MobX, valores derivados
GetX get Baixa Prototipagem rapida, solo
Qual usar?

Estado de um widget apenas?
  └─► setState

Compartilhado entre 2+ telas, time pequeno?
  └─► Provider   ← esta aula

Provider dominado? Quer mais poder sem reescrever?
  └─► Riverpod

Equipe grande, regras complexas, rastreabilidade?
  └─► BLoC / Cubit

Background JS/Redux no time?
  └─► Redux

Background JS/MobX ou muitos valores derivados?
  └─► MobX

Gerenciadores Avancados: Referencia Rapida

BLoC / Cubit

# pubspec.yaml
flutter_bloc: ^9.x

Cubit — metodos publicos emitem estados:

// Estados (classes imutaveis)
sealed class AccountState {}
class AccountLoading extends AccountState {}
class AccountLoaded extends AccountState {
  final List<Account> accounts;
  AccountLoaded(this.accounts);
}
class AccountError extends AccountState {
  final String message;
  AccountError(this.message);
}

// Cubit
class AccountCubit extends Cubit<AccountState> {
  AccountCubit() : super(AccountLoading());

  Future<void> load() async {
    emit(AccountLoading());
    try {
      emit(AccountLoaded(await AccountService().getAccounts()));
    } catch (e) {
      emit(AccountError(e.toString()));
    }
  }
}

// UI
BlocProvider(
  create: (_) => AccountCubit()..load(),
  child: BlocBuilder<AccountCubit, AccountState>(
    builder: (context, state) => switch (state) {
      AccountLoading() => const CircularProgressIndicator(),
      AccountLoaded(:final accounts) => AccountList(accounts: accounts),
      AccountError(:final message) => Text(message),
    },
  ),
)

BLoC — UI despacha eventos, Bloc processa:

// Eventos
sealed class AccountEvent {}
class LoadAccounts extends AccountEvent {}
class AddAccount extends AccountEvent {
  final Account account;
  AddAccount(this.account);
}

// Bloc
class AccountBloc extends Bloc<AccountEvent, AccountState> {
  AccountBloc() : super(AccountLoading()) {
    on<LoadAccounts>(_onLoad);
    on<AddAccount>(_onAdd);
  }

  Future<void> _onLoad(LoadAccounts e, Emitter<AccountState> emit) async {
    emit(AccountLoading());
    try {
      emit(AccountLoaded(await AccountService().getAccounts()));
    } catch (e) {
      emit(AccountError(e.toString()));
    }
  }
}

// Disparar evento na UI
context.read<AccountBloc>().add(LoadAccounts());

BLoC vs Cubit:

Cubit BLoC
Entrada Chamada de metodo Evento despachado
Rastreabilidade Media Alta
Boilerplate Medio Alto
Ideal para Logica media Logica complexa / multiplos fluxos

Redux

# pubspec.yaml
redux: ^5.x
flutter_redux: ^0.10.x
// Estado global imutavel
class AppState {
  final List<Account> accounts;
  final bool isLoading;
  const AppState({this.accounts = const [], this.isLoading = false});
  AppState copyWith({List<Account>? accounts, bool? isLoading}) => AppState(
    accounts: accounts ?? this.accounts,
    isLoading: isLoading ?? this.isLoading,
  );
}

// Acoes
class LoadAccountsAction {}
class AccountsLoadedAction {
  final List<Account> accounts;
  AccountsLoadedAction(this.accounts);
}

// Reducer (funcao pura — sem async)
AppState appReducer(AppState state, dynamic action) => switch (action) {
  LoadAccountsAction() => state.copyWith(isLoading: true),
  AccountsLoadedAction(:final accounts) =>
    state.copyWith(accounts: accounts, isLoading: false),
  _ => state,
};

// Middleware (async fica aqui)
void accountsMiddleware(Store<AppState> store, dynamic action, NextDispatcher next) {
  if (action is LoadAccountsAction) {
    AccountService().getAccounts()
      .then((a) => store.dispatch(AccountsLoadedAction(a)));
  }
  next(action);
}

// Setup
final store = Store<AppState>(appReducer,
    initialState: const AppState(), middleware: [accountsMiddleware]);
runApp(StoreProvider(store: store, child: const App()));

// UI
StoreConnector<AppState, List<Account>>(
  converter: (store) => store.state.accounts,
  builder: (context, accounts) => AccountList(accounts: accounts),
)

// Despachar
StoreProvider.of<AppState>(context).dispatch(LoadAccountsAction());

Diferencas chave em relacao ao Provider:

  • Estado e unico e global (uma Store vs multiplos providers)
  • Mutacao so via dispatch + reducer — nao ha metodos no "provider"
  • Logica async fica no middleware, separada do reducer
  • Historico completo de acoes disponivel para debug

MobX

# pubspec.yaml
dependencies:
  mobx: ^2.4.x
  flutter_mobx: ^2.2.x
dev_dependencies:
  mobx_codegen: ^2.6.x
  build_runner: ^2.4.x
// account_store.dart
part 'account_store.g.dart'; // gerado pelo build_runner

class AccountStore = _AccountStore with _$AccountStore;

abstract class _AccountStore with Store {
  @observable
  ObservableList<Account> accounts = ObservableList();

  @observable
  bool isLoading = false;

  @computed
  double get totalBalance =>
      accounts.fold(0.0, (sum, a) => sum + a.balance);

  @action
  Future<void> loadAccounts() async {
    isLoading = true;
    try {
      accounts = ObservableList.of(await AccountService().getAccounts());
    } finally {
      isLoading = false;
    }
  }

  @action
  void addAccount(Account account) => accounts.add(account);
}

// Gerar codigo
// dart run build_runner build --delete-conflicting-outputs

// UI — Observer reconstroi so os widgets que leram observaveis
final store = AccountStore();

Observer(
  builder: (_) => store.isLoading
      ? const CircularProgressIndicator()
      : ListView.builder(
          itemCount: store.accounts.length,
          itemBuilder: (_, i) => AccountWidget(account: store.accounts[i]),
        ),
)

// Acao — sem context, chamada direta
ElevatedButton(
  onPressed: store.loadAccounts,
  child: const Text('Carregar'),
)

Diferencas chave em relacao ao Provider:

  • Reatividade e automatica: MobX rastreia quais observaveis foram lidos e reconstroi so o necessario
  • @computed recalcula o valor derivado automaticamente, sem notifyListeners
  • Nao precisa de BuildContext para acessar o store
  • Exige build_runner — adiciona passo ao workflow de desenvolvimento

Tabela Comparativa Completa

Provider BLoC/Cubit Redux MobX
Mutacao Imperativa emit() / add() Reducer puro @action
Reatividade Manual Via stream Via subscription Automatica
Cod. gerado Nao Nao Nao Sim
Rastreabilidade Baixa Alta Muito alta Media
Boilerplate Baixo Medio–Alto Alto Medio
Curva Baixa Media Alta Media
Testabilidade Boa Excelente Excelente Boa

Armadilhas Comuns

Usar context.read dentro de build()

// ERRADO: nao escuta mudancas
Widget build(BuildContext context) {
  final accounts = context.read<AccountProvider>().accounts;
  return Text('${accounts.length}'); // nao atualiza!
}

// CORRETO
Widget build(BuildContext context) {
  final accounts = context.watch<AccountProvider>().accounts;
  return Text('${accounts.length}'); // atualiza automaticamente
}

Modificar lista interna diretamente

// ERRADO: a UI nao sabe que mudou
void addItem(Account a) {
  _accounts.add(a); // modifica sem notificar
}

// CORRETO
void addItem(Account a) {
  _accounts = [..._accounts, a];
  notifyListeners();
}

Usar context apos await sem checar mounted

// ERRADO: pode crashar se widget foi desmontado
await context.read<AccountProvider>().loadAll();
Navigator.pop(context); // pode ser inválido!

// CORRETO
await context.read<AccountProvider>().loadAll();
if (context.mounted) Navigator.pop(context);

Criar provider dentro de build()

// ERRADO: nova instancia a cada rebuild
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    create: (_) => AccountProvider(), // cria toda vez!
    child: ...,
  );
}

// CORRETO: provider criado fora do build (no main.dart ou acima na arvore)

Recursos e Documentacao

Provider

BLoC / Cubit

Redux

MobX

Riverpod (evolucao natural do Provider)

Flutter — Testes de Integração: Cheatsheet Rápido

Comandos Essenciais

flutter test integration_test/                    # Rodar todos os integration tests
flutter test integration_test/app_test.dart       # Rodar arquivo específico
flutter test integration_test/ --reporter expanded # Output detalhado

# Via flutter drive (com driver)
flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart

# Emulador Android específico
flutter test integration_test/ -d emulator-5554

# Todos os testes (unit + widget + integração)
flutter test && flutter test integration_test/

Configuração Mínima

pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

test_driver/integration_test.dart

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

Estrutura do arquivo de teste

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:meu_app/main.dart' as app;

void main() {
  // OBRIGATÓRIO: inicializar o binding do integration_test
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('descrição do fluxo', (tester) async {
    app.main();                  // inicia o app real
    await tester.pumpAndSettle(); // espera tudo carregar

    // ... interações e verificações
  });
}

Estrutura de Pastas

banco_douro_app/
  integration_test/
    app_test.dart               ← Teste de smoke (app abre?)
    flows/
      login_flow_test.dart      ← Fluxo de login E2E
      account_flow_test.dart    ← Fluxo de contas E2E
    pages/
      login_page.dart           ← Page Object: login
      dashboard_page.dart       ← Page Object: dashboard
      add_account_page.dart     ← Page Object: modal de conta
  test_driver/
    integration_test.dart       ← Driver (flutter drive)

Keys na UI (Essencial para Integration Tests)

Adicionar Keys nos widgets

// TextField / TextFormField
TextFormField(
  key: const Key('emailField'),
  ...
)

// ElevatedButton / botões
ElevatedButton(
  key: const Key('loginButton'),
  ...
)

// Scaffold (identifica a tela)
Scaffold(
  key: const Key('loginScreen'),
  ...
)

// ListView
ListView.builder(
  key: const Key('accountList'),
  ...
)

Encontrar por Key no teste

find.byKey(const Key('emailField'))
find.byKey(const Key('loginButton'))

Interações do Tester

// Digitar texto
await tester.enterText(find.byKey(const Key('emailField')), 'texto');

// Tocar em elemento
await tester.tap(find.byKey(const Key('loginButton')));
await tester.tap(find.text('Salvar'));

// Rolar lista
await tester.drag(find.byKey(const Key('accountList')), const Offset(0, -300));
await tester.scrollUntilVisible(find.text('Item'), 100);

// Segurar (long press)
await tester.longPress(find.byKey(const Key('accountItem')));

// Arrastar (drag and drop)
await tester.drag(find.byKey(const Key('draggable')), const Offset(200, 0));

pump vs pumpAndSettle

// pump() — processa UM frame
await tester.pump();

// pump(Duration) — processa frames por X tempo
await tester.pump(const Duration(milliseconds: 500));

// pumpAndSettle() — aguarda até TODAS as animações e futures terminarem
await tester.pumpAndSettle();

// pumpAndSettle com timeout (para operações de rede)
await tester.pumpAndSettle(const Duration(seconds: 10));

Quando usar cada um:

Situação Use
Verificar estado intermediário pump()
Aguardar animação de 300ms pump(Duration(milliseconds: 300))
Após tap em botão com navegação pumpAndSettle(Duration(seconds: 5))
Após operação de rede pumpAndSettle(Duration(seconds: 10))
Verificar loading spinner pump() antes do pumpAndSettle()

Finders

find.byKey(const Key('id'))         // Por Key — mais estável
find.text('Entrar')                 // Por texto exato
find.textContaining('Olá')         // Por texto parcial
find.byType(TextField)              // Por tipo de widget
find.byIcon(Icons.add)              // Por ícone
find.byWidget(meuWidget)            // Por instância

// Combinando finders
find.descendant(
  of: find.byKey(const Key('form')),
  matching: find.byType(TextFormField),
)
find.ancestor(
  of: find.text('Salvar'),
  matching: find.byType(ElevatedButton),
)

expect em Integration Tests

// Verificar presença
expect(find.byKey(const Key('dashboardScreen')), findsOneWidget);
expect(find.text('Entrar'), findsOneWidget);

// Verificar ausência
expect(find.text('Loading...'), findsNothing);

// Verificar múltiplos
expect(find.byType(AccountWidget), findsWidgets);    // 1 ou mais
expect(find.byType(TextField), findsNWidgets(2));    // exatamente 2

Page Object Model

// integration_test/pages/login_page.dart
class LoginPage {
  final WidgetTester tester;
  const LoginPage(this.tester);

  Future<void> isVisible() async {
    expect(find.byKey(const Key('loginScreen')), findsOneWidget);
  }

  Future<void> login({required String email, required String password}) async {
    await tester.enterText(find.byKey(const Key('emailField')), email);
    await tester.enterText(find.byKey(const Key('passwordField')), password);
    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle(const Duration(seconds: 10));
  }
}

// Uso no teste
final loginPage = LoginPage(tester);
await loginPage.isVisible();
await loginPage.login(email: 'admin@admin.com', password: 'admin');

Inicialização e Teardown

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Fluxo de Contas', () {
    // Helper para login (reutilizado em vários testes)
    Future<void> doLogin(WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      await tester.enterText(find.byKey(const Key('emailField')), 'admin@admin.com');
      await tester.enterText(find.byKey(const Key('passwordField')), 'admin');
      await tester.tap(find.byKey(const Key('loginButton')));
      await tester.pumpAndSettle(const Duration(seconds: 10));
    }

    testWidgets('lista contas', (tester) async {
      await doLogin(tester);
      // ...
    });
  });
}

Screenshots (com flutter drive)

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('tira screenshot', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    await binding.takeScreenshot('01_login_screen');

    await tester.tap(find.byKey(const Key('loginButton')));
    await tester.pumpAndSettle(const Duration(seconds: 5));

    await binding.takeScreenshot('02_dashboard_screen');
  });
}

Metodologias — Referência Rápida

TDD — Test-Driven Development

RED → GREEN → REFACTOR

1. Escrever teste que falha (RED)
2. Implementar o mínimo para passar (GREEN)
3. Melhorar sem quebrar os testes (REFACTOR)
4. Repetir
// RED: teste primeiro
test('deve lançar exceção quando saldo insuficiente', () {
  expect(() => validateTransaction(...), throwsA(isA<InsufficientFundsException>()));
});

// GREEN: implementação mínima
void validateTransaction(...) {
  if (sender.balance < amount) throw InsufficientFundsException('Saldo insuficiente');
}

// REFACTOR: melhorar a mensagem e adicionar validações extras

BDD — Behavior-Driven Development

Given [estado inicial]
When  [ação do usuário]
Then  [resultado esperado]
// Nomenclatura BDD nos testes Flutter
group('Dado que o usuário está logado', () {
  group('Quando ele adiciona uma nova conta', () {
    testWidgets('Então a conta aparece na lista', (tester) async {
      // ...
    });
  });
});

DDD — Domain-Driven Design

Testar por camada:

Domínio (models, helpers):   → Testes de unidade puros
Repositórios (SQLite, HTTP): → Testes com mocks
Providers (estado):          → Testes com mocks
UI (telas):                  → Testes de widget
Fluxo completo:              → Testes de integração

Priorização de Testes

Alta prioridade (sempre testar):
  ✅ Cálculos financeiros e de negócio
  ✅ Validações críticas (saldo insuficiente, campos obrigatórios)
  ✅ Fluxos de autenticação
  ✅ Operações que alteram dados (criar, editar, deletar)

Média prioridade:
  ✅ Telas principais (login, dashboard)
  ✅ Estados de loading e erro
  ✅ Fallback para dados locais

Baixa prioridade:
  ⚠️  Animações decorativas
  ⚠️  Cores e estilos
  ⚠️  Textos estáticos sem lógica

CI — GitHub Actions

# .github/workflows/integration_tests.yml
name: Integration Tests

on:
  pull_request:
    branches: [main]

jobs:
  integration_test:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.x'
      - run: flutter pub get
        working-directory: banco_douro_app
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          script: cd banco_douro_app && flutter test integration_test/

Boas Práticas Anti-Flakiness

// ✅ Use Keys em vez de textos dinâmicos
find.byKey(const Key('loginButton'))   // estável
find.text('Entrar')                     // frágil se o texto mudar

// ✅ Timeout generoso para operações de rede
await tester.pumpAndSettle(const Duration(seconds: 10));

// ✅ Cada teste é autossuficiente (não depende de outros)
// No setUp ou início de cada testWidgets, comece do zero

// ✅ Limpe dados entre testes quando necessário
setUp(() async {
  await DatabaseHelper.instance.clearAll();
});

// ✅ Prefira scrollUntilVisible a assumir posição do item
await tester.scrollUntilVisible(find.text('Item distante'), 100);

Tipos de Teste — Tabela Comparativa

Tipo Pacote Ambiente Vel. Custo Quando
Unidade flutter_test Dart puro Rápido Baixo Lógica, helpers, models
Widget flutter_test Engine simulada Rápido Médio Componentes de UI
Integração integration_test Emulador/físico Lento Alto Fluxos completos E2E
Mock mockito Qualquer Rápido Baixo Isolar dependências externas

Pirâmide de Testes

      /\
     /E2E\      ← Poucos (lentos, caros, frágeis)
    /------\       integration_test — fluxos completos
   / Widget \
  /  Tests  \   ← Médios — flutter_test + pumpWidget
 /------------\
/   Unidade   \ ← Muitos (rápidos, baratos, estáveis)
/______________\   flutter_test — funções, classes, providers

Meta: A base deve ser larga (muitos testes de unidade), o topo estreito (poucos E2E cobrindo os fluxos críticos).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment