Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save tiagolpadua/6b406c43097da3486bf48f25c40be6f2 to your computer and use it in GitHub Desktop.

Select an option

Save tiagolpadua/6b406c43097da3486bf48f25c40be6f2 to your computer and use it in GitHub Desktop.
Flutter - Material Complementar

Flutter — Transições de Tela com Animations e Hero: Cheatsheet Rápido

Comandos Essenciais

# Instalar dependências após adicionar o pacote animations
flutter pub get

# Rodar o app para visualizar as transições
flutter run

# Ativar slow animations via Flutter DevTools (no navegador, aba "Performance")
# Ou via emulador Android: Configurações → Opções do desenvolvedor → Escala de animação → 10x
# Ou via código, adicionando temporariamente no main():
# timeDilation = 10.0;  // requer import 'package:flutter/scheduler.dart'

# Verificar erros após as modificações
flutter analyze

Instalação — pacote animations

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  animations: ^2.0.11

Import nos arquivos Dart

import 'package:animations/animations.dart';

SharedAxisTransition

Via PageRouteBuilder (navegação imperativa)

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (_, __, ___) => const MinhaProximaTela(),
    transitionsBuilder: (_, animation, secondaryAnimation, child) {
      return SharedAxisTransition(
        animation: animation,
        secondaryAnimation: secondaryAnimation,
        transitionType: SharedAxisTransitionType.scaled,   // ← trocar conforme necessidade
        child: child,
      );
    },
  ),
);

Tipos disponíveis

Tipo Movimento Quando usar
SharedAxisTransitionType.horizontal Slide lateral (esq/dir) Wizard, onboarding (passo 1 → 2)
SharedAxisTransitionType.vertical Slide vertical (cima/baixo) Hierarquia pai → filho
SharedAxisTransitionType.scaled Fade + escala (zoom) Login → app, troca de contexto

Via onGenerateRoute (rotas nomeadas)

// main.dart
MaterialApp(
  onGenerateRoute: AppRoutes.generate,
  initialRoute: 'login',
)

// lib/ui/routes/app_routes.dart
class AppRoutes {
  static Route<dynamic> generate(RouteSettings settings) {
    switch (settings.name) {
      case 'login':
        return _sharedAxisRoute(const LoginScreen(), settings);
      case 'dashboard':
        return _sharedAxisRoute(const DashboardScreen(), settings);
      default:
        return MaterialPageRoute(builder: (_) => const LoginScreen());
    }
  }

  static PageRouteBuilder _sharedAxisRoute(Widget page, RouteSettings settings) {
    return PageRouteBuilder(
      settings: settings,
      pageBuilder: (_, __, ___) => page,
      transitionsBuilder: (_, animation, secondaryAnimation, child) {
        return SharedAxisTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          transitionType: SharedAxisTransitionType.scaled,
          child: child,
        );
      },
    );
  }
}

FadeThroughTransition

Via PageRouteBuilder

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (_, __, ___) => const OutraTela(),
    transitionsBuilder: (_, animation, secondaryAnimation, child) {
      return FadeThroughTransition(
        animation: animation,
        secondaryAnimation: secondaryAnimation,
        child: child,
      );
    },
  ),
);

Via PageTransitionSwitcher (troca de widgets na mesma tela)

PageTransitionSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
    return FadeThroughTransition(
      animation: primaryAnimation,
      secondaryAnimation: secondaryAnimation,
      child: child,
    );
  },
  child: isLoading
      ? const CircularProgressIndicator(key: ValueKey('loading'))
      : MeuConteudo(key: ValueKey('content-${dados.length}')),
)

Atenção: sempre forneça key única para cada filho do PageTransitionSwitcher. Sem key, não há detecção de mudança e a transição não é acionada.

FadeThrough para BottomNavigationBar

class _HomeState extends State<HomeScreen> {
  int _index = 0;
  final _pages = const [DashboardScreen(), ExtratoScreen(), PerfilScreen()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageTransitionSwitcher(
        transitionBuilder: (child, primary, secondary) => FadeThroughTransition(
          animation: primary,
          secondaryAnimation: secondary,
          child: child,
        ),
        child: KeyedSubtree(
          key: ValueKey(_index),
          child: _pages[_index],
        ),
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _index,
        onDestinationSelected: (i) => setState(() => _index = i),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.home), label: 'Início'),
          NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Extrato'),
          NavigationDestination(icon: Icon(Icons.person), label: 'Perfil'),
        ],
      ),
    );
  }
}

ContainerTransform — OpenContainer

Configuração mínima

OpenContainer(
  closedBuilder: (context, openContainer) {
    return MeuCard(onTap: openContainer);
  },
  openBuilder: (context, closeContainer) {
    return MinhaTelaDetalhe();
  },
)

Configuração completa

OpenContainer<bool>(                              // tipo de retorno opcional
  transitionDuration: const Duration(milliseconds: 400),
  transitionType: ContainerTransitionType.fadeThrough,
  closedElevation: 2,
  openElevation: 0,
  closedShape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  closedColor: AppColors.cardBackground,
  openColor: AppColors.background,
  onClosed: (returnValue) {
    // chamado quando o usuário fecha a tela aberta
    // returnValue é o valor retornado por closeContainer(value)
  },
  closedBuilder: (context, openContainer) {
    return InkWell(
      onTap: openContainer,
      child: MeuCard(),
    );
  },
  openBuilder: (context, closeContainer) {
    return MinhaTelaDetalhe(
      onFechar: () => closeContainer(true),
    );
  },
)

Tipos de transição do ContainerTransform

Tipo Comportamento
ContainerTransitionType.fade Conteúdo do container some durante a expansão
ContainerTransitionType.fadeThrough Novo conteúdo surge com fade no meio da transição

Hero

Uso básico (duas telas)

// Tela A (origem)
Hero(
  tag: 'meu-elemento-unico',      // tag deve ser idêntica em ambas as telas
  child: MeuWidget(),
)

// Tela B (destino)
Hero(
  tag: 'meu-elemento-unico',
  child: MeuWidgetMaior(),         // pode ter tamanho e forma diferentes
)

Hero em listas — tag dinâmica

// Use o ID do item para garantir unicidade
Hero(
  tag: 'account-icon-${account.id}',
  child: CircleAvatar(radius: 20, child: Icon(Icons.account_balance)),
)

Regras importantes do Hero

✓  tag deve ser única na tela (dois Hero com o mesmo tag na mesma tela = erro)
✓  tag deve ser idêntica em origem e destino
✓  funciona com MaterialPageRoute, PageRouteBuilder e CupertinoPageRoute
✓  funciona com push, pushReplacement, pushAndRemoveUntil
✗  não funciona dentro de showDialog sem overlay customizado
✗  Hero dentro de Hero não é suportado

Customizar o widget durante o voo

Hero(
  tag: 'account-icon-${account.id}',
  flightShuttleBuilder: (_, animation, __, fromContext, toContext) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, _) {
        final radius = 20 + (animation.value * 28);   // 20 → 48
        return CircleAvatar(
          radius: radius,
          child: const Icon(Icons.account_balance),
        );
      },
    );
  },
  child: CircleAvatar(radius: 20, child: Icon(Icons.account_balance)),
)

Dismissible — Swipe to Delete

Dismissible mínimo

Dismissible(
  key: ValueKey(item.id),                    // obrigatório e único por item
  direction: DismissDirection.endToStart,    // direita → esquerda
  onDismissed: (_) => deleteItem(item.id),
  child: MeuCard(),
)

Com fundo revelado e confirmação

Dismissible(
  key: ValueKey(account.id),
  direction: DismissDirection.endToStart,
  background: Container(
    color: AppColors.negative,
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.only(right: 24),
    child: const Icon(Icons.delete_outline, color: Colors.white),
  ),
  confirmDismiss: (_) async {
    return await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Excluir?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx, false),
              child: const Text('Cancelar')),
          TextButton(onPressed: () => Navigator.pop(ctx, true),
              child: const Text('Excluir')),
        ],
      ),
    );
  },
  onDismissed: (_) => onDelete?.call(),
  child: MeuCard(),
)

Direções disponíveis

Valor Comportamento
DismissDirection.endToStart Direita → esquerda (mais comum)
DismissDirection.startToEnd Esquerda → direita
DismissDirection.horizontal Ambas as direções
DismissDirection.vertical Cima ou baixo
DismissDirection.none Desabilita o swipe

AnimatedList

AnimatedList mínimo

// 1. Declare a key
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();

// 2. Use AnimatedList no lugar de ListView
AnimatedList(
  key: _listKey,
  initialItemCount: items.length,
  itemBuilder: (context, index, animation) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(1, 0),
        end: Offset.zero,
      ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
      child: MeuItem(items[index]),
    );
  },
)

// 3. Inserir com animação
_listKey.currentState?.insertItem(index,
    duration: const Duration(milliseconds: 350));

// 4. Remover com animação
_listKey.currentState?.removeItem(
  index,
  (context, animation) => FadeTransition(
    opacity: animation,
    child: MeuItem(itemRemovido),   // snapshot do item que está saindo
  ),
  duration: const Duration(milliseconds: 300),
);

Regras importantes

✓  initialItemCount deve coincidir com o tamanho da lista local no initState
✓  Sempre manter uma lista local (_localItems) sincronizada com o AnimatedList
✓  removeItem precisa de um builder para exibir o item enquanto ele sai
✓  insertItem e removeItem devem ser chamados APÓS alterar _localItems
✗  Não usar provider.items diretamente como fonte — o AnimatedList não detecta mudanças

AnimatedIcon no FAB

Setup completo

// 1. Mixin no State
class _MeuState extends State<MinhaWidget>
    with SingleTickerProviderStateMixin {

  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _controller.dispose();   // sempre fazer dispose!
    super.dispose();
  }

// 2. Animar forward/reverse
  void _abrir() {
    _controller.forward();   // 0.0 → 1.0
  }

  void _fechar() {
    _controller.reverse();   // 1.0 → 0.0
  }

// 3. Usar no FAB
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: _abrir,
      child: AnimatedIcon(
        icon: AnimatedIcons.add_event,
        progress: _controller,
        color: Colors.white,
      ),
    );
  }
}

Pares de ícones disponíveis

Constante De → Para
AnimatedIcons.add_event mais → calendário
AnimatedIcons.menu_arrow hamburguer → seta esquerda
AnimatedIcons.menu_close hamburguer → fechar
AnimatedIcons.play_pause play → pause
AnimatedIcons.search_ellipsis lupa → reticências
AnimatedIcons.arrow_menu seta esquerda → hamburguer
AnimatedIcons.close_menu fechar → hamburguer
AnimatedIcons.event_add calendário → mais
AnimatedIcons.ellipsis_search reticências → lupa
AnimatedIcons.pause_play pause → play

SingleTicker vs MultiTicker

Mixin Quando usar
SingleTickerProviderStateMixin Um único AnimationController no State
TickerProviderStateMixin Dois ou mais AnimationControllers

Snackbar com Ação "Desfazer"

ScaffoldMessenger.of(context).clearSnackBars();   // limpa snackbars anteriores
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('${item.name} excluído.'),
    duration: const Duration(seconds: 4),
    action: SnackBarAction(
      label: 'Desfazer',
      onPressed: () {
        // restaurar localmente + reinserir na lista
        provider.restoreItemLocally(item, index);
        _listKey.currentState?.insertItem(index,
            duration: const Duration(milliseconds: 350));
      },
    ),
  ),
);

Tabela de Decisão Rápida

Qual transição usar?

  Usuário toca num card → abre tela de detalhe
    └─► OpenContainer

  Um ícone/imagem específico conecta origem e destino
    └─► Hero  (pode combinar com OpenContainer)

  Troca de abas (BottomNavBar)
    └─► FadeThroughTransition + PageTransitionSwitcher

  Login → Dashboard (entrar no app)
    └─► SharedAxisTransition.scaled

  Passo 1 → Passo 2 → Passo 3 (wizard)
    └─► SharedAxisTransition.horizontal

  Lista → Detalhe sem elemento visual conectando
    └─► SharedAxisTransition.vertical

  Loading → Conteúdo carregado (na mesma tela)
    └─► FadeThroughTransition + PageTransitionSwitcher

Acessibilidade — Reduce Motion

// Verificar preferência do usuário
final reduceMotion = MediaQuery.of(context).disableAnimations;

// SharedAxisTransition, FadeThroughTransition e OpenContainer
// já respeitam disableAnimations automaticamente.

// Se construir animações customizadas, verificar manualmente:
final duration = reduceMotion
    ? Duration.zero
    : const Duration(milliseconds: 400);

Estrutura de Arquivos Recomendada

lib/
├── ui/
│   ├── routes/
│   │   └── app_routes.dart       ← centraliza PageRouteBuilders
│   ├── screens/
│   │   ├── login_screen.dart
│   │   ├── splash_screen.dart
│   │   ├── dashboard_screen.dart
│   │   └── account_detail_screen.dart
│   └── widgets/
│       └── account_widget.dart   ← contém OpenContainer
└── main.dart                     ← usa onGenerateRoute: AppRoutes.generate

Boas Práticas

Fazer Evitar
Centralizar rotas em AppRoutes PageRouteBuilder inline espalhados pelo codigo
Duracao entre 300ms-500ms para transicoes de tela Transicoes acima de 600ms, tornam o app lento
Tags de Hero com ID do objeto em listas Tag estatica em listas ('account-icon' sem ID)
Testar com slow animations (10x) no emulador Avaliar animacoes apenas na velocidade normal
Fornecer key unica nos filhos do PageTransitionSwitcher Omitir key — transicao nao e acionada
Usar Dismissible com confirmDismiss para acoes destrutivas Dispensar sem confirmacao do usuario
Chamar dispose() no AnimationController Vazar controllers sem dispose — causa memory leak

Resumo dos Widgets

Widget Pacote Para que serve
SharedAxisTransition animations Transicao com eixo (X, Y ou Z) entre rotas
FadeThroughTransition animations Crossfade sem hierarquia entre destinos
OpenContainer animations Card/item expande e vira uma tela inteira
PageTransitionSwitcher animations Motor para trocar widgets com transicao customizada
Hero Flutter nativo Elemento "voa" entre duas telas durante a transicao
PageRouteBuilder Flutter nativo Define rota com transitionsBuilder customizado
Dismissible Flutter nativo Swipe para dispensar/excluir um item da lista
AnimatedList Flutter nativo Lista com animacao de insercao e remocao de items
AnimatedIcon Flutter nativo Anima entre dois icones usando um AnimationController
AnimationController Flutter nativo Controla progresso, direcao e duracao de animacoes

Flutter — Distribuição Automatizada: Firebase App Distribution e CI/CD

Firebase App Distribution

Setup do projeto Firebase

console.firebase.google.com
  → Adicionar projeto → banco-douro-app
  → Registrar app Android: package name → baixar google-services.json
  → Registrar app iOS: bundle ID → baixar GoogleService-Info.plist

Conta de serviço Firebase

Firebase Console → ⚙️ Configurações → Contas de serviço
  → Gerar nova chave privada → salvar JSON

Google Cloud Console → IAM
  → Editar conta de serviço
  → Adicionar papel: Firebase App Distribution Admin

Grupos de testers

Firebase Console → App Distribution → Testers e grupos → Criar grupo
  → qa-team      (equipe interna de QA)
  → stakeholders (aprovadores e gerentes)

