Skip to content

Instantly share code, notes, and snippets.

@tiagolpadua
Last active February 12, 2026 18:43
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 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