Upload manual (Firebase CLI)

npm install -g firebase-tools
firebase login

firebase appdistribution:distribute app-release.apk \
  --app $FIREBASE_ANDROID_APP_ID \
  --groups qa-team \
  --release-notes "Descrição da versão"

Variáveis no Codemagic — grupo firebase_credentials

FIREBASE_SERVICE_ACCOUNT_CREDENTIALS  → JSON da conta de serviço (secure)
FIREBASE_ANDROID_APP_ID               → 1:123...:android:abc (não secure)
FIREBASE_IOS_APP_ID                   → 1:123...:ios:def (não secure)
GOOGLE_SERVICES_JSON_BASE64           → google-services.json em base64 (secure)

Converter google-services.json para base64:

base64 -i android/app/google-services.json | pbcopy   # macOS
base64 -w 0 android/app/google-services.json          # Linux

Seção publishing.firebase no codemagic.yaml

publishing:
  firebase:
    firebase_service_account: $FIREBASE_SERVICE_ACCOUNT_CREDENTIALS
    android:
      app_id: $FIREBASE_ANDROID_APP_ID
      groups:
        - qa-team
      artifact_type: apk
      release_notes:
        - language: pt-br
          text: "Build $BUILD_NUMBER — Branch: $CM_BRANCH — Commit: $CM_COMMIT"
    ios:
      app_id: $FIREBASE_IOS_APP_ID
      groups:
        - qa-team
      artifact_type: ipa
      release_notes:
        - language: pt-br
          text: "Build $BUILD_NUMBER — Branch: $CM_BRANCH"

Google Play via Codemagic

Conta de serviço Google Play

Google Play Console → Configuração → Acesso à API
  → Criar novo projeto de serviço
  → Google Cloud Console → Criar conta de serviço → Baixar JSON
  → Google Play Console → Conceder permissão: Release Manager

Variáveis no Codemagic — grupo google_play

GCLOUD_SERVICE_ACCOUNT_CREDENTIALS  → JSON da conta de serviço (secure)
PACKAGE_NAME                        → com.bancodouro.app

Seção publishing.google_play no codemagic.yaml

publishing:
  google_play:
    credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
    track: internal          # internal | alpha | beta | production
    submit_as_draft: false   # true = salva como rascunho sem publicar

App Store Connect via Codemagic

API Key App Store Connect

App Store Connect → Usuários e Acesso → Chaves → API Keys
  → Gerar Chave de API → Role: App Manager
  → Baixar .p8 (disponível para download apenas uma vez)
  → Anotar: Key ID e Issuer ID

Variáveis no Codemagic — grupo app_store

APP_STORE_CONNECT_KEY_IDENTIFIER  → Key ID
APP_STORE_CONNECT_ISSUER_ID       → Issuer ID
APP_STORE_CONNECT_PRIVATE_KEY     → conteúdo do .p8 (secure)

Seção publishing.app_store_connect no codemagic.yaml

publishing:
  app_store_connect:
    api_key: $APP_STORE_CONNECT_PRIVATE_KEY
    key_id: $APP_STORE_CONNECT_KEY_IDENTIFIER
    issuer_id: $APP_STORE_CONNECT_ISSUER_ID
    submit_to_testflight: true    # envia para TestFlight após upload
    submit_to_app_store: false    # nunca automatizar sem aprovação humana

codemagic.yaml — Workflows Completos desta Aula

workflows:

  # CI: roda em todo push e PR
  ci-tests:
    name: CI — Análise e Testes
    instance_type: mac_mini_m1
    triggering:
      events:
        - push
        - pull_request
    environment:
      flutter: stable
    working_directory: banco_douro_app
    scripts:
      - name: Dependências
        script: flutter pub get
      - name: Análise estática
        script: flutter analyze --fatal-infos
      - name: Formatação
        script: dart format --set-exit-if-changed .
      - name: Testes
        script: flutter test --coverage

  # CD Staging: push main → Firebase App Distribution → qa-team
  cd-staging:
    name: CD — Staging (Firebase App Distribution)
    instance_type: mac_mini_m1
    triggering:
      events:
        - push
      branch_patterns:
        - pattern: main
          include: true
    environment:
      flutter: stable
      vars:
        FIREBASE_ANDROID_APP_ID: "1:123456789:android:abc123"
      groups:
        - firebase_credentials
        - android_signing
    working_directory: banco_douro_app
    scripts:
      - name: Dependências
        script: flutter pub get
      - name: Testes
        script: flutter test
      - name: Restaurar google-services.json
        script: |
          echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > \
            android/app/google-services.json
      - name: Build APK
        script: |
          flutter build apk --release \
            --build-number=$BUILD_NUMBER
    artifacts:
      - banco_douro_app/build/app/outputs/flutter-apk/app-release.apk
    publishing:
      firebase:
        firebase_service_account: $FIREBASE_SERVICE_ACCOUNT_CREDENTIALS
        android:
          app_id: $FIREBASE_ANDROID_APP_ID
          groups:
            - qa-team
          artifact_type: apk
          release_notes:
            - language: pt-br
              text: "Build $BUILD_NUMBER — Branch: $CM_BRANCH — Commit: $CM_COMMIT"

  # CD Produção Android: tag v* → Google Play (internal track)
  cd-production-android:
    name: CD — Produção Android (Google Play)
    instance_type: mac_mini_m1
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*"
          include: true
    environment:
      flutter: stable
      groups:
        - google_play
        - android_signing
    working_directory: banco_douro_app
    scripts:
      - name: Dependências
        script: flutter pub get
      - name: Testes
        script: flutter test
      - name: Build AAB produção
        script: |
          flutter build appbundle --release \
            --build-number=$BUILD_NUMBER
    artifacts:
      - banco_douro_app/build/app/outputs/bundle/release/app-release.aab
    publishing:
      google_play:
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        track: internal
        submit_as_draft: false

  # CD Produção iOS: tag v* → App Store Connect (TestFlight)
  cd-production-ios:
    name: CD — Produção iOS (App Store)
    instance_type: mac_mini_m1
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*"
          include: true
    environment:
      flutter: stable
      groups:
        - app_store
        - ios_signing
    working_directory: banco_douro_app
    scripts:
      - name: Dependências
        script: flutter pub get
      - name: Testes
        script: flutter test
      - name: Build IPA produção
        script: |
          flutter build ipa --release \
            --build-number=$BUILD_NUMBER \
            --export-options-plist=/Users/builder/export_options.plist
    artifacts:
      - banco_douro_app/build/ios/ipa/*.ipa
    publishing:
      app_store_connect:
        api_key: $APP_STORE_CONNECT_PRIVATE_KEY
        key_id: $APP_STORE_CONNECT_KEY_IDENTIFIER
        issuer_id: $APP_STORE_CONNECT_ISSUER_ID
        submit_to_testflight: true
        submit_to_app_store: false

Trigger por Evento — Pipeline Completo

Evento Workflow Destino
push (qualquer branch) ci-tests — (só valida)
pull_request ci-tests — (só valida)
push para main cd-staging Firebase App Distribution → qa-team
tag v* cd-production-android Google Play (internal track)
tag v* cd-production-ios App Store Connect (TestFlight)

Como criar e publicar uma tag de release

git tag v1.0.0
git push origin v1.0.0

Comparação: Firebase App Distribution vs. Lojas

Aspecto Firebase App Distribution Google Play / App Store
Público Testers internos e QA Usuários finais
Revisão Nenhuma Obrigatória
Tempo até o tester Segundos Horas a dias
Rastreamento de instalação Sim Parcialmente (Play Console)
Versão permanente Não (expira) Sim
Feedback in-app Sim (screenshot + comentário) Via reviews públicas
Restrição iOS Dispositivo precisa estar no profile Ad Hoc Apenas necessita conta Apple

Checklist desta Aula

FIREBASE APP DISTRIBUTION
✅ Projeto Firebase criado
✅ App Android e iOS registrados no Firebase
✅ google-services.json e GoogleService-Info.plist no .gitignore
✅ Conta de serviço Firebase com role App Distribution Admin
✅ FIREBASE_SERVICE_ACCOUNT_CREDENTIALS como secret no Codemagic
✅ GOOGLE_SERVICES_JSON_BASE64 como secret no Codemagic
✅ Grupos qa-team e stakeholders criados no Firebase Console
✅ Workflow cd-staging com publishing.firebase no codemagic.yaml
✅ Push para main → Firebase → testers notificados por e-mail

GOOGLE PLAY
✅ Conta de serviço Google Play com role Release Manager
✅ GCLOUD_SERVICE_ACCOUNT_CREDENTIALS como secret no Codemagic
✅ Workflow cd-production-android com publishing.google_play
✅ Pipeline: tag v* → Google Play internal track

APP STORE CONNECT
✅ API Key gerada (.p8 guardado — disponível para download apenas uma vez)
✅ Key ID e Issuer ID anotados
✅ APP_STORE_CONNECT_* como secrets no Codemagic
✅ Workflow cd-production-ios com publishing.app_store_connect
✅ Pipeline: tag v* → App Store Connect (TestFlight)

Resumo de Conceitos

Conceito Definição rápida
Firebase App Distribution Distribuição de builds para testers sem revisão de loja
Conta de serviço Credencial de serviço com escopo limitado — padrão para CI/CD
Grupos de testers Organizar testers por papel (qa-team, stakeholders)
publishing.firebase Upload automático para Firebase App Distribution via Codemagic
publishing.google_play Publicação automática na Google Play via Codemagic
publishing.app_store_connect Publicação automática na App Store via Codemagic
Trigger por tag v* Portão humano deliberado antes do lançamento para produção
track: internal Track da Google Play sem revisão pública — para validar o fluxo de CD
submit_to_testflight: true Enviar para TestFlight automaticamente após upload no App Store Connect
submit_to_app_store: false Manter aprovação manual para o submit final à App Store

Flutter Cheatsheet - Aula 01

Introducao ao Flutter e Dart

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


Indice

  1. Comandos Essenciais
  2. Estrutura do Projeto
  3. Widgets Fundamentais
  4. Layout Basico
  5. Estilizacao
  6. Navegacao
  7. Recursos e Documentacao

Comandos Essenciais

Setup e Verificacao

# Verificar instalacao do Flutter
flutter doctor
flutter doctor -v              # Versao detalhada

# Criar novo projeto
flutter create nome_projeto
flutter create --org com.empresa nome_projeto

# Listar dispositivos disponiveis
flutter devices

Documentacao: Get Started

Executar Aplicacao

# Rodar app
flutter run
flutter run -d chrome          # Web
flutter run -d macos           # macOS
flutter run -d <device-id>     # Dispositivo especifico

# Atalhos durante execucao
r    # Hot Reload (recarrega codigo)
R    # Hot Restart (reinicia app)
q    # Sair

Documentacao: flutter run

Gerenciamento de Dependencias

flutter pub get                # Instalar dependencias
flutter pub upgrade            # Atualizar dependencias
flutter pub outdated           # Ver desatualizadas
flutter pub add nome_pacote    # Adicionar pacote

Documentacao: Using packages

Build e Analise

flutter build apk              # Android APK
flutter build ios              # iOS
flutter build web              # Web
flutter analyze                # Analisar codigo
flutter test                   # Rodar testes
flutter clean                  # Limpar cache

Estrutura do Projeto

pubspec.yaml

name: meu_app
description: Descricao do app
version: 1.0.0+1

environment:
  sdk: ">=3.0.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
  assets:
    - assets/images/

Documentacao: pubspec file

Estrutura de Pastas Recomendada

lib/
├── main.dart           # Ponto de entrada
├── screens/            # Telas do app
├── widgets/            # Widgets reutilizaveis
├── models/             # Classes de dados
├── services/           # Logica de negocio
└── utils/              # Utilitarios

Widgets Fundamentais

Estrutura Minima do App

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Meu App',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: HomePage(),
    );
  }
}

Documentacao: MaterialApp

Scaffold

Scaffold(
  appBar: AppBar(
    title: Text('Titulo'),
    actions: [
      IconButton(icon: Icon(Icons.search), onPressed: () {}),
    ],
  ),
  body: Center(child: Text('Conteudo')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  bottomNavigationBar: BottomNavigationBar(...),
)

Documentacao: Scaffold

StatelessWidget

class MeuWidget extends StatelessWidget {
  final String titulo;

  const MeuWidget({required this.titulo});

  @override
  Widget build(BuildContext context) {
    return Text(titulo);
  }
}

Documentacao: StatelessWidget

StatefulWidget

class Contador extends StatefulWidget {
  @override
  State<Contador> createState() => _ContadorState();
}

class _ContadorState extends State<Contador> {
  int _count = 0;

  void _incrementar() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$_count'),
        ElevatedButton(
          onPressed: _incrementar,
          child: Text('Incrementar'),
        ),
      ],
    );
  }
}

Documentacao: StatefulWidget


Layout Basico

Container

Container(
  width: 100,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
    boxShadow: [
      BoxShadow(color: Colors.black26, blurRadius: 4),
    ],
  ),
  child: Text('Conteudo'),
)

Documentacao: Container

Column e Row

// Column (vertical)
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Item 1'),
    Text('Item 2'),
  ],
)

// Row (horizontal)
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Icon(Icons.star),
    Text('5.0'),
  ],
)

Documentacao: Column | Row

ListView

// ListView simples
ListView(
  children: [
    ListTile(title: Text('Item 1')),
    ListTile(title: Text('Item 2')),
  ],
)

// ListView.builder (eficiente para listas longas)
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(title: Text(items[index]));
  },
)

Documentacao: ListView

Stack

Stack(
  alignment: Alignment.center,
  children: [
    Container(width: 200, height: 200, color: Colors.blue),
    Text('Sobreposto'),
  ],
)

Documentacao: Stack


Estilizacao

Text e TextStyle

Text(
  'Hello Flutter',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

Documentacao: Text

Cores

// Cores predefinidas
Colors.red
Colors.blue[500]       // Com variacao

// Cores customizadas
Color(0xFF42A5F5)                      // Hex
Color.fromRGBO(66, 165, 245, 1.0)      // RGBA
Colors.blue.withOpacity(0.5)           // Com opacidade

Documentacao: Colors

EdgeInsets (Padding/Margin)

EdgeInsets.all(16)                              // Todos iguais
EdgeInsets.symmetric(horizontal: 16, vertical: 8)
EdgeInsets.only(left: 16, top: 8)
EdgeInsets.fromLTRB(16, 8, 16, 8)               // L, T, R, B

Documentacao: EdgeInsets

SizedBox (Espacamento)

SizedBox(height: 16)   // Espaco vertical
SizedBox(width: 16)    // Espaco horizontal

Navegacao

Navegacao Basica

// Ir para nova tela
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => NovaTela()),
);

// Voltar
Navigator.pop(context);

// Passar dados
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetalhesScreen(id: 123),
  ),
);

Documentacao: Navigation

Rotas Nomeadas

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/detail': (context) => DetailScreen(),
  },
)

// Navegar
Navigator.pushNamed(context, '/detail');

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Documentacao Flutter docs.flutter.dev
API Reference api.flutter.dev
Catalogo de Widgets Widget Catalog
Cookbook (Receitas) Cookbook
Codelabs codelabs.developers.google.com

Ferramentas

Ferramenta Link
DartPad (Playground) dartpad.dev
pub.dev (Pacotes) pub.dev
Flutter Gallery gallery.flutter.dev

Canais Oficiais

Canal Link
YouTube Flutter youtube.com/@flutterdev
Twitter/X @FlutterDev
Discord Discord Flutter

Linguagem Dart

Recurso Link
Dart Language Tour dart.dev/language
Effective Dart dart.dev/effective-dart
Dart Cheatsheet dart.dev/codelabs/dart-cheatsheet

Atalhos VS Code

Atalho Acao
stless Criar StatelessWidget
stful Criar StatefulWidget
Cmd/Ctrl + . Quick Fix / Refactor
Cmd/Ctrl + Shift + P Command Palette
F5 Iniciar Debug

Checklist da Aula

  • Flutter Doctor sem erros
  • Criar e rodar projeto Flutter
  • Entender StatelessWidget vs StatefulWidget
  • Usar setState() para atualizar UI
  • Criar layouts com Column, Row, Container
  • Navegar entre telas
  • Usar Hot Reload durante desenvolvimento

Proxima Aula: Widgets Basicos e Layout

Flutter Cheatsheet - Aula 02

Widgets Basicos e Layout

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


Indice

  1. StatelessWidget
  2. Widgets de Estrutura
  3. Widgets de Texto e Imagem
  4. Layout com Container
  5. Row e Column
  6. Stack e Positioned
  7. Recursos e Documentacao

StatelessWidget

Template Basico

class MeuWidget extends StatelessWidget {
  final String titulo;  // Propriedades SEMPRE final

  const MeuWidget({required this.titulo});  // const constructor

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(titulo),
    );
  }
}

// Uso
MeuWidget(titulo: 'Ola')
const MeuWidget(titulo: 'Constante')  // Mais eficiente

Documentacao: StatelessWidget

Quando Usar StatelessWidget

  • UI que nao muda apos criacao
  • Componentes puramente visuais
  • Widgets que dependem apenas de parametros

Widgets de Estrutura

MaterialApp

MaterialApp(
  title: 'Meu App',
  debugShowCheckedModeBanner: false,
  theme: ThemeData(
    primarySwatch: Colors.orange,
    brightness: Brightness.light,
  ),
  darkTheme: ThemeData.dark(),
  themeMode: ThemeMode.system,
  home: HomeScreen(),
)

Documentacao: MaterialApp

Scaffold

Scaffold(
  appBar: AppBar(title: Text('Titulo')),
  body: Center(child: Text('Conteudo')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  drawer: Drawer(...),
  bottomNavigationBar: BottomNavigationBar(...),
  backgroundColor: Colors.grey[100],
)

Documentacao: Scaffold

AppBar

AppBar(
  leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
  title: Text('Titulo'),
  centerTitle: true,
  actions: [
    IconButton(icon: Icon(Icons.search), onPressed: () {}),
    IconButton(icon: Icon(Icons.settings), onPressed: () {}),
  ],
  elevation: 0,
  backgroundColor: Colors.orange,
  foregroundColor: Colors.white,
)

Documentacao: AppBar


Widgets de Texto e Imagem

Text

Text(
  'Ola, Mundo!',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

Documentacao: Text

FontWeight

FontWeight.w100  // Thin
FontWeight.w300  // Light
FontWeight.w400  // Normal (regular)
FontWeight.w500  // Medium
FontWeight.w600  // SemiBold
FontWeight.w700  // Bold
FontWeight.w900  // Black

Image

// Imagem da rede
Image.network(
  'https://example.com/image.jpg',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
)

// Imagem local (assets)
Image.asset('assets/images/logo.png')

Documentacao: Image

BoxFit

BoxFit.cover     // Preenche, pode cortar
BoxFit.contain   // Cabe inteira
BoxFit.fill      // Estica
BoxFit.fitWidth  // Ajusta largura
BoxFit.fitHeight // Ajusta altura
BoxFit.scaleDown // Diminui se necessario

Documentacao: BoxFit

CircleAvatar

CircleAvatar(
  radius: 40,
  backgroundImage: NetworkImage('url'),
  backgroundColor: Colors.grey,
  child: Text('AB'),  // Fallback
)

Documentacao: CircleAvatar

Icon

Icon(
  Icons.favorite,
  size: 32,
  color: Colors.red,
)

Documentacao: Icon | Material Icons


Layout com Container

Container Completo

Container(
  width: 200,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(horizontal: 20),
  alignment: Alignment.center,
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    border: Border.all(color: Colors.grey),
    boxShadow: [
      BoxShadow(
        color: Colors.black12,
        blurRadius: 8,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Text('Conteudo'),
)

Documentacao: Container

BoxDecoration Comum

// Card
BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(8),
  boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
)

// Circulo
BoxDecoration(
  shape: BoxShape.circle,
  color: Colors.orange,
)

// Gradiente
BoxDecoration(
  gradient: LinearGradient(
    colors: [Colors.orange, Colors.deepOrange],
  ),
)

Documentacao: BoxDecoration


Row e Column

Column (Vertical)

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisSize: MainAxisSize.min,
  children: [
    Widget1(),
    Widget2(),
    Widget3(),
  ],
)

Documentacao: Column

Row (Horizontal)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Widget1(),
    Widget2(),
    Widget3(),
  ],
)

Documentacao: Row

MainAxisAlignment

MainAxisAlignment.start        // Inicio
MainAxisAlignment.end          // Final
MainAxisAlignment.center       // Centro
MainAxisAlignment.spaceBetween // Espaco entre
MainAxisAlignment.spaceAround  // Espaco ao redor
MainAxisAlignment.spaceEvenly  // Espaco igual

CrossAxisAlignment

CrossAxisAlignment.start    // Inicio
CrossAxisAlignment.end      // Final
CrossAxisAlignment.center   // Centro
CrossAxisAlignment.stretch  // Estica

Documentacao: MainAxisAlignment

Expanded e Flexible

Row(
  children: [
    Container(width: 50),  // Fixo
    Expanded(
      child: Container(),  // Preenche resto
    ),
    Expanded(
      flex: 2,             // 2x mais espaco
      child: Container(),
    ),
  ],
)

Documentacao: Expanded


Stack e Positioned

Stack

Stack(
  alignment: Alignment.center,
  children: [
    // Fundo (primeiro)
    Container(width: 200, height: 200, color: Colors.blue),
    // Topo (ultimo)
    Text('Overlay'),
  ],
)

Documentacao: Stack

Positioned

Stack(
  children: [
    Container(...),
    Positioned(
      top: 10,
      right: 10,
      child: Icon(Icons.star),
    ),
    Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      child: Container(height: 50),
    ),
    Positioned.fill(
      child: Center(child: Text('Centro')),
    ),
  ],
)

Documentacao: Positioned


Espacamento

EdgeInsets

EdgeInsets.all(16)                               // Todos iguais
EdgeInsets.symmetric(horizontal: 20, vertical: 10)
EdgeInsets.only(left: 16, top: 8)                // Especificos
EdgeInsets.fromLTRB(16, 8, 16, 8)                // L, T, R, B
EdgeInsets.zero                                   // Zero

Documentacao: EdgeInsets

SizedBox

SizedBox(height: 16)  // Espaco vertical
SizedBox(width: 8)    // Espaco horizontal

Spacer

Row(
  children: [
    Text('Esquerda'),
    Spacer(),        // Empurra para extremidades
    Text('Direita'),
  ],
)

Documentacao: Spacer


Cores e Alinhamento

Colors

// Predefinidas
Colors.red
Colors.blue
Colors.orange

// Com variacao
Colors.red[100]   // Claro
Colors.red[500]   // Normal
Colors.red[900]   // Escuro

// Customizadas
Color(0xFF42A5F5)              // Hex
Color.fromRGBO(66, 165, 245, 1.0)
Colors.blue.withOpacity(0.5)

Documentacao: Colors

Alignment

Alignment.topLeft
Alignment.topCenter
Alignment.topRight
Alignment.centerLeft
Alignment.center
Alignment.centerRight
Alignment.bottomLeft
Alignment.bottomCenter
Alignment.bottomRight

BorderRadius

BorderRadius.circular(12)
BorderRadius.only(
  topLeft: Radius.circular(12),
  topRight: Radius.circular(12),
)
BorderRadius.vertical(top: Radius.circular(12))
BorderRadius.horizontal(left: Radius.circular(12))

Documentacao: BorderRadius


Recursos e Documentacao

Documentacao Oficial

Recurso Link
Widget Catalog docs.flutter.dev/ui/widgets
Layout Tutorial docs.flutter.dev/ui/layout
Box Constraints docs.flutter.dev/ui/layout/constraints

Widgets Essenciais

Widget Documentacao
Container api.flutter.dev/flutter/widgets/Container
Row api.flutter.dev/flutter/widgets/Row
Column api.flutter.dev/flutter/widgets/Column
Stack api.flutter.dev/flutter/widgets/Stack
ListView api.flutter.dev/flutter/widgets/ListView

Ferramentas

Ferramenta Link
DartPad dartpad.dev
Flutter Gallery gallery.flutter.dev
Material Icons fonts.google.com/icons

Comparacao CSS para Flutter

CSS Flutter
div Container
span Text
img Image
display: flex Row / Column
flex-direction Row vs Column
justify-content mainAxisAlignment
align-items crossAxisAlignment
flex: 1 Expanded
position: absolute Stack + Positioned
padding Padding / Container.padding
margin Container.margin
border-radius BorderRadius
box-shadow BoxShadow

Atalhos VS Code

Atalho Acao
stless StatelessWidget
cont Container
col Column
row Row
txt Text
Cmd/Ctrl + . Wrap with widget

Checklist da Aula

  • Criar StatelessWidget com propriedades final
  • Configurar MaterialApp com tema
  • Montar Scaffold com AppBar e body
  • Estilizar texto com TextStyle
  • Usar imagens (network e asset)
  • Criar layouts com Container e BoxDecoration
  • Organizar com Row e Column
  • Sobrepor elementos com Stack
  • Aplicar padding e margin com EdgeInsets

Proxima Aula: StatefulWidget e Ciclo de Vida

Flutter Cheatsheet - Aula 03

StatefulWidget e Ciclo de Vida

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


Indice

  1. StatefulWidget
  2. setState
  3. Ciclo de Vida
  4. Keys
  5. Padroes Comuns
  6. Recursos e Documentacao

StatefulWidget

Template Basico

class MeuWidget extends StatefulWidget {
  final String titulo;  // Props sao final

  const MeuWidget({required this.titulo});

  @override
  State<MeuWidget> createState() => _MeuWidgetState();
}

class _MeuWidgetState extends State<MeuWidget> {
  int _contador = 0;  // Estado mutavel

  @override
  Widget build(BuildContext context) {
    return Text('${widget.titulo}: $_contador');
    //          ^ Acessa props via widget.
  }
}

Documentacao: StatefulWidget

Diferenca Stateless vs Stateful

Aspecto StatelessWidget StatefulWidget
Estado Sem estado interno Com estado mutavel
Props Apenas final final + vars no State
Rebuild Quando pai reconstroi Quando chama setState()
Lifecycle Apenas build() Ciclo completo
Uso UI estatica UI dinamica

setState

Uso Correto

// Simples
setState(() {
  _count++;
});

// Multiplas variaveis
setState(() {
  _count++;
  _lastUpdate = DateTime.now();
});

// Modificar antes, setState vazio
_count++;
setState(() {});

Com Operacoes Async

// CORRETO - verificar mounted
void _loadData() async {
  final data = await fetchData();
  if (mounted) {  // SEMPRE verificar!
    setState(() {
      _data = data;
    });
  }
}

// ERRADO - async dentro do setState
setState(() async {
  _data = await fetchData();  // NUNCA fazer isso!
});

Documentacao: setState


Ciclo de Vida

Ordem de Execucao

createState() -> initState() -> didChangeDependencies() -> build()
                     |                                        ^
               didUpdateWidget() <----------------------------|
                     |
                deactivate() -> dispose()

Template Completo

class _MeuWidgetState extends State<MeuWidget> {
  late Timer _timer;
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();  // SEMPRE primeiro!
    _controller = ScrollController();
    _timer = Timer.periodic(Duration(seconds: 1), (_) {
      if (mounted) setState(() => _seconds++);
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Seguro usar Theme.of(context) aqui
  }

  @override
  void didUpdateWidget(MeuWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.prop != oldWidget.prop) {
      // Reagir a mudancas nas props
    }
  }

  @override
  void dispose() {
    _timer.cancel();
    _controller.dispose();
    super.dispose();  // SEMPRE ultimo!
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Documentacao: State Lifecycle

Tabela de Metodos

Metodo Quando e chamado Uso comum
initState() State criado Timers, controllers, listeners
didChangeDependencies() Apos initState, InheritedWidget mudou Theme.of, MediaQuery
build() Apos setState, initState, didUpdate Construir UI
didUpdateWidget() Props mudaram Reagir a novas props
dispose() Removido definitivamente Cleanup!

Keys

ValueKey

// Para valores unicos
ListView.builder(
  itemBuilder: (ctx, i) => ListTile(
    key: ValueKey(items[i].id),  // ID unico
    title: Text(items[i].name),
  ),
)

Documentacao: ValueKey

ObjectKey

// Para objetos como identificador
ListTile(
  key: ObjectKey(item),
  title: Text(item.name),
)

GlobalKey

// Acessar state de fora
final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(...),
)

// Usar
_formKey.currentState!.validate();
_formKey.currentState!.save();

Documentacao: GlobalKey

Quando Usar Keys

// SEMPRE em listas dinamicas
items.map((item) => TodoItem(
  key: ValueKey(item.id),
  item: item,
)).toList()

// Em ReorderableListView (obrigatorio)
ReorderableListView(
  children: items.map((item) => ListTile(
    key: ValueKey(item.id),
    title: Text(item.name),
  )).toList(),
)

// NAO usar index como key
key: ValueKey(index)  // Ruim!

Documentacao: When to use Keys


Padroes Comuns

Loading State

class _MyWidgetState extends State<MyWidget> {
  bool _isLoading = true;
  List<Item>? _items;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    try {
      final items = await api.fetch();
      if (mounted) {
        setState(() {
          _items = items;
          _isLoading = false;
        });
      }
    } catch (e) {
      if (mounted) {
        setState(() {
          _error = e.toString();
          _isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return CircularProgressIndicator();
    if (_error != null) return Text('Erro: $_error');
    return ListView(...);
  }
}

Timer Pattern

class _TimerState extends State<TimerWidget> {
  int _seconds = 0;
  Timer? _timer;
  bool _isRunning = false;

  void _start() {
    _timer = Timer.periodic(Duration(seconds: 1), (_) {
      if (mounted) setState(() => _seconds++);
    });
    setState(() => _isRunning = true);
  }

  void _pause() {
    _timer?.cancel();
    setState(() => _isRunning = false);
  }

  void _reset() {
    _timer?.cancel();
    setState(() {
      _seconds = 0;
      _isRunning = false;
    });
  }

  @override
  void dispose() {
    _timer?.cancel();  // IMPORTANTE!
    super.dispose();
  }
}

Documentacao: Timer

Checklist de Dispose

@override
void dispose() {
  // Controllers
  _textController.dispose();
  _scrollController.dispose();
  _animationController.dispose();
  _tabController.dispose();

  // Timers
  _timer?.cancel();

  // Streams
  _subscription?.cancel();
  _streamController?.close();

  // Focus
  _focusNode.dispose();

  super.dispose();
}

Recursos e Documentacao

Documentacao Oficial

Recurso Link
StatefulWidget api.flutter.dev/flutter/widgets/StatefulWidget
State Lifecycle api.flutter.dev/flutter/widgets/State
Keys api.flutter.dev/flutter/foundation/Key
Timer api.dart.dev/stable/dart-async/Timer

Tutoriais

Recurso Link
State Management Intro docs.flutter.dev/data-and-backend/state-mgmt/intro
Lifecycle Cookbook docs.flutter.dev/cookbook

Videos Recomendados

Video Link
Flutter Widget Lifecycle youtube.com/watch?v=FL_U8ORv-2Q
When to Use Keys youtube.com/watch?v=kn0EOS-ZiIc

Comparacao Flutter vs React

Flutter React Hooks
initState() useEffect(() => {}, [])
dispose() Cleanup do useEffect
didUpdateWidget() useEffect(() => {}, [deps])
setState() setX()
widget.prop props.prop
mounted - (hooks cancelam auto)

Atalhos VS Code

Atalho Acao
stful StatefulWidget completo
stless StatelessWidget
Cmd/Ctrl + . Quick fixes
F5 Debug

Checklist da Aula

  • Usar super.initState() primeiro
  • Usar super.dispose() ultimo
  • Verificar mounted antes de setState async
  • Dispose todos os controllers
  • Cancelar todos os timers
  • Usar Keys em listas dinamicas
  • Nao usar index como key
  • Nao fazer async dentro de setState

Proxima Aula: Formularios e Navegacao

Flutter Cheatsheet - Aula 04

Formularios, Navegacao e BuildContext

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


Indice

  1. Formularios
  2. TextEditingController
  3. Validacao
  4. Navegacao
  5. BuildContext
  6. InheritedWidget
  7. Recursos e Documentacao

Formularios

Template Basico

class MyForm extends StatefulWidget {
  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      // Processar dados
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _controller,
            validator: (v) => v!.isEmpty ? 'Obrigatorio' : null,
          ),
          ElevatedButton(
            onPressed: _submit,
            child: Text('Enviar'),
          ),
        ],
      ),
    );
  }
}

Documentacao: Form

GlobalKey

final _formKey = GlobalKey<FormState>();

// Validar todos os campos
bool isValid = _formKey.currentState!.validate();

// Salvar (chama onSaved de cada campo)
_formKey.currentState!.save();

// Resetar formulario
_formKey.currentState!.reset();

Documentacao: FormState

InputDecoration

TextFormField(
  decoration: InputDecoration(
    labelText: 'Nome',
    hintText: 'Digite seu nome',
    prefixIcon: Icon(Icons.person),
    suffixIcon: Icon(Icons.clear),
    border: OutlineInputBorder(),
    filled: true,
    fillColor: Colors.grey[100],
    errorText: 'Mensagem de erro',
  ),
)

Documentacao: InputDecoration


TextEditingController

Uso Basico

final _controller = TextEditingController();

// Usar no campo
TextField(controller: _controller)

// Ler valor
String valor = _controller.text;

// Definir valor
_controller.text = 'Novo valor';

// Limpar
_controller.clear();

// SEMPRE dispose!
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

Documentacao: TextEditingController

Ouvir Mudancas

@override
void initState() {
  super.initState();
  _controller.addListener(() {
    print('Valor: ${_controller.text}');
  });
}

Tipos de Teclado

keyboardType: TextInputType.text,      // Padrao
keyboardType: TextInputType.number,    // Numeros
keyboardType: TextInputType.email,     // Email
keyboardType: TextInputType.phone,     // Telefone
keyboardType: TextInputType.url,       // URL
keyboardType: TextInputType.multiline, // Multiplas linhas

Validacao

Validacoes Comuns

// Campo obrigatorio
validator: (v) => v!.isEmpty ? 'Obrigatorio' : null

// Email
validator: (v) {
  if (v!.isEmpty) return 'Obrigatorio';
  if (!v.contains('@')) return 'Email invalido';
  return null;
}

// Senha (minimo 6)
validator: (v) {
  if (v!.isEmpty) return 'Obrigatorio';
  if (v.length < 6) return 'Minimo 6 caracteres';
  return null;
}

// Numero
validator: (v) {
  if (v!.isEmpty) return 'Obrigatorio';
  if (int.tryParse(v) == null) return 'Numero invalido';
  return null;
}

// Range numerico
validator: (v) {
  final n = int.tryParse(v!);
  if (n == null) return 'Numero invalido';
  if (n < 1 || n > 100) return 'Entre 1 e 100';
  return null;
}

Documentacao: Form Validation

DropdownButtonFormField

DropdownButtonFormField<String>(
  value: _selectedValue,
  decoration: InputDecoration(labelText: 'Opcao'),
  items: ['A', 'B', 'C'].map((item) {
    return DropdownMenuItem(value: item, child: Text(item));
  }).toList(),
  onChanged: (v) => setState(() => _selectedValue = v),
  validator: (v) => v == null ? 'Selecione' : null,
)

Documentacao: DropdownButtonFormField


Navegacao

Navegacao Basica

// Ir para nova tela (push)
Navigator.push(
  context,
  MaterialPageRoute(builder: (ctx) => NovaTela()),
);

// Voltar (pop)
Navigator.pop(context);

// Passar dados
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (ctx) => Detalhes(item: meuItem),
  ),
);

// Retornar dados
Navigator.pop(context, resultado);

// Aguardar resultado
final result = await Navigator.push<Tipo>(...);
if (result != null) {
  // usar result
}

Documentacao: Navigator

Rotas Nomeadas

// Configurar
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (ctx) => HomeScreen(),
    '/profile': (ctx) => ProfileScreen(),
    '/settings': (ctx) => SettingsScreen(),
  },
)

// Navegar
Navigator.pushNamed(context, '/profile');

// Com argumentos
Navigator.pushNamed(
  context,
  '/details',
  arguments: {'id': 123},
);

// Receber argumentos
final args = ModalRoute.of(context)!.settings.arguments as Map;

Documentacao: Named Routes

Navegacao Avancada

// Substituir tela atual
Navigator.pushReplacement(context, route);
Navigator.pushReplacementNamed(context, '/home');

// Remover todas ate condicao
Navigator.pushAndRemoveUntil(
  context,
  route,
  (route) => false,  // Remove todas
);

// Pode voltar?
Navigator.canPop(context)

Documentacao: Navigation Cookbook


BuildContext

Acessando Dados

// Tema
Theme.of(context).primaryColor

// Tamanho da tela
MediaQuery.of(context).size.width
MediaQuery.of(context).size.height

// Navegacao
Navigator.of(context).push(...)

// Snackbar
ScaffoldMessenger.of(context).showSnackBar(...)

Documentacao: BuildContext

Cuidado com Async

// PERIGOSO - context pode estar invalido
void _load() async {
  await fetchData();
  Navigator.of(context).pop();  // Erro!
}

// SEGURO - capturar antes
void _load() async {
  final navigator = Navigator.of(context);
  await fetchData();
  navigator.pop();
}

// SEGURO - verificar mounted
void _load() async {
  await fetchData();
  if (mounted) {
    Navigator.of(context).pop();
  }
}

InheritedWidget

Criar

class AppSettings extends InheritedWidget {
  final bool isDarkMode;
  final VoidCallback toggle;

  const AppSettings({
    required this.isDarkMode,
    required this.toggle,
    required Widget child,
  }) : super(child: child);

  static AppSettings of(BuildContext context) {
    return context
      .dependOnInheritedWidgetOfExactType<AppSettings>()!;
  }

  @override
  bool updateShouldNotify(AppSettings old) {
    return isDarkMode != old.isDarkMode;
  }
}

Documentacao: InheritedWidget

Prover

class AppProvider extends StatefulWidget {
  final Widget child;
  const AppProvider({required this.child});

  @override
  State<AppProvider> createState() => _AppProviderState();
}

class _AppProviderState extends State<AppProvider> {
  bool _isDark = false;

  void _toggle() => setState(() => _isDark = !_isDark);

  @override
  Widget build(BuildContext context) {
    return AppSettings(
      isDarkMode: _isDark,
      toggle: _toggle,
      child: widget.child,
    );
  }
}

Consumir

Widget build(BuildContext context) {
  final settings = AppSettings.of(context);

  return Container(
    color: settings.isDarkMode ? Colors.black : Colors.white,
  );
}

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Forms docs.flutter.dev/cookbook/forms
Form Validation docs.flutter.dev/cookbook/forms/validation
Navigation docs.flutter.dev/ui/navigation
Named Routes docs.flutter.dev/cookbook/navigation/named-routes

API Reference

Widget Link
Form api.flutter.dev/flutter/widgets/Form
TextFormField api.flutter.dev/flutter/material/TextFormField
Navigator api.flutter.dev/flutter/widgets/Navigator
InheritedWidget api.flutter.dev/flutter/widgets/InheritedWidget

Videos Recomendados

Video Link
Forms in Flutter youtube.com/watch?v=2rn3XbBijy4
Navigation youtube.com/watch?v=nyvwx7o277U
InheritedWidget youtube.com/watch?v=1t-8rBCGBYw

Comparacao Flutter vs React

Flutter React
Form + GlobalKey <form> + useRef
TextEditingController useState
validator Custom validation
Navigator.push navigate()
Navigator.pop navigate(-1)
InheritedWidget Context
.of(context) useContext()

Quick Reference

Acao Codigo
Criar form Form(key: _formKey, child: ...)
Validar _formKey.currentState!.validate()
Resetar _formKey.currentState!.reset()
Ler texto _controller.text
Limpar _controller.clear()
Ir para tela Navigator.push(context, route)
Voltar Navigator.pop(context)
Voltar com dado Navigator.pop(context, data)
Rota nomeada Navigator.pushNamed(context, '/route')
Pegar tema Theme.of(context)
Pegar tela MediaQuery.of(context).size

Checklist da Aula

  • Criar Form com GlobalKey
  • Usar TextEditingController
  • Implementar validacoes
  • Fazer dispose dos controllers
  • Navegar entre telas com Navigator
  • Passar e receber dados na navegacao
  • Usar BuildContext corretamente com async
  • Entender InheritedWidget

Proxima Aula: Animacoes e Gerenciamento de Pacotes

Flutter Cheatsheet - Aula 05

Animacoes e Gerenciamento de Pacotes

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


Indice

  1. Animacoes Implicitas
  2. Animacoes Explicitas
  3. Tween e Curves
  4. Transicoes Prontas
  5. Gerenciamento de Pacotes
  6. Recursos e Documentacao

Animacoes Implicitas

AnimatedContainer

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: _expanded ? 200 : 100,
  height: _expanded ? 200 : 100,
  color: _expanded ? Colors.blue : Colors.red,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(_expanded ? 20 : 10),
  ),
  child: Text('Conteudo'),
)

Documentacao: AnimatedContainer

AnimatedOpacity

AnimatedOpacity(
  duration: Duration(milliseconds: 500),
  opacity: _visible ? 1.0 : 0.0,
  child: Container(...),
)

Documentacao: AnimatedOpacity

Outros Widgets Animados

// Alinhamento
AnimatedAlign(
  duration: Duration(milliseconds: 300),
  alignment: _top ? Alignment.topCenter : Alignment.bottomCenter,
  child: Widget(),
)

// Padding
AnimatedPadding(
  duration: Duration(milliseconds: 300),
  padding: EdgeInsets.all(_big ? 50 : 10),
  child: Widget(),
)

// Escala
AnimatedScale(
  duration: Duration(milliseconds: 300),
  scale: _big ? 1.5 : 1.0,
  child: Widget(),
)

// Rotacao (0.5 = 180 graus)
AnimatedRotation(
  duration: Duration(milliseconds: 300),
  turns: _rotated ? 0.5 : 0,
  child: Widget(),
)

// Troca de widget
AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: _showFirst
      ? Text('Primeiro', key: ValueKey(1))
      : Text('Segundo', key: ValueKey(2)),
)

Documentacao: Implicit Animations


Animacoes Explicitas

AnimationController

class _MyState extends State<MyWidget>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,  // Requer o mixin!
    );
  }

  @override
  void dispose() {
    _controller.dispose();  // OBRIGATORIO!
    super.dispose();
  }
}

Documentacao: AnimationController

Metodos do Controller

_controller.forward();              // Iniciar (0 -> 1)
_controller.reverse();              // Reverter (1 -> 0)
_controller.repeat();               // Loop infinito
_controller.repeat(reverse: true);  // Loop ida/volta
_controller.stop();                 // Parar
_controller.reset();                // Voltar ao inicio
_controller.animateTo(0.5);         // Ir para valor

// Status
_controller.value        // 0.0 a 1.0
_controller.isAnimating  // bool
_controller.isCompleted  // bool

AnimatedBuilder

AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Opacity(
      opacity: _controller.value,
      child: child,
    );
  },
  child: ExpensiveWidget(),  // Nao reconstroi
)

Documentacao: AnimatedBuilder


Tween e Curves

Tween Basico

final sizeTween = Tween<double>(
  begin: 100,
  end: 200,
);

// Criar animation
late Animation<double> _sizeAnimation;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    duration: Duration(seconds: 1),
    vsync: this,
  );

  _sizeAnimation = sizeTween.animate(_controller);
}

// Usar
Container(width: _sizeAnimation.value)

Documentacao: Tween

Tipos de Tween

Tween<double>()      // Numeros
ColorTween()         // Cores
SizeTween()          // Tamanhos
RectTween()          // Retangulos
AlignmentTween()     // Alinhamentos
DecorationTween()    // BoxDecoration

Curves

Curve Efeito
Curves.linear Velocidade constante
Curves.easeIn Comeca devagar
Curves.easeOut Termina devagar
Curves.easeInOut Devagar-rapido-devagar
Curves.bounceOut Quica no final
Curves.elasticOut Efeito elastico
Curves.fastOutSlowIn Material Design

Documentacao: Curves

Aplicar Curve

_animation = Tween<double>(
  begin: 0,
  end: 1,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Curves.bounceOut,
  ),
);

Transicoes Prontas

// Fade
FadeTransition(
  opacity: _animation,
  child: Widget(),
)

// Slide
SlideTransition(
  position: _offsetAnimation,  // Animation<Offset>
  child: Widget(),
)

// Scale
ScaleTransition(
  scale: _animation,
  child: Widget(),
)

// Rotation
RotationTransition(
  turns: _animation,
  child: Widget(),
)

// Size
SizeTransition(
  sizeFactor: _animation,
  child: Widget(),
)

Documentacao: Transition Widgets

Staggered Animations

for (int i = 0; i < items.length; i++) {
  final start = i * 0.1;
  final end = start + 0.3;

  _animations[i] = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(
        start,
        end.clamp(0.0, 1.0),
        curve: Curves.easeOut,
      ),
    ),
  );
}

Documentacao: Staggered Animations


Gerenciamento de Pacotes

pub.dev

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  nome_pacote: ^1.2.3

Site: pub.dev

Comandos

flutter pub get        # Instalar
flutter pub upgrade    # Atualizar
flutter pub outdated   # Ver desatualizados
flutter pub add pacote # Adicionar via CLI
flutter pub remove pkg # Remover
flutter pub deps       # Ver arvore

Documentacao: Using packages

Versionamento Semantico

MAJOR.MINOR.PATCH
  2  .  3  .  1

MAJOR = Breaking changes
MINOR = Novas features
PATCH = Bug fixes

Constraints de Versao

# Caret (recomendado)
pacote: ^1.2.3     # >=1.2.3 <2.0.0

# Range explicito
pacote: ">=1.2.0 <2.0.0"

# Versao exata (evitar)
pacote: 1.2.3

# Cuidado com 0.x
^0.2.3  =  >=0.2.3 <0.3.0  # Mais restritivo!

Documentacao: Semantic Versioning


Pacotes de Animacao

flutter_animate

// Instalacao
// flutter pub add flutter_animate

import 'package:flutter_animate/flutter_animate.dart';

Text('Hello')
  .animate()
  .fadeIn(duration: 500.ms)
  .slideX(begin: -0.2)

Pacote: pub.dev/packages/flutter_animate

Encadeamento

Widget()
  .animate()
  .fadeIn(duration: 600.ms)
  .then(delay: 200.ms)
  .slide()
  .scale()

Loop

Icon(Icons.favorite)
  .animate(onPlay: (c) => c.repeat(reverse: true))
  .scale(begin: Offset(1, 1), end: Offset(1.2, 1.2))

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Animations Overview docs.flutter.dev/ui/animations
Implicit Animations docs.flutter.dev/ui/animations/implicit-animations
Explicit Animations docs.flutter.dev/ui/animations/tutorial
Hero Animations docs.flutter.dev/ui/animations/hero-animations

API Reference

Widget Link
AnimatedContainer api.flutter.dev/flutter/widgets/AnimatedContainer
AnimationController api.flutter.dev/flutter/animation/AnimationController
Tween api.flutter.dev/flutter/animation/Tween
Curves api.flutter.dev/flutter/animation/Curves

Pacotes Uteis

Pacote Descricao Link
flutter_animate Animacoes declarativas pub.dev/packages/flutter_animate
animations Transicoes Material pub.dev/packages/animations
lottie Animacoes After Effects pub.dev/packages/lottie
rive Animacoes interativas pub.dev/packages/rive

Videos Recomendados

Video Link
Animations Deep Dive youtube.com/watch?v=GXIJJkq_H8g
Implicit Animations youtube.com/watch?v=IVTjpW3W33s

Quick Reference

Acao Codigo
Fade simples AnimatedOpacity(opacity: x, duration: d)
Container animado AnimatedContainer(duration: d, ...)
Criar controller AnimationController(duration: d, vsync: this)
Iniciar animacao _controller.forward()
Loop _controller.repeat(reverse: true)
Parar _controller.stop()
Valor 0-1 _controller.value
Mapear valor Tween<T>(begin: a, end: b)
Aplicar curve CurvedAnimation(parent: c, curve: Curves.x)
Instalar pacote flutter pub add nome
Atualizar flutter pub upgrade

Checklist Animacao Implicita

AnimatedContainer(
  // 1. Duration (obrigatoria)
  duration: Duration(milliseconds: 300),

  // 2. Curve (opcional)
  curve: Curves.easeInOut,

  // 3. Propriedades para animar
  width: _expanded ? 200 : 100,
  color: _expanded ? Colors.blue : Colors.red,

  // 4. Child
  child: Content(),
)

Checklist Animacao Explicita

// 1. Mixin no State
with SingleTickerProviderStateMixin

// 2. Controller no initState
_controller = AnimationController(
  duration: Duration(seconds: 1),
  vsync: this,
);

// 3. Dispose (OBRIGATORIO)
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

// 4. AnimatedBuilder no build
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) => ...,
  child: MyWidget(),
)

Checklist da Aula

  • Usar AnimatedContainer para animacoes simples
  • Usar AnimatedOpacity para fade effects
  • Criar AnimationController com vsync
  • Usar Tween para mapear valores
  • Aplicar Curves para suavizar animacoes
  • Dispose controllers corretamente
  • Navegar e avaliar pacotes no pub.dev
  • Entender versionamento semantico

Proxima Aula: Gerenciamento de Estado (Provider)

Flutter Cheatsheet - Aula 06

Gerenciamento de Estado (Provider)

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


Indice

  1. ValueNotifier
  2. Provider
  3. ChangeNotifier
  4. Consumer e Selector
  5. MultiProvider e Escopo
  6. Recursos e Documentacao

ValueNotifier

Criar e Usar

// Criar
final counter = ValueNotifier<int>(0);

// Ler valor
print(counter.value);  // 0

// Atualizar (notifica automaticamente)
counter.value = 5;
counter.value++;

// Dispose (importante!)
counter.dispose();

Documentacao: ValueNotifier

ValueListenableBuilder

ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, child) {
    return Text('Valor: $value');
  },
  child: Icon(Icons.star),  // Nao reconstroi
)

Documentacao: ValueListenableBuilder

Classe Customizada

class ThemeNotifier extends ValueNotifier<ThemeMode> {
  ThemeNotifier() : super(ThemeMode.light);

  void toggleTheme() {
    value = value == ThemeMode.light
        ? ThemeMode.dark
        : ThemeMode.light;
  }
}

Provider

Instalacao

# pubspec.yaml
dependencies:
  provider: ^6.1.1
flutter pub add provider

Pacote: pub.dev/packages/provider

Import

import 'package:provider/provider.dart';

ChangeNotifier

Criar Provider

class CounterProvider extends ChangeNotifier {
  int _count = 0;

  // Getter publico
  int get count => _count;

  // Metodos que modificam estado
  void increment() {
    _count++;
    notifyListeners();  // IMPORTANTE!
  }

  void decrement() {
    _count--;
    notifyListeners();
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

Documentacao: ChangeNotifier

Fornecer Provider

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: MyApp(),
    ),
  );
}

Documentacao: ChangeNotifierProvider


Acessar Provider

watch() vs read()

// watch() - Reconstroi quando muda (usar no build)
Widget build(BuildContext context) {
  final counter = context.watch<CounterProvider>();
  return Text('${counter.count}');
}

// read() - Sem rebuild (usar em callbacks)
void _increment() {
  context.read<CounterProvider>().increment();
}

Regra de Ouro

Metodo Onde Usar Rebuild?
watch() No build() Sim
read() Em callbacks Nao

Documentacao: Provider Usage


Consumer e Selector

Consumer

// Basico
Consumer<CounterProvider>(
  builder: (context, provider, child) {
    return Text('${provider.count}');
  },
)

// Com child (otimizado)
Consumer<CounterProvider>(
  builder: (context, provider, child) {
    return Row(
      children: [
        Text('${provider.count}'),
        child!,  // Nao reconstroi
      ],
    );
  },
  child: Icon(Icons.star),
)

Documentacao: Consumer

Consumer2 (dois providers)

Consumer2<ProviderA, ProviderB>(
  builder: (context, providerA, providerB, child) {
    return Text('${providerA.value} - ${providerB.value}');
  },
)

Selector

// So reconstroi quando count muda
Selector<CounterProvider, int>(
  selector: (_, provider) => provider.count,
  builder: (_, count, __) {
    return Text('$count');
  },
)

// Multiplos valores
Selector<Provider, ({int a, String b})>(
  selector: (_, p) => (a: p.valueA, b: p.valueB),
  builder: (_, data, __) {
    return Text('${data.a} - ${data.b}');
  },
)

Documentacao: Selector

context.select()

// Alternativa ao widget Selector
Widget build(BuildContext context) {
  final count = context.select<CounterProvider, int>(
    (provider) => provider.count,
  );
  return Text('$count');
}

MultiProvider e Escopo

MultiProvider

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthProvider()),
        ChangeNotifierProvider(create: (_) => CartProvider()),
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
      ],
      child: MyApp(),
    ),
  );
}

Documentacao: MultiProvider

Escopo Global (todo o app)

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => GlobalProvider(),
      child: MyApp(),
    ),
  );
}

Escopo Local (uma tela)

class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => LocalProvider(),
      child: ScreenContent(),
    );
  }
}

Boas Praticas

Getters Imutaveis

// BOM - Retorna copia imutavel
List<Item> get items => List.unmodifiable(_items);

// RUIM - Expoe lista interna
List<Item> get items => _items;

notifyListeners() no Final

void addMultiple(List<Item> newItems) {
  for (final item in newItems) {
    _items.add(item);
  }
  notifyListeners();  // Uma vez no final
}

Dispose do ValueNotifier

@override
void dispose() {
  _counter.dispose();
  super.dispose();
}

Erros Comuns

read() no build

// ERRADO - nunca atualiza
Widget build(BuildContext context) {
  final count = context.read<Counter>().count;
  return Text('$count');
}

// CERTO
Widget build(BuildContext context) {
  final count = context.watch<Counter>().count;
  return Text('$count');
}

Esquecer notifyListeners()

// ERRADO - UI nao atualiza
void increment() {
  _count++;
}

// CERTO
void increment() {
  _count++;
  notifyListeners();
}

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Provider Package pub.dev/packages/provider
State Management docs.flutter.dev/data-and-backend/state-mgmt
Simple State docs.flutter.dev/data-and-backend/state-mgmt/simple

API Reference

Classe Link
ValueNotifier api.flutter.dev/flutter/foundation/ValueNotifier
ChangeNotifier api.flutter.dev/flutter/foundation/ChangeNotifier
ValueListenableBuilder api.flutter.dev/flutter/widgets/ValueListenableBuilder

Videos Recomendados

Video Link
Provider Explained youtube.com/watch?v=L_QMsE2v6dw
State Management youtube.com/watch?v=3tm-R7ymwhc

Alternativas ao Provider

Pacote Descricao Link
Riverpod Provider mais robusto pub.dev/packages/riverpod
Bloc State management reativo pub.dev/packages/bloc
GetX Solucao completa pub.dev/packages/get

Quick Reference

Acao Codigo
Criar ValueNotifier ValueNotifier<T>(valor)
Atualizar ValueNotifier notifier.value = novo
Criar Provider class X extends ChangeNotifier
Notificar mudanca notifyListeners()
Fornecer provider ChangeNotifierProvider(create: ...)
Ler com rebuild context.watch<T>()
Ler sem rebuild context.read<T>()
Consumer basico Consumer<T>(builder: ...)
Selector otimizado Selector<T, V>(selector: ..., builder: ...)
Multiplos providers MultiProvider(providers: [...])

Quando Usar O Que?

Situacao Solucao
Estado local simples setState()
Estado local reativo ValueNotifier
Estado compartilhado Provider
Multiplos estados MultiProvider
Otimizacao de rebuilds Selector

Fluxo de Dados

User Action (tap, input)
         |
         v
context.read<Provider>().method()
         |
         v
Provider modifica estado
         |
         v
notifyListeners()
         |
         v
Widgets com watch()/Consumer rebuildam
         |
         v
UI atualizada

Checklist da Aula

  • Criar e usar ValueNotifier
  • Usar ValueListenableBuilder corretamente
  • Criar ChangeNotifier para Provider
  • Configurar ChangeNotifierProvider
  • Usar context.watch() e context.read() corretamente
  • Usar Consumer para rebuilds granulares
  • Configurar MultiProvider
  • Usar Selector para otimizacao
  • Entender quando usar cada abordagem

Proxima Aula: Persistencia de Dados

Flutter Cheatsheet - Aula 07

Programacao Assincrona e Consumo de API

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


Indice

  1. Future e async/await
  2. FutureBuilder
  3. Stream
  4. StreamBuilder
  5. HTTP e API
  6. JSON Serialization
  7. Recursos e Documentacao

Future e async/await

Criar Future

// Com Future.delayed
Future<String> buscar() {
  return Future.delayed(
    Duration(seconds: 2),
    () => 'Dados carregados',
  );
}

// Com async
Future<int> calcular() async {
  await Future.delayed(Duration(seconds: 1));
  return 42;
}

// Valor imediato
Future<String> imediato() => Future.value('Pronto');

// Erro imediato
Future<String> erro() => Future.error('Falhou');

Documentacao: Future

async/await

// Funcao assincrona
Future<void> carregarDados() async {
  final dados = await buscarDaAPI();
  print(dados);
}

// Chamar sem esperar
carregarDados();  // Retorna imediatamente

// Chamar e esperar
await carregarDados();  // Espera terminar

Tratamento de Erros

Future<void> carregar() async {
  try {
    final dados = await buscarDaAPI();
    exibir(dados);
  } catch (e) {
    print('Erro: $e');
  } finally {
    esconderLoading();
  }
}

// Com tipos especificos
try {
  // ...
} on FormatException catch (e) {
  print('Formato invalido');
} on HttpException catch (e) {
  print('Erro HTTP');
} catch (e) {
  print('Erro generico');
}

Execucao Paralela

// Future.wait - espera todos
final resultados = await Future.wait([
  buscarA(),
  buscarB(),
  buscarC(),
]);
// resultados[0], resultados[1], resultados[2]

// Future.any - primeiro que completar
final primeiro = await Future.any([
  buscarA(),
  buscarB(),
]);

Documentacao: async-await


FutureBuilder

Estrutura Basica

FutureBuilder<TipoDados>(
  future: meuFuture,
  builder: (context, snapshot) {
    // Retorna widget baseado no estado
    return Widget;
  },
)

Documentacao: FutureBuilder

ConnectionState

Estado Descricao
none Nenhum Future
waiting Aguardando
active Stream ativo
done Completou

Exemplo Completo

late Future<List<Exercise>> _future;

@override
void initState() {
  super.initState();
  _future = _loadData();  // Criar AQUI, nao no build!
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<List<Exercise>>(
    future: _future,
    builder: (context, snapshot) {
      // Loading
      if (snapshot.connectionState == ConnectionState.waiting) {
        return Center(child: CircularProgressIndicator());
      }

      // Erro
      if (snapshot.hasError) {
        return Center(child: Text('Erro: ${snapshot.error}'));
      }

      // Sucesso
      if (snapshot.hasData) {
        final data = snapshot.data!;
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (context, i) => ListTile(title: Text(data[i].name)),
        );
      }

      // Sem dados
      return Center(child: Text('Sem dados'));
    },
  );
}

Pull-to-Refresh

void _refresh() {
  setState(() {
    _future = _loadData();
  });
}

RefreshIndicator(
  onRefresh: () async {
    _refresh();
    await _future;
  },
  child: FutureBuilder(...),
)

Stream

Criar Stream

// Stream.periodic - valores em intervalo
Stream<int> contador() {
  return Stream.periodic(
    Duration(seconds: 1),
    (count) => count,  // 0, 1, 2, 3...
  );
}

// Stream.fromIterable - de lista
Stream<String> fromList() {
  return Stream.fromIterable(['a', 'b', 'c']);
}

// async* e yield - gerador
Stream<int> contarAte(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

Documentacao: Stream

StreamController

// Criar controller
final _controller = StreamController<int>();

// Broadcast (multiplos listeners)
final _controller = StreamController<int>.broadcast();

// Acessar stream
Stream<int> get stream => _controller.stream;

// Emitir valor
_controller.add(42);

// Emitir erro
_controller.addError('Falhou');

// Fechar (IMPORTANTE!)
_controller.close();

Ouvir Stream

// Com listen
final subscription = stream.listen(
  (value) => print('Valor: $value'),
  onError: (e) => print('Erro: $e'),
  onDone: () => print('Fim'),
);

// Cancelar
subscription.cancel();

// Com await for
await for (final valor in stream) {
  print(valor);
}

StreamBuilder

Estrutura Basica

StreamBuilder<int>(
  stream: meuStream,
  initialData: 0,  // Valor inicial (opcional)
  builder: (context, snapshot) {
    return Widget;
  },
)

Documentacao: StreamBuilder

Exemplo: Timer

class TimerWidget extends StatefulWidget {
  @override
  State<TimerWidget> createState() => _TimerWidgetState();
}

class _TimerWidgetState extends State<TimerWidget> {
  final _controller = StreamController<int>.broadcast();
  Timer? _timer;
  int _seconds = 60;

  void _start() {
    _timer = Timer.periodic(Duration(seconds: 1), (_) {
      if (_seconds > 0) {
        _seconds--;
        _controller.add(_seconds);
      } else {
        _stop();
      }
    });
  }

  void _stop() {
    _timer?.cancel();
  }

  @override
  void dispose() {
    _timer?.cancel();
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _controller.stream,
      initialData: _seconds,
      builder: (context, snapshot) {
        final secs = snapshot.data ?? 0;
        return Text(
          '$secs',
          style: TextStyle(
            fontSize: 48,
            color: secs <= 10 ? Colors.red : Colors.black,
          ),
        );
      },
    );
  }
}

HTTP e API

Instalacao

# pubspec.yaml
dependencies:
  http: ^1.1.0
flutter pub add http

Pacote: pub.dev/packages/http

Import

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

GET Request

Future<List<Exercise>> getExercises() async {
  final response = await http.get(
    Uri.parse('https://api.exemplo.com/exercises'),
    headers: {'Content-Type': 'application/json'},
  );

  if (response.statusCode == 200) {
    final List<dynamic> data = json.decode(response.body);
    return data.map((j) => Exercise.fromJson(j)).toList();
  } else {
    throw Exception('Erro: ${response.statusCode}');
  }
}

POST Request

Future<Exercise> createExercise(Exercise ex) async {
  final response = await http.post(
    Uri.parse('https://api.exemplo.com/exercises'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode(ex.toJson()),
  );

  if (response.statusCode == 201) {
    return Exercise.fromJson(json.decode(response.body));
  }
  throw Exception('Erro ao criar');
}

PUT Request

Future<Exercise> updateExercise(Exercise ex) async {
  final response = await http.put(
    Uri.parse('https://api.exemplo.com/exercises/${ex.id}'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode(ex.toJson()),
  );

  if (response.statusCode == 200) {
    return Exercise.fromJson(json.decode(response.body));
  }
  throw Exception('Erro ao atualizar');
}

DELETE Request

Future<void> deleteExercise(String id) async {
  final response = await http.delete(
    Uri.parse('https://api.exemplo.com/exercises/$id'),
  );

  if (response.statusCode != 200 && response.statusCode != 204) {
    throw Exception('Erro ao deletar');
  }
}

Timeout

final response = await http
    .get(Uri.parse(url))
    .timeout(Duration(seconds: 10));

JSON Serialization

Modelo

class Exercise {
  final String id;
  final String name;
  final int sets;
  final int reps;

  Exercise({
    required this.id,
    required this.name,
    required this.sets,
    required this.reps,
  });

  // JSON -> Objeto
  factory Exercise.fromJson(Map<String, dynamic> json) {
    return Exercise(
      id: json['id'] as String,
      name: json['name'] as String,
      sets: json['sets'] as int,
      reps: json['reps'] as int,
    );
  }

  // Objeto -> JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'sets': sets,
      'reps': reps,
    };
  }
}

Documentacao: JSON Serialization

Parse de Lista

// String JSON -> Lista de objetos
List<Exercise> parseExercises(String responseBody) {
  final List<dynamic> data = json.decode(responseBody);
  return data.map((json) => Exercise.fromJson(json)).toList();
}

// Lista de objetos -> String JSON
String exercisesToJson(List<Exercise> exercises) {
  return json.encode(exercises.map((e) => e.toJson()).toList());
}

Padrao de Estados

Enum de Status

enum AsyncStatus { initial, loading, success, error }

Implementacao

class _MyScreenState extends State<MyScreen> {
  AsyncStatus _status = AsyncStatus.initial;
  List<Exercise> _data = [];
  String _error = '';

  Future<void> _load() async {
    setState(() => _status = AsyncStatus.loading);

    try {
      _data = await api.getExercises();
      setState(() => _status = AsyncStatus.success);
    } catch (e) {
      _error = e.toString();
      setState(() => _status = AsyncStatus.error);
    }
  }

  @override
  Widget build(BuildContext context) {
    switch (_status) {
      case AsyncStatus.initial:
        return ElevatedButton(onPressed: _load, child: Text('Carregar'));
      case AsyncStatus.loading:
        return CircularProgressIndicator();
      case AsyncStatus.success:
        return ListView.builder(...);
      case AsyncStatus.error:
        return Column(children: [
          Text(_error),
          ElevatedButton(onPressed: _load, child: Text('Retry')),
        ]);
    }
  }
}

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Async Programming dart.dev/codelabs/async-await
FutureBuilder api.flutter.dev/.../FutureBuilder
StreamBuilder api.flutter.dev/.../StreamBuilder
JSON Serialization docs.flutter.dev/.../json

Pacotes

Pacote Descricao Link
http Cliente HTTP pub.dev/packages/http
dio Cliente HTTP avancado pub.dev/packages/dio
json_serializable Geracao de codigo JSON pub.dev/packages/json_serializable

Quick Reference

Acao Codigo
Criar Future Future<T> func() async { }
Esperar Future await meuFuture
Delay Future.delayed(Duration(seconds: 1))
Paralelo Future.wait([f1, f2])
Try/catch async try { await x } catch (e) { }
FutureBuilder FutureBuilder<T>(future: f, builder: ...)
Verificar loading snapshot.connectionState == ConnectionState.waiting
Verificar erro snapshot.hasError
Verificar dados snapshot.hasData
Acessar dados snapshot.data!
Criar Stream Stream.periodic(duration, (i) => i)
StreamController StreamController<T>.broadcast()
Emitir valor controller.add(valor)
Fechar stream controller.close()
StreamBuilder StreamBuilder<T>(stream: s, builder: ...)
GET HTTP http.get(Uri.parse(url))
POST HTTP http.post(uri, body: json.encode(data))
Parse JSON json.decode(response.body)
Encode JSON json.encode(objeto.toJson())

Erros Comuns

Future no build()

// ERRADO - cria novo Future a cada rebuild
Widget build(BuildContext context) {
  return FutureBuilder(
    future: _loadData(),  // PROBLEMA!
    builder: ...
  );
}

// CERTO - Future criado no initState
late Future<Data> _future;

void initState() {
  _future = _loadData();
}

Esquecer de fechar StreamController

// ERRADO - memory leak
class _MyState extends State<MyWidget> {
  final _controller = StreamController<int>();
}

// CERTO
@override
void dispose() {
  _controller.close();  // IMPORTANTE!
  super.dispose();
}

Nao tratar erros

// ERRADO - app quebra
final data = await api.getExercises();

// CERTO
try {
  final data = await api.getExercises();
} catch (e) {
  // Tratar erro
}

Checklist da Aula

  • Criar Future com async/await
  • Tratar erros com try/catch
  • Usar FutureBuilder corretamente
  • Criar Future no initState, nao no build
  • Verificar ConnectionState
  • Criar e usar StreamController
  • Usar StreamBuilder para dados em tempo real
  • Fechar StreamController no dispose
  • Fazer requisicoes HTTP com o pacote http
  • Parse de JSON para objetos
  • Implementar estados loading/error/success

Flutter Cheatsheet - Aula 08

Comunicacao Nativa (MethodChannel)

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


Indice

  1. MethodChannel (Dart)
  2. Android (Kotlin)
  3. iOS (Swift)
  4. Tipos Suportados
  5. Dados Complexos
  6. Boas Praticas
  7. Recursos e Documentacao

MethodChannel (Dart)

Criar Canal

import 'package:flutter/services.dart';

// Nome unico com dominio reverso
const platform = MethodChannel('com.fittracker.app/native');

Documentacao: MethodChannel

Chamar Metodo Nativo

// Sem argumentos
final result = await platform.invokeMethod('getBatteryLevel');

// Com argumentos
final result = await platform.invokeMethod('saveData', {
  'key': 'username',
  'value': 'Joao',
});

// Com tipo generico (recomendado)
final int? result = await platform.invokeMethod<int>('getBatteryLevel');

Tratar Erros

try {
  final result = await platform.invokeMethod('metodo');
} on PlatformException catch (e) {
  print('Codigo: ${e.code}');
  print('Mensagem: ${e.message}');
  print('Detalhes: ${e.details}');
} on MissingPluginException {
  print('Metodo nao implementado');
}

Classe Wrapper

class NativeBridge {
  static const _channel = MethodChannel('com.fittracker.app/native');

  static Future<int> getBatteryLevel() async {
    return await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
  }

  static Future<Map<String, dynamic>> getDeviceInfo() async {
    final result = await _channel.invokeMethod('getDeviceInfo');
    return Map<String, dynamic>.from(result as Map);
  }

  static Future<void> vibrate({int duration = 500}) async {
    await _channel.invokeMethod('vibrate', {'duration': duration});
  }
}

Documentacao: Platform Channels


Android (Kotlin)

Estrutura Basica

// Em android/app/src/main/kotlin/.../MainActivity.kt

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.fittracker.app/native"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getBatteryLevel" -> getBatteryLevel(result)
                "getDeviceInfo" -> getDeviceInfo(result)
                else -> result.notImplemented()
            }
        }
    }
}

Nivel de Bateria

private fun getBatteryLevel(result: MethodChannel.Result) {
    val batteryManager = getSystemService(
        Context.BATTERY_SERVICE
    ) as BatteryManager

    val level = batteryManager.getIntProperty(
        BatteryManager.BATTERY_PROPERTY_CAPACITY
    )

    if (level != -1) {
        result.success(level)
    } else {
        result.error("UNAVAILABLE", "Bateria indisponivel", null)
    }
}

Info do Dispositivo

private fun getDeviceInfo(result: MethodChannel.Result) {
    val info = HashMap<String, Any>()
    info["brand"] = Build.BRAND
    info["model"] = Build.MODEL
    info["osVersion"] = Build.VERSION.RELEASE
    info["sdkVersion"] = Build.VERSION.SDK_INT
    info["platform"] = "Android"

    result.success(info)
}

Receber Argumentos

// Flutter envia: invokeMethod('metodo', {'key': 'value'})
val args = call.arguments as? HashMap<*, *>
val key = call.argument<String>("key") ?: "default"
val value = call.argument<Int>("value") ?: 0

Resultados

Metodo Descricao
result.success(valor) Sucesso
result.error(code, msg, details) Erro
result.notImplemented() Nao implementado

iOS (Swift)

Estrutura Basica

// Em ios/Runner/AppDelegate.swift

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController
            as! FlutterViewController

        let channel = FlutterMethodChannel(
            name: "com.fittracker.app/native",
            binaryMessenger: controller.binaryMessenger
        )

        channel.setMethodCallHandler { (call, result) in
            switch call.method {
            case "getBatteryLevel":
                self.getBatteryLevel(result: result)
            case "getDeviceInfo":
                self.getDeviceInfo(result: result)
            default:
                result(FlutterMethodNotImplemented)
            }
        }

        GeneratedPluginRegistrant.register(with: self)
        return super.application(application,
            didFinishLaunchingWithOptions: launchOptions)
    }
}

Nivel de Bateria

private func getBatteryLevel(result: FlutterResult) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    let level = UIDevice.current.batteryLevel

    if level < 0 {
        result(FlutterError(
            code: "UNAVAILABLE",
            message: "Bateria indisponivel",
            details: nil
        ))
    } else {
        result(Int(level * 100))
    }
}

Info do Dispositivo

private func getDeviceInfo(result: FlutterResult) {
    let device = UIDevice.current
    let info: [String: Any] = [
        "brand": "Apple",
        "model": device.model,
        "name": device.name,
        "osVersion": device.systemVersion,
        "platform": "iOS",
    ]
    result(info)
}

Receber Argumentos

// Flutter envia: invokeMethod('metodo', {'key': 'value'})
let args = call.arguments as? [String: Any]
let key = args?["key"] as? String ?? "default"
let value = args?["value"] as? Int ?? 0

Resultados

Chamada Descricao
result(valor) Sucesso
result(FlutterError(...)) Erro
result(FlutterMethodNotImplemented) Nao implementado

Tipos Suportados

Mapeamento de Tipos

Dart Kotlin/Java Swift/ObjC
null null nil
bool Boolean Bool
int Int / Long Int
double Double Double
String String String
Uint8List byte[] FlutterStandardTypedData
List ArrayList NSArray
Map HashMap NSDictionary

Casting Seguro no Dart

// int
final int? level = await platform.invokeMethod<int>('getLevel');
final safeLevel = level ?? -1;

// String
final String? name = await platform.invokeMethod<String>('getName');
final safeName = name ?? 'unknown';

// Map (SEMPRE converter)
final result = await platform.invokeMethod('getInfo');
final map = Map<String, dynamic>.from(result as Map);

// List de Maps
final result = await platform.invokeMethod('getList');
final list = (result as List)
    .map((item) => Map<String, dynamic>.from(item as Map))
    .toList();

Dados Complexos

Enviar Map com Lista

// Flutter -> Nativo
await platform.invokeMethod('saveWorkout', {
  'name': 'Treino A',
  'exercises': [
    {'name': 'Supino', 'sets': 4, 'reps': 12},
    {'name': 'Agachamento', 'sets': 4, 'reps': 10},
  ],
  'completed': true,
});

Receber Map Complexo

// Nativo -> Flutter
final result = await platform.invokeMethod('getWorkout');
final workout = Map<String, dynamic>.from(result as Map);
final exercises = (workout['exercises'] as List)
    .map((e) => Map<String, dynamic>.from(e as Map))
    .toList();

No Android (Kotlin)

// Receber dados complexos
val workout = call.arguments as? HashMap<*, *>
val name = workout?.get("name") as? String
val exercises = workout?.get("exercises") as? ArrayList<*>

// Retornar dados complexos
val result = HashMap<String, Any>()
result["total"] = 42
result["items"] = arrayListOf(
    hashMapOf("name" to "A", "value" to 1),
    hashMapOf("name" to "B", "value" to 2),
)
result.success(result)

No iOS (Swift)

// Receber dados complexos
let workout = call.arguments as? [String: Any]
let name = workout?["name"] as? String
let exercises = workout?["exercises"] as? [[String: Any]]

// Retornar dados complexos
let resultData: [String: Any] = [
    "total": 42,
    "items": [
        ["name": "A", "value": 1],
        ["name": "B", "value": 2],
    ]
]
result(resultData)

Boas Praticas

Nomes de Canal

// BOM - dominio reverso, descritivo
MethodChannel('com.fittracker.app/native')
MethodChannel('com.fittracker.app/battery')
MethodChannel('com.fittracker.app/device')

// RUIM - generico
MethodChannel('native')
MethodChannel('channel1')

Encapsular em Classe

// BOM - classe wrapper
class NativeBridge {
  static const _channel = MethodChannel('com.fittracker.app/native');
  static Future<int> getBattery() async { ... }
}

// RUIM - canal exposto diretamente
const platform = MethodChannel('com.fittracker.app/native');
// usado direto em varios widgets...

Fallback Gracioso

// BOM - fallback se nativo falhar
static Future<int> getBatterySafe() async {
  try {
    return await getBattery();
  } catch (_) {
    return -1;
  }
}

Repository Pattern

abstract class DeviceRepository {
  Future<int> getBatteryLevel();
}

class NativeDeviceRepository implements DeviceRepository {
  @override
  Future<int> getBatteryLevel() async {
    return await _channel.invokeMethod<int>('getBattery') ?? -1;
  }
}

class MockDeviceRepository implements DeviceRepository {
  @override
  Future<int> getBatteryLevel() async => 85;
}

EventChannel (Bonus)

Stream de Dados Nativos

// Dart - receber stream do nativo
const eventChannel = EventChannel('com.fittracker.app/events');

Stream<int> get batteryStream {
  return eventChannel
      .receiveBroadcastStream()
      .map((event) => event as int);
}

// Na UI
StreamBuilder<int>(
  stream: batteryStream,
  builder: (context, snapshot) {
    return Text('${snapshot.data}%');
  },
)

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Platform Channels docs.flutter.dev/.../platform-channels
MethodChannel api.flutter.dev/.../MethodChannel
EventChannel api.flutter.dev/.../EventChannel
PlatformException api.flutter.dev/.../PlatformException

Pacotes Alternativos

Pacote Descricao Link
pigeon Geracao de codigo type-safe pub.dev/packages/pigeon
flutter_ffi Foreign Function Interface dart.dev/guides/libraries/c-interop
battery_plus Bateria (pronto) pub.dev/packages/battery_plus
device_info_plus Info dispositivo (pronto) pub.dev/packages/device_info_plus
connectivity_plus Conectividade (pronto) pub.dev/packages/connectivity_plus

Quick Reference

Acao Codigo
Criar canal (Dart) MethodChannel('com.app/name')
Chamar metodo await platform.invokeMethod('metodo')
Chamar com args await platform.invokeMethod('m', {'k': 'v'})
Tratar erro on PlatformException catch (e) { }
Converter Map Map<String, dynamic>.from(result as Map)
Handler Android MethodChannel(...).setMethodCallHandler { call, result -> }
Rotear Android when (call.method) { "m" -> ... }
Sucesso Android result.success(valor)
Erro Android result.error("CODE", "msg", null)
Handler iOS channel.setMethodCallHandler { (call, result) in }
Rotear iOS switch call.method { case "m": ... }
Sucesso iOS result(valor)
Erro iOS result(FlutterError(code:message:details:))
Nao implementado result.notImplemented() / result(FlutterMethodNotImplemented)

Erros Comuns

Nome do canal diferente

// ERRADO - nomes diferentes!
// Dart:
MethodChannel('com.app/native')
// Kotlin:
val CHANNEL = "com.app/nativo"  // DIFERENTE!

// CERTO - mesmo nome em ambos
// Dart e Kotlin usam: "com.app/native"

Nao converter Map

// ERRADO - Map<dynamic, dynamic>
final result = await platform.invokeMethod('getInfo');
final brand = result['brand']; // Pode falhar!

// CERTO - converter explicitamente
final result = await platform.invokeMethod('getInfo');
final map = Map<String, dynamic>.from(result as Map);
final brand = map['brand'] as String;

Esquecer de tratar erros

// ERRADO - app quebra se nativo falhar
final level = await platform.invokeMethod('getBattery');

// CERTO
try {
  final level = await platform.invokeMethod('getBattery');
} on PlatformException catch (e) {
  // Tratar erro
}

Nao chamar super.configureFlutterEngine

// ERRADO - plugins nao funcionam
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    // Esqueceu super!
    MethodChannel(...).setMethodCallHandler { ... }
}

// CERTO
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine) // IMPORTANTE!
    MethodChannel(...).setMethodCallHandler { ... }
}

Checklist da Aula

  • Entender quando usar comunicacao nativa
  • Criar MethodChannel com nome unico
  • Chamar invokeMethod com e sem argumentos
  • Tratar PlatformException e MissingPluginException
  • Implementar handler no Android (Kotlin)
  • Implementar handler no iOS (Swift)
  • Retornar sucesso, erro e nao implementado
  • Enviar e receber dados complexos (Map, List)
  • Fazer casting seguro de tipos
  • Encapsular comunicacao em classe wrapper
  • Usar FutureBuilder para exibir dados nativos

Flutter Cheatsheet - Aula 09

Pacotes, Plugins e Modularizacao (Flutter Modular)

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


Indice

  1. Criando Pacotes
  2. Instalando Pacotes
  3. Pacotes vs Plugins
  4. Flutter Modular - Setup
  5. Flutter Modular - Rotas
  6. Flutter Modular - Injecao de Dependencias
  7. Flutter Modular - Navegacao
  8. Boas Praticas
  9. Recursos e Documentacao

Criando Pacotes

Criar Pacote

# Pacote Dart puro
flutter create --template=package meu_pacote

# Plugin (Dart + nativo)
flutter create --template=plugin --platforms=android,ios meu_plugin

Documentacao: Developing Packages

Estrutura do Pacote

meu_pacote/
├── lib/
│   ├── meu_pacote.dart    # API publica (exports)
│   └── src/               # Codigo interno
├── test/                   # Testes
├── pubspec.yaml           # Metadados
├── README.md              # Documentacao
├── CHANGELOG.md           # Historico
└── LICENSE                # Licenca

Arquivo Principal (Exports)

/// Meu pacote reutilizavel.
library meu_pacote;

export 'src/models/exercise.dart';
export 'src/services/exercise_service.dart';
// NAO exporte implementacoes internas

pubspec.yaml do Pacote

name: fittracker_core
description: Modelos e servicos do FitTracker.
version: 1.0.0

environment:
  sdk: ^3.10.3

dependencies:
  http: ^1.2.0

Instalando Pacotes

Via CLI

# Adicionar dependencia
flutter pub add http

# Adicionar dev dependency
flutter pub add --dev flutter_lints

# Remover dependencia
flutter pub remove http

# Atualizar dependencias
flutter pub upgrade

Via pubspec.yaml

dependencies:
  # Do pub.dev
  http: ^1.2.0

  # Pacote local
  fittracker_core:
    path: ../fittracker_core

  # Pacote Git
  meu_pacote:
    git:
      url: https://github.com/user/repo.git
      ref: v1.0.0

  # Hosted privado
  pacote_corp:
    hosted:
      name: pacote_corp
      url: https://packages.empresa.com
    version: ^1.0.0

Versionamento Semantico

MAJOR.MINOR.PATCH  ->  1.2.3

^1.2.3  =  >=1.2.3 e <2.0.0  (recomendado)
Operador Significado
^1.2.3 >=1.2.3, <2.0.0
>=1.0.0 <2.0.0 Range explicito
any Qualquer (nao recomendado)

Documentacao: Using Packages


Pacotes vs Plugins

Pacote Plugin
Codigo Apenas Dart Dart + Nativo
Template --template=package --template=plugin
Exemplo provider, http camera, battery_plus
Uso Logica, modelos, UI APIs nativas
Precisa de codigo nativo? ── Sim ──> Plugin
         │
         Nao ──> Pacote

Flutter Modular - Setup

Instalar

flutter pub add flutter_modular

main.dart

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

void main() {
  runApp(
    ModularApp(
      module: AppModule(),
      child: AppWidget(),
    ),
  );
}

Documentacao: Flutter Modular

AppWidget

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'FitTracker',
      theme: ThemeData(
        primarySwatch: Colors.orange,
        useMaterial3: true,
      ),
      routerConfig: Modular.routerConfig,
    );
  }
}

AppModule

class AppModule extends Module {
  @override
  void binds(Injector i) {
    i.addSingleton(ExerciseService.new);
  }

  @override
  void routes(RouteManager r) {
    r.child('/', child: (context) => HomePage());
    r.module('/exercises', module: ExerciseModule());
  }
}

Flutter Modular - Rotas

Tipos de Rotas

class AppModule extends Module {
  @override
  void routes(RouteManager r) {
    // Widget direto
    r.child('/', child: (context) => HomePage());

    // Sub-modulo
    r.module('/exercises', module: ExerciseModule());

    // Redirecionamento
    r.redirect('/home', to: '/');

    // Rota 404 (fallback)
    r.wildcard(child: (context) => NotFoundPage());
  }
}

Parametros Dinamicos

// Definir rota com parametro
r.child('/:id', child: (context) => DetailPage(
  id: r.args.params['id'] ?? '',
));

// Navegar
Modular.to.pushNamed('/exercises/123');

Query Strings

// Definir rota
r.child('/search', child: (context) => SearchPage(
  query: r.args.queryParams['q'] ?? '',
));

// Navegar
Modular.to.pushNamed('/exercises/search?q=supino');

Modulo Filho

class ExerciseModule extends Module {
  @override
  void binds(Injector i) {
    i.add(ExerciseController.new);
  }

  @override
  void routes(RouteManager r) {
    r.child('/', child: (context) => ExerciseListPage());
    r.child('/:id', child: (context) => ExerciseDetailPage(
      id: r.args.params['id'] ?? '',
    ));
  }
}

Flutter Modular - Injecao de Dependencias

Tipos de Registro

@override
void binds(Injector i) {
  // Factory - nova instancia a cada chamada
  i.add(MyController.new);

  // Singleton - instancia unica (imediata)
  i.addSingleton(MyService.new);

  // Lazy Singleton - instancia unica (no primeiro uso)
  i.addLazySingleton(MyRepository.new);

  // Instancia existente
  i.addInstance(ApiClient(baseUrl: 'https://api.com'));
}
Metodo Quando Cria Instancias
i.add Cada chamada Multiplas
i.addSingleton Na inicializacao Uma
i.addLazySingleton Primeiro uso Uma
i.addInstance Ja criada Uma

Consumir Dependencias

// Obter instancia (lanca erro se nao existir)
final service = Modular.get<ExerciseService>();

// Obter nullable
final service = Modular.tryGet<ExerciseService>();

// Com valor padrao
final service = Modular.get<ExerciseService>(
  defaultValue: ExerciseService(),
);

Injecao Automatica no Construtor

class ExerciseController {
  final ExerciseService service;

  ExerciseController(this.service); // Injetado automaticamente!
}

// No modulo
i.addSingleton(ExerciseService.new);
i.add(ExerciseController.new); // service resolvido automaticamente

Dispose

// Via BindConfig
i.addSingleton<MyBloc>(
  MyBloc.new,
  config: BindConfig(onDispose: (bloc) => bloc.close()),
);

// Via interface Disposable
class MyController implements Disposable {
  @override
  void dispose() { /* limpar recursos */ }
}

// Manual
Modular.dispose<MyController>();

Flutter Modular - Navegacao

Navegar

// Push nomeado
Modular.to.pushNamed('/exercises');

// Com argumentos
Modular.to.pushNamed('/detail', arguments: myObject);

// Voltar
Modular.to.pop();

// Substituir rota
Modular.to.pushReplacementNamed('/home');

// Navegar limpando historico
Modular.to.navigate('/login');

Receber Argumentos

// Na rota de destino
final args = Modular.args;
final id = args.params['id'];          // parametro da URL
final query = args.queryParams['q'];    // query string
final data = args.data;                 // arguments passados

Boas Praticas

Pacotes

// BOM - API publica explicita
library fittracker_core;
export 'src/models/exercise.dart';

// RUIM - Expor tudo
export 'src/internal_helper.dart'; // Codigo interno!

Modular

// BOM - Dependencia no escopo correto
class ExerciseModule extends Module {
  @override
  void binds(Injector i) {
    i.addLazySingleton(ExerciseService.new); // Apenas neste modulo
  }
}

// RUIM - Tudo no AppModule
class AppModule extends Module {
  @override
  void binds(Injector i) {
    i.addSingleton(ExerciseService.new);
    i.addSingleton(TimerService.new);
    i.addSingleton(AuthService.new);
    // 30+ servicos no modulo raiz...
  }
}

Organizacao de Modulos

// BOM - Um modulo por feature
r.module('/auth', module: AuthModule());
r.module('/exercises', module: ExerciseModule());
r.module('/timer', module: TimerModule());

// RUIM - Tudo misturado em um modulo
r.child('/login', child: (_) => LoginPage());
r.child('/exercises', child: (_) => ExerciseListPage());
r.child('/timer', child: (_) => TimerPage());
// 50+ child routes...

Erros Comuns

Esqueceu MaterialApp.router

// ERRADO - MaterialApp normal
return MaterialApp(
  home: HomePage(),
);

// CERTO - MaterialApp.router para Modular
return MaterialApp.router(
  routerConfig: Modular.routerConfig,
);

Acessar dependencia nao registrada

// ERRADO - Servico nao registrado no modulo
final service = Modular.get<MyService>(); // ERRO!

// CERTO - Usar tryGet para seguranca
final service = Modular.tryGet<MyService>();
if (service == null) {
  // Tratar ausencia
}

Nome do pacote com hifen

// ERRADO no pubspec.yaml
name: meu-pacote  // Hifen nao permitido!

// CERTO
name: meu_pacote  // Usar underscore

Pacote nao encontrado apos criar

# Esqueceu de rodar:
flutter pub get

Quick Reference

Acao Comando/Codigo
Criar pacote flutter create --template=package nome
Criar plugin flutter create --template=plugin nome
Adicionar pacote flutter pub add nome
Remover pacote flutter pub remove nome
Atualizar deps flutter pub upgrade
Publicar pacote dart pub publish
Verificar publicacao dart pub publish --dry-run
Setup Modular ModularApp(module: AppModule(), child: AppWidget())
Router Modular MaterialApp.router(routerConfig: Modular.routerConfig)
Rota child r.child('/', child: (_) => Page())
Rota module r.module('/path', module: MyModule())
Navegar Modular.to.pushNamed('/path')
Voltar Modular.to.pop()
Obter dep Modular.get<MyService>()
Factory i.add(MyClass.new)
Singleton i.addSingleton(MyClass.new)
Lazy Singleton i.addLazySingleton(MyClass.new)
Dispose Modular.dispose<MyClass>()

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Developing Packages docs.flutter.dev/.../developing-packages
Using Packages docs.flutter.dev/.../using-packages
Package Layout dart.dev/.../package-layout
pub.dev pub.dev
Flutter Modular modular.flutterando.com.br
Semantic Versioning semver.org

Pacotes Uteis

Pacote Descricao Link
flutter_modular Rotas e DI modularizadas pub.dev/packages/flutter_modular
provider Gerenciamento de estado pub.dev/packages/provider
http Cliente HTTP pub.dev/packages/http
get_it Service locator (DI) pub.dev/packages/get_it
auto_route Geracao de rotas pub.dev/packages/auto_route
injectable DI com code generation pub.dev/packages/injectable

Checklist da Aula

  • Entender importancia da modularizacao para grandes equipes
  • Criar pacote Flutter com flutter create --template=package
  • Definir API publica com exports
  • Instalar pacotes via CLI e pubspec.yaml
  • Diferenciar Pacote e Plugin
  • Entender versionamento semantico (SemVer)
  • Conhecer opcoes de compartilhamento (pub.dev, Git, hosted)
  • Configurar Flutter Modular (ModularApp, AppModule, AppWidget)
  • Definir rotas com child, module, redirect e wildcard
  • Usar Modular.to para navegacao
  • Registrar dependencias com binds (add, addSingleton, addLazySingleton)
  • Consumir dependencias com Modular.get
  • Organizar modulos filhos por feature

Flutter Cheatsheet - Aula 10

Clean Architecture, Testes Unitarios e Testes de Widget

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


Indice

  1. Clean Architecture - Camadas
  2. Camada Domain
  3. Camada Infra
  4. Camada External
  5. Camada Presenter
  6. Testes Unitarios
  7. Mocks com Mockito
  8. Testes de Widget
  9. Metodologias (TDD, BDD, DDD)
  10. Recursos e Documentacao

Clean Architecture - Camadas

Estrutura de Pastas

lib/
├── main.dart
└── app/
  ├── app_module.dart
  ├── app_widget.dart
  ├── core/                        # Compartilhado entre modulos
  └── modules/
    └── exercises/               # Feature
      ├── exercise_module.dart
      ├── domain/              # Nucleo - regras de negocio
      ├── infra/               # Models, repositorios, interfaces
      ├── external/            # Implementacoes concretas
      └── presenter/           # UI, controllers, widgets

Documentacao: Clean Dart

Regra de Dependencia

External ──> Infra ──> Domain <── Presenter

Domain NAO depende de ninguem.
Infra implementa interfaces do Domain.
External implementa interfaces da Infra.
Presenter consome UseCases do Domain.

Acoplamento vs Desacoplamento

// ACOPLADO - tela conhece banco diretamente
class MyPage {
  final database = AppDatabase(); // dependencia concreta!
}

// DESACOPLADO - tela conhece apenas abstracao
class MyPage {
  final GetExercises useCase; // dependencia abstrata!
  MyPage(this.useCase);
}

Camada Domain

Entity

// domain/entities/exercise.dart

class Exercise {
  final String id;
  final String name;
  final int sets;
  final int reps;
  final String category;
  final bool isCompleted;

  const Exercise({
    required this.id,
    required this.name,
    required this.sets,
    required this.reps,
    required this.category,
    this.isCompleted = false,
  });

  Exercise copyWith({String? name, bool? isCompleted, /* ... */}) {
    return Exercise(
      id: id,
      name: name ?? this.name,
      isCompleted: isCompleted ?? this.isCompleted,
      // ...
    );
  }
}

Repository Interface

// domain/repositories/exercise_repository.dart

abstract class ExerciseRepository {
  Future<List<Exercise>> getAll();
  Future<Exercise?> getById(String id);
  Future<void> add(Exercise exercise);
  Future<void> update(Exercise exercise);
  Future<void> remove(String id);
}

UseCase

// domain/usecases/get_exercises.dart

class GetExercises {
  final ExerciseRepository repository;
  GetExercises(this.repository);

  Future<List<Exercise>> call() => repository.getAll();
}
// domain/usecases/add_exercise.dart

class AddExercise {
  final ExerciseRepository repository;
  AddExercise(this.repository);

  Future<void> call(Exercise exercise) async {
    if (exercise.name.isEmpty) {
      throw ArgumentError('Nome nao pode ser vazio');
    }
    await repository.add(exercise);
  }
}

Camada Infra

Model (Entity + Serializacao JSON Manual)

// infra/models/exercise_model.dart

class ExerciseModel extends Exercise {
  const ExerciseModel({
    required super.id,
    required super.name,
    required super.sets,
    required super.reps,
    required super.category,
    super.isCompleted,
  });

  factory ExerciseModel.fromJson(Map<String, dynamic> json) {
    return ExerciseModel(
      id: json['id'] as String,
      name: json['name'] as String,
      sets: json['sets'] as int,
      reps: json['reps'] as int,
      category: json['category'] as String,
      isCompleted: json['isCompleted'] as bool? ?? false,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'sets': sets,
      'reps': reps,
      'category': category,
      'isCompleted': isCompleted,
    };
  }

  factory ExerciseModel.fromEntity(Exercise e) => ExerciseModel(
    id: e.id, name: e.name, sets: e.sets,
    reps: e.reps, category: e.category,
    isCompleted: e.isCompleted,
  );
}

DataSource Interface

// infra/datasources/exercise_datasource.dart

abstract class ExerciseDataSource {
  Future<List<ExerciseModel>> getAll();
  Future<ExerciseModel?> getById(String id);
  Future<void> save(ExerciseModel exercise);
  Future<void> delete(String id);
}

Repository Implementation (Infra)

// infra/repositories/exercise_repository_impl.dart

class ExerciseRepositoryImpl implements ExerciseRepository {
  final ExerciseDataSource dataSource;
  ExerciseRepositoryImpl(this.dataSource);

  @override
  Future<List<Exercise>> getAll() async {
    return (await dataSource.getAll()).cast<Exercise>();
  }

  @override
  Future<Exercise?> getById(String id) async {
    return dataSource.getById(id);
  }

  @override
  Future<void> add(Exercise exercise) async {
    await dataSource.save(ExerciseModel.fromEntity(exercise));
  }

  @override
  Future<void> update(Exercise exercise) async {
    await dataSource.save(ExerciseModel.fromEntity(exercise));
  }

  @override
  Future<void> remove(String id) async {
    await dataSource.delete(id);
  }
}

Camada External

DataSource In-Memory (Exemplo)

// external/datasources/exercise_datasource_in_memory.dart

class ExerciseDataSourceInMemory implements ExerciseDataSource {
  final List<ExerciseModel> _items = [];

  @override
  Future<List<ExerciseModel>> getAll() async => List.unmodifiable(_items);

  @override
  Future<ExerciseModel?> getById(String id) async {
    try {
      return _items.firstWhere((e) => e.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  Future<void> save(ExerciseModel exercise) async {
    // salvar ou atualizar
  }

  @override
  Future<void> delete(String id) async {
    // remover
  }
}

Camada Presenter

Controller

// presenter/controllers/exercise_controller.dart

class ExerciseController extends ChangeNotifier {
  final GetExercises getExercises;

  List<Exercise> exercises = [];
  bool isLoading = false;
  String? errorMessage;

  ExerciseController({required this.getExercises});

  Future<void> loadExercises() async {
    isLoading = true;
    notifyListeners();
    try {
      exercises = await getExercises.call();
    } catch (e) {
      errorMessage = e.toString();
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
}

Testes Unitarios

Estrutura Basica (AAA)

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('descricao do teste', () {
    // Arrange (Preparar)
    final service = MyService();

    // Act (Agir)
    final result = service.doSomething();

    // Assert (Verificar)
    expect(result, equals(expectedValue));
  });
}

Documentacao: Flutter Testing

setUp e tearDown

void main() {
  late MyService service;

  setUp(() {
    service = MyService(); // Executado ANTES de cada teste
  });

  tearDown(() {
    // Executado APOS cada teste (limpar recursos)
  });

  group('MyService', () {
    test('teste 1', () { /* ... */ });
    test('teste 2', () { /* ... */ });
  });
}

Matchers Uteis

Matcher Uso
equals(x) Igualdade
isNull / isNotNull Nulidade
isA<Type>() Tipo
isEmpty / isNotEmpty Colecao vazia
hasLength(n) Tamanho da colecao
contains(x) Contem item
greaterThan(x) Maior que
lessThan(x) Menor que
throwsException Lanca excecao
throwsA(isA<T>()) Lanca tipo especifico

Rodar Testes

# Rodar todos os testes
flutter test

# Rodar arquivo especifico
flutter test test/unit/get_exercises_test.dart

# Com cobertura
flutter test --coverage

Mocks com Mockito

Setup

dev_dependencies:
  mockito: ^5.4.0

Documentacao: Mockito

Criar Mock Manual

import 'package:mockito/mockito.dart';

// Mock manual - sem @GenerateMocks, sem build_runner
class MockExerciseRepository extends Mock implements ExerciseRepository {}

void main() {
  late MockExerciseRepository mockRepo;

  setUp(() {
    mockRepo = MockExerciseRepository();
  });
}

Configurar Retorno

// Retorno com sucesso (Future)
when(mockRepo.getAll()).thenAnswer(
  (_) async => [exercise1, exercise2],
);

// Retorno sincrono
when(mockRepo.getAll()).thenReturn([exercise]);

// Simular erro
when(mockRepo.getAll()).thenThrow(Exception('Erro'));

// Aceitar qualquer argumento
when(mockRepo.getById(any)).thenAnswer((_) async => null);
when(mockRepo.add(any)).thenAnswer((_) async {});

Verificar Chamadas

// Foi chamado 1 vez
verify(mockRepo.getAll()).called(1);

// Nunca foi chamado
verifyNever(mockRepo.remove(any));

// Nenhuma outra interacao
verifyNoMoreInteractions(mockRepo);

Exemplo Completo

import 'package:mockito/mockito.dart';

// Mock manual
class MockExerciseRepository extends Mock implements ExerciseRepository {}

void main() {
  late GetExercises useCase;
  late MockExerciseRepository mockRepo;

  setUp(() {
    mockRepo = MockExerciseRepository();
    useCase = GetExercises(mockRepo);
  });

  test('deve retornar exercicios', () async {
    when(mockRepo.getAll()).thenAnswer(
      (_) async => [Exercise(id: '1', name: 'Supino', sets: 4, reps: 12, category: 'Peito')],
    );

    final result = await useCase.call();

    expect(result, hasLength(1));
    expect(result.first.name, 'Supino');
    verify(mockRepo.getAll()).called(1);
  });
}

Testes de Widget

Estrutura Basica

testWidgets('descricao', (WidgetTester tester) async {
  // Renderizar widget
  await tester.pumpWidget(MaterialApp(home: MyWidget()));

  // Verificar UI
  expect(find.text('Titulo'), findsOneWidget);
  expect(find.byIcon(Icons.add), findsOneWidget);
});

Finders

find.text('Exercicios');           // Por texto
find.byType(ElevatedButton);       // Por tipo
find.byIcon(Icons.add);            // Por icone
find.byKey(Key('my_key'));          // Por Key
find.widgetWithText(ListTile, 'Supino'); // Widget com texto

Expectations

expect(find.text('Titulo'), findsOneWidget);   // Exatamente 1
expect(find.text('Erro'), findsNothing);        // Nenhum
expect(find.byType(Card), findsNWidgets(5));    // Exatamente N
expect(find.byType(Card), findsWidgets);        // Pelo menos 1

Interacoes

// Tap
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Inserir texto
await tester.enterText(find.byType(TextField), 'Supino');
await tester.pump();

// Scroll
await tester.drag(find.byType(ListView), Offset(0, -300));
await tester.pump();

// Aguardar animacoes
await tester.pumpAndSettle();

pump vs pumpAndSettle

Metodo Quando usar
pump() Apos mudanca de estado simples
pumpAndSettle() Quando ha animacoes/timers
pump(Duration) Avancar tempo especifico

Testando com Provider

testWidgets('com Provider', (tester) async {
  final mockController = MockExerciseController();
  when(mockController.exercises).thenReturn([exercise]);
  when(mockController.isLoading).thenReturn(false);

  await tester.pumpWidget(
    MaterialApp(
      home: ChangeNotifierProvider.value(
        value: mockController,
        child: ExerciseListPage(),
      ),
    ),
  );

  expect(find.text('Supino'), findsOneWidget);
});

Metodologias (TDD, BDD, DDD)

TDD - Test-Driven Development

1. RED     ──> Escreva teste (falha)
2. GREEN   ──> Codigo minimo (passa)
3. REFACTOR ──> Melhore (continua passando)

BDD - Behavior-Driven Development

Given: contexto inicial (estado)
When: acao do usuario
Then: resultado esperado

DDD - Domain-Driven Design

Linguagem Ubiqua: termos do negocio no codigo
Bounded Context: cada area tem seu modelo
Entities: objetos com identidade

Quais Testes Sao Essenciais?

SEMPRE teste:
├── UseCases (regras de negocio)
├── Repositories (acesso a dados)
├── Controllers (logica de estado)
└── Widgets criticos (formularios, listas)

NAO precisa testar:
├── Getters/Setters triviais
├── Construtores simples
└── O framework Flutter

Erros Comuns

Entity com dependencia de framework

// ERRADO - Entity depende de Flutter
import 'package:flutter/material.dart';
class Exercise {
  final Color color; // Dependencia do Flutter!
}

// CERTO - Entity e Dart puro
class Exercise {
  final String colorHex; // String pura
}

UseCase fazendo mais que deveria

// ERRADO - UseCase acessa banco diretamente
class GetExercises {
  final AppDatabase db;
  Future<List> call() => db.exerciseDao.getAll();
}

// CERTO - UseCase usa interface do repository
class GetExercises {
  final ExerciseRepository repository;
  Future<List> call() => repository.getAll();
}

Teste sem mock

// ERRADO - depende de implementacao real
test('get exercises', () async {
  final repo = ExerciseRepositoryImpl(); // implementacao real!
  final result = await repo.getAll();
});

// CERTO - usa mock
test('get exercises', () async {
  when(mockRepo.getAll()).thenAnswer((_) async => [exercise]);
  final result = await useCase.call();
});

Quick Reference

Acao Comando/Codigo
Estrutura Clean app/modules/<feature>/{domain,infra,external,presenter}
Entity class Exercise { ... }
Repository Interface abstract class ExerciseRepository { ... }
UseCase class GetExercises { Future<List> call() => repo.getAll(); }
Model + JSON manual infra/models/exercise_model.dart
Repository (Infra) infra/repositories/exercise_repository_impl.dart
DataSource in-memory external/datasources/exercise_datasource_in_memory.dart
Mock manual class MockExerciseRepository extends Mock implements ExerciseRepository {}
Mock when when(mock.method()).thenAnswer((_) async => value)
Mock verify verify(mock.method()).called(1)
Teste unitario test('desc', () { expect(result, equals(expected)); })
Teste widget testWidgets('desc', (tester) async { await tester.pumpWidget(...); })
Rodar testes flutter test
Teste especifico flutter test test/my_test.dart
Cobertura flutter test --coverage

Recursos e Documentacao

Documentacao Oficial

Recurso Link
Clean Dart (Flutterando) github.com/Flutterando/Clean-Dart
Clean Architecture (Uncle Bob) blog.cleancoder.com
Mockito pub.dev/packages/mockito
Flutter Testing docs.flutter.dev/testing

Pacotes Utilizados

Pacote Descricao Link
mockito Framework de mocks pub.dev/packages/mockito

Checklist da Aula

  • Entender o que e Clean Architecture e suas vantagens
  • Conhecer a regra de dependencia (camadas internas nao conhecem externas)
  • Diferenciar acoplamento e desacoplamento
  • Criar Entity, Repository Interface e UseCase (Domain)
  • Criar Model com fromJson/toJson manual (Infra)
  • Criar DataSource in-memory (External)
  • Implementar Repository (Infra)
  • Entender a piramide de testes
  • Escrever testes unitarios com padrao AAA
  • Usar setUp/tearDown e group
  • Criar mocks manuais com Mockito (extends Mock implements ...)
  • Usar when/thenAnswer e verify
  • Escrever testes de widget com WidgetTester
  • Usar find e expect com widgets
  • Simular interacoes (tap, enterText, drag)
  • Conhecer TDD, BDD e DDD
  • Saber quais testes sao essenciais em um projeto

Proxima Aula: Testes de Integracao e CI/CD

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