Apostila · Flutter × React Native
Flutter React Native Ambos Mercado
Apostila de desenvolvimento mobile multiplataforma

Flutter × React Native

Do primeiro hello world até internals de engine, arquitetura em escala e CI/CD — sempre comparando os dois frameworks lado a lado, com foco no que o mercado de trabalho realmente cobra em vagas e entrevistas.

25 capítulosbásico → muito avançado
2 trilhas paralelasaprenda um, entenda os dois
Foco em empregoentrevistas, portfólio, roadmap
Capítulo 00 Ambos Comece aqui

Como usar esta apostila

Esta apostila ensina Flutter e React Native em paralelo. Cada conceito (estado, navegação, testes…) é apresentado uma vez — porque a ideia é a mesma nos dois — e depois mostrado em código dos dois lados. Isso tem uma razão prática de mercado: recrutadores e times raramente exigem "só Flutter" ou "só React Native"; eles querem alguém que entenda desenvolvimento mobile declarativo e consiga transitar.

Três formas de estudar

  • Trilha única: escolha um framework (veja o capítulo 01 para decidir), leia tudo focando na cor dele e apenas "passe o olho" no outro lado. Recomendado para quem busca a primeira vaga.
  • Trilha dupla: leia os dois lados de cada comparação. Mais lento, mas transforma você num candidato raro — dev mobile que conversa com qualquer stack.
  • Consulta: já trabalha com um deles? Use o sumário para pular direto a capítulos avançados (12+) e à parte de carreira (21+).
Convenção de cores

Azul = Flutter · Verde-água = React Native · Roxo = conceito comum aos dois · Âmbar = dica de mercado de trabalho. Os blocos "lado a lado" mostram o mesmo problema resolvido nos dois frameworks.

Mercado

Ao longo da apostila, caixas como esta conectam o conteúdo técnico ao que aparece em descrições de vaga e entrevistas reais. Se seu objetivo é emprego, não pule nenhuma delas.

Pré-requisitos

Lógica de programação básica (variáveis, funções, loops, condicionais) em qualquer linguagem. Se você vem do zero absoluto, estude primeiro fundamentos de programação; se vem da web (HTML/CSS/JS), React Native será terreno familiar; se vem de Java/C#, Dart parecerá quase natural.

Capítulo 01 Mercado Comece aqui

Panorama: o que são, quem usa e qual escolher

Ambos resolvem o mesmo problema: escrever um código-base e entregar apps para Android e iOS (e cada vez mais web e desktop), reduzindo custo e tempo de desenvolvimento. A diferença está em como desenham a interface e em qual ecossistema você entra.

FlutterReact Native
CriadorGoogle (2017)Meta/Facebook (2015)
LinguagemDartJavaScript / TypeScript
Como renderizaDesenha cada pixel com engine própria (Impeller/Skia) — UI idêntica em qualquer aparelhoTraduz componentes para views nativas reais de cada plataforma — UI com "cara" do sistema
Ponto forteConsistência visual, performance previsível, tooling excelente, hot reload muito estávelEcossistema JavaScript gigante, reaproveita devs web/React, atualizações OTA maduras
Ponto fracoDart é usado quase só em Flutter; apps tendem a ficar maiores em tamanhoCamada de interop com nativo adiciona complexidade em apps muito grandes
Quem usaNubank, iFood (partes), BMW, Google Pay, AlibabaMeta (Facebook/Instagram em partes), Shopify, Microsoft (Office/Teams em partes), Discord, PicPay
Além do mobileWeb, Windows, macOS, Linux, embarcadosWeb (react-native-web), Windows/macOS (parcerias), TV

Como o mercado enxerga cada um

  • Flutter é muito forte em fintechs e bancos digitais (o Nubank é o caso mais citado do mundo), consultorias e produtos que exigem UI muito customizada e idêntica nas duas plataformas.
  • React Native domina em startups e empresas com forte cultura web/JavaScript: se o time já tem React na web, contratar/treinar para RN é natural. A ascensão do Expo tornou o início de projeto muito mais simples.
  • Vagas frequentemente pedem "Flutter ou React Native" — conhecer os conceitos comuns (UI declarativa, estado, arquitetura) vale mais do que decorar API de um só.
Como escolher sua trilha principal

Venha da web / já sabe JavaScript ou React? Comece por React Native — sua curva será curtíssima e você aproveita o ecossistema npm.

Vem de Java, C#, Kotlin ou do zero? Flutter costuma ser mais amigável: linguagem tipada, documentação oficial excepcional e menos decisões de configuração no início.

Olhe as vagas da sua região: pesquise no LinkedIn/Gupy/Programathor por "Flutter" e "React Native" + sua cidade ou "remoto". A demanda local deve pesar na decisão. Salários no Brasil são historicamente parecidos entre os dois; a senioridade pesa muito mais que o framework.

Vocabulário que você precisa dominar desde já

UI declarativawidget / componenteestado (state)hot reloadSDKbuild Android (APK/AAB)build iOS (IPA)emulador / simuladorpackage manager (pub / npm)

UI declarativa significa: você descreve como a tela deve estar para um dado estado, e o framework descobre o que mudar. É o oposto do modelo imperativo antigo (textView.setText(...)). Esse conceito único sustenta tudo nesta apostila.

Capítulo 02 Ambos Básico

Ambiente de desenvolvimento

Requisito comum aos dois: Android Studio (pelo SDK do Android e emulador) e, para gerar builds de iPhone, um Mac com Xcode (limitação da Apple — no Windows/Linux você desenvolve normalmente e testa iOS via serviços de nuvem ou aparelho de teste do time).

Flutter
  1. Baixe o SDK em docs.flutter.dev e adicione flutter/bin ao PATH.
  2. Instale Android Studio + plugin Flutter/Dart, ou use VS Code com a extensão Flutter.
  3. Rode flutter doctor — ele diagnostica tudo que falta (licenças Android, Xcode etc.). Só siga em frente com tudo ✓.
  4. Crie e rode: flutter create meu_app && cd meu_app && flutter run.

Gerenciador de pacotes: pub (pubspec.yaml). Catálogo: pub.dev.

React Native
  1. Instale Node.js LTS (recomendado via nvm).
  2. Caminho recomendado hoje: Exponpx create-expo-app meu-app. Ele abstrai o build nativo no início.
  3. Teste no celular com o app Expo Go (escaneia QR code) ou em emulador Android/simulador iOS.
  4. Quando precisar de código nativo custom, use development builds (npx expo prebuild / EAS) — sem "ejetar" no sentido antigo.

Gerenciador de pacotes: npm/yarn/pnpm (package.json). Catálogo: npmjs.com.

Estrutura de projeto

Flutter
meu_app/
├─ lib/            ← seu código Dart (main.dart)
├─ android/ ios/   ← projetos nativos gerados
├─ test/           ← testes
├─ pubspec.yaml    ← dependências e assets
└─ analysis_options.yaml  ← lints
React Native (Expo)
meu-app/
├─ app/            ← telas (Expo Router, file-based)
├─ components/     ← componentes reutilizáveis
├─ android/ ios/   ← gerados sob demanda (prebuild)
├─ package.json    ← dependências e scripts
└─ tsconfig.json   ← TypeScript
Hot reload — seu melhor amigo

Nos dois frameworks, salvar o arquivo atualiza o app rodando sem perder o estado (Flutter: hot reload; RN: Fast Refresh). Esse ciclo de feedback de segundos é o que torna mobile multiplataforma tão produtivo — use um aparelho físico sempre que puder, é mais fiel que o emulador.

Mercado

Saber diagnosticar ambiente (flutter doctor, versões de Node/JDK/Gradle/CocoaPods) é um diferencial real: boa parte do sofrimento de times mobile está em build quebrado, não em código. Em entrevistas para vaga júnior, "já publiquei um APK/AAB assinado" e "sei rodar no aparelho físico" contam pontos.

Capítulo 03 Ambos Básico

As linguagens: Dart e TypeScript

Você não precisa "terminar" a linguagem antes do framework — aprenda os dois juntos. Mas estes fundamentos são inegociáveis, porque aparecem em todo teste técnico.

Variáveis, tipos e null safety

Dart
final nome = 'Ana';        // imutável (runtime)
const pi = 3.14;           // imutável (compile-time)
var idade = 25;            // tipo inferido (int)
String? apelido;           // pode ser null (null safety)
print(apelido ?? 'sem apelido'); // operador ??
print(apelido?.length);    // acesso seguro ?.
TypeScript
const nome = 'Ana';        // imutável (binding)
let idade = 25;            // tipo inferido (number)
let apelido: string | null = null;
console.log(apelido ?? 'sem apelido');
console.log(apelido?.length);
// TS: tipos são apagados em runtime (só checagem)

Null safety é o conceito nº 1: os dois sistemas de tipos forçam você a tratar valores possivelmente nulos, eliminando a clássica falha null pointer. Dart verifica em compilação e garante em runtime; TypeScript verifica só em compilação (JavaScript por baixo aceita qualquer coisa — por isso valide dados vindos de APIs).

Funções, coleções e o trio map/filter/reduce

Dart
int dobro(int x) => x * 2;  // arrow function

final nums = [1, 2, 3, 4];
final pares = nums.where((n) => n.isEven).toList();
final dobros = nums.map(dobro).toList();
final soma = nums.fold(0, (acc, n) => acc + n);

final user = {'nome': 'Ana', 'idade': 25}; // Map
TypeScript
const dobro = (x: number) => x * 2;

const nums = [1, 2, 3, 4];
const pares = nums.filter(n => n % 2 === 0);
const dobros = nums.map(dobro);
const soma = nums.reduce((acc, n) => acc + n, 0);

const user = { nome: 'Ana', idade: 25 }; // objeto

Classes, imutabilidade e modelagem de dados

Dart
class Usuario {
  final String nome;
  final int idade;
  const Usuario({required this.nome, required this.idade});

  Usuario copyWith({String? nome, int? idade}) =>
      Usuario(nome: nome ?? this.nome, idade: idade ?? this.idade);
}
// Dart 3: records e pattern matching
final (lat, lng) = (-23.55, -46.63);
sealed class Resultado {}       // classes seladas p/ estados
TypeScript
interface Usuario {
  readonly nome: string;
  readonly idade: number;
}
// imutabilidade por convenção: spread
const u1: Usuario = { nome: 'Ana', idade: 25 };
const u2 = { ...u1, idade: 26 };

// union types p/ estados (equivale a sealed)
type Resultado =
  | { status: 'ok'; dados: string[] }
  | { status: 'erro'; msg: string };
Conceito-chave: imutabilidade

UI declarativa funciona comparando estados. Se você muta um objeto em vez de criar uma cópia nova, o framework pode não perceber a mudança. Por isso copyWith (Dart) e spread {...obj} (TS) aparecem o tempo todo. Grave esse padrão — ele cai em entrevista.

Assíncrono: Future/Promise e async/await

Dart
Future<String> buscarNome() async {
  await Future.delayed(const Duration(seconds: 1));
  return 'Ana';
}
// Streams: sequência de valores no tempo
Stream<int> contador() async* {
  for (var i = 1; i <= 3; i++) { yield i; }
}
TypeScript
async function buscarNome(): Promise<string> {
  await new Promise(r => setTimeout(r, 1000));
  return 'Ana';
}
// Async iterators / Observables (RxJS) cobrem o papel de Streams
async function* contador() {
  for (let i = 1; i <= 3; i++) yield i;
}
Mercado

Testes técnicos de vaga júnior quase sempre cobram: manipulação de listas (map/filter/reduce), null safety, async/await com tratamento de erro e modelagem de uma entidade (classe/interface) a partir de um JSON. Treine exatamente isso.

Capítulo 04 Ambos Básico

Primeiro app: UI declarativa na prática

O "hello world" do mobile declarativo é o contador: um número na tela e um botão que o incrementa. Ele ensina os três pilares — estrutura da UI, estado e re-renderização.

Flutter — widgets
import 'package:flutter/material.dart';

void main() => runApp(const MeuApp());

class MeuApp extends StatelessWidget {
  const MeuApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: TelaContador());
}

class TelaContador extends StatefulWidget {
  const TelaContador({super.key});
  @override
  State<TelaContador> createState() => _TelaContadorState();
}

class _TelaContadorState extends State<TelaContador> {
  int contador = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contador')),
      body: Center(child: Text('Cliques: $contador',
          style: const TextStyle(fontSize: 24))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => contador++),
        child: const Icon(Icons.add),
      ),
    );
  }
}
React Native — componentes
import { useState } from 'react';
import { View, Text, Pressable,
         StyleSheet } from 'react-native';

export default function TelaContador() {
  const [contador, setContador] = useState(0);

  return (
    <View style={styles.tela}>
      <Text style={styles.texto}>
        Cliques: {contador}
      </Text>
      <Pressable
        style={styles.botao}
        onPress={() => setContador(c => c + 1)}>
        <Text style={styles.mais}>+</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  tela:  { flex: 1, alignItems: 'center',
           justifyContent: 'center', gap: 16 },
  texto: { fontSize: 24 },
  botao: { backgroundColor: '#0E8F81',
           borderRadius: 28, padding: 16 },
  mais:  { color: '#fff', fontSize: 20 },
});

O que está acontecendo (nos dois)

  1. A UI é uma função do estado. build() / o corpo do componente descrevem a tela para o estado atual.
  2. Mudar estado dispara re-renderização. setState (Flutter) e setContador (React) avisam o framework: "o estado mudou, redesenhe".
  3. O framework é eficiente: ele não redesenha a tela inteira do zero — compara a descrição nova com a antiga e aplica só as diferenças (diffing na árvore de widgets/elements no Flutter; reconciliação do React no RN).
ConceitoFlutterReact Native
Bloco de UIWidget (tudo é widget: texto, padding, tema…)Componente (função que retorna JSX)
Sem estadoStatelessWidgetComponente sem hooks de estado
Com estadoStatefulWidget + classe StateuseState, useReducer
Dados do paiParâmetros do construtorProps
Ciclo de vidainitState, didUpdateWidget, disposeuseEffect (montagem, atualização, limpeza)
Composição > herança

Nos dois mundos, você constrói telas compondo peças pequenas (um Card que contém um Column que contém Texts) em vez de herdar de classes gigantes. Componentes pequenos, com uma responsabilidade, são o que revisores de código e entrevistadores querem ver.

Capítulo 05 Ambos Básico

Layout e estilização

Os dois usam o mesmo modelo mental de caixas flexíveis: elementos em linha ou coluna, distribuindo espaço com pesos e alinhamentos. Se você conhece Flexbox da web, já sabe 80% do layout de RN — e o Row/Column do Flutter é o mesmo conceito com outros nomes.

Preciso de…FlutterReact Native
Empilhar na verticalColumn<View> (flexDirection padrão é column)
Lado a ladoRowflexDirection: 'row'
Ocupar espaço restanteExpanded / Flexible (flex)flex: 1
Espaçamento internoPadding(padding: EdgeInsets…)padding / paddingHorizontal…
Sobrepor elementosStack + Positionedposition: 'absolute'
CentralizarCenter ou alinhamentos de Row/ColumnalignItems + justifyContent: 'center'
RolagemSingleChildScrollView, ListViewScrollView, FlatList
Área segura (notch)SafeAreaSafeAreaView (safe-area-context)

Um card de perfil, dos dois lados

Flutter
Container(
  padding: const EdgeInsets.all(16),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [BoxShadow(
      color: Colors.black12, blurRadius: 8)],
  ),
  child: Row(children: [
    const CircleAvatar(radius: 24),
    const SizedBox(width: 12),
    Expanded(child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: const [
        Text('Ana Souza',
          style: TextStyle(fontWeight: FontWeight.bold)),
        Text('Dev Mobile',
          style: TextStyle(color: Colors.grey)),
      ],
    )),
  ]),
)
React Native
<View style={s.card}>
  <Image source={{ uri: foto }} style={s.avatar} />
  <View style={{ flex: 1 }}>
    <Text style={s.nome}>Ana Souza</Text>
    <Text style={s.cargo}>Dev Mobile</Text>
  </View>
</View>

const s = StyleSheet.create({
  card: { flexDirection: 'row', gap: 12,
    padding: 16, backgroundColor: '#fff',
    borderRadius: 12, elevation: 3,          // Android
    shadowColor: '#000', shadowOpacity: 0.08,
    shadowRadius: 8 },                        // iOS
  avatar: { width: 48, height: 48, borderRadius: 24 },
  nome:   { fontWeight: '700' },
  cargo:  { color: '#6b7280' },
});

Tema e design system

  • Flutter: ThemeData centraliza cores, tipografia e formas (Material 3 com ColorScheme.fromSeed). Widgets Cupertino imitam iOS quando necessário. Acesse com Theme.of(context).
  • React Native: não há tema nativo único — times usam Context/Provider próprio, bibliotecas como react-native-paper, Tamagui, NativeWind (Tailwind para RN) ou tokens de design compartilhados com a web.

Responsividade

  • Flutter: MediaQuery.sizeOf(context), LayoutBuilder (constraints do pai) e o mantra oficial "constraints go down, sizes go up, parent sets position" — entenda essa frase, ela explica todo erro de "overflow amarelo".
  • RN: useWindowDimensions(), porcentagens/flex, e componentes adaptativos por plataforma via Platform.select ou arquivos Component.ios.tsx / Component.android.tsx.
Mercado

Um teste prático comum é reproduzir uma tela do Figma pixel a pixel, responsiva e com dark mode. Treine: pegue telas de apps reais (Nubank, iFood, Spotify) e reconstrua. Publique os resultados no GitHub com screenshot no README — isso vira portfólio direto.

Capítulo 06 Ambos Básico

Listas, imagens e formulários

90% das telas de apps reais são: uma lista de coisas (feed, extrato, catálogo) ou um formulário (login, cadastro, checkout). Dominar esses dois padrões com carinho — loading, erro, vazio, validação — é o que separa projeto de estudo de app profissional.

Listas performáticas (virtualização)

Nunca renderize mil itens de uma vez. Os dois frameworks têm listas preguiçosas que só constroem o que está visível:

Flutter — ListView.builder
ListView.builder(
  itemCount: produtos.length,
  itemBuilder: (context, i) {
    final p = produtos[i];
    return ListTile(
      leading: Image.network(p.thumb),
      title: Text(p.nome),
      subtitle: Text('R\$ ${p.preco}'),
      onTap: () => abrirDetalhe(p),
    );
  },
)
React Native — FlatList
<FlatList
  data={produtos}
  keyExtractor={p => p.id}
  renderItem={({ item: p }) => (
    <Pressable onPress={() => abrirDetalhe(p)}
      style={s.linha}>
      <Image source={{ uri: p.thumb }} style={s.thumb}/>
      <View>
        <Text>{p.nome}</Text>
        <Text>R$ {p.preco}</Text>
      </View>
    </Pressable>
  )}
/>

Padrões associados que você deve saber implementar: pull to refresh (RefreshIndicator / prop refreshControl), paginação infinita (detectar fim do scroll e buscar próxima página) e chaves estáveis (keys) para o diffing funcionar.

Formulário de login com validação

Flutter — Form + validator
final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(children: [
    TextFormField(
      decoration: const InputDecoration(
        labelText: 'E-mail'),
      keyboardType: TextInputType.emailAddress,
      validator: (v) =>
        (v == null || !v.contains('@'))
          ? 'E-mail inválido' : null,
    ),
    TextFormField(
      obscureText: true,
      decoration: const InputDecoration(
        labelText: 'Senha'),
      validator: (v) => (v ?? '').length < 6
        ? 'Mínimo 6 caracteres' : null,
    ),
    FilledButton(
      onPressed: () {
        if (_formKey.currentState!.validate()) {
          fazerLogin();
        }
      },
      child: const Text('Entrar'),
    ),
  ]),
)
React Native — react-hook-form + zod
const schema = z.object({
  email: z.string().email('E-mail inválido'),
  senha: z.string().min(6, 'Mínimo 6 caracteres'),
});

const { control, handleSubmit,
        formState: { errors } } = useForm({
  resolver: zodResolver(schema),
});

<Controller control={control} name="email"
  render={({ field }) => (
    <TextInput
      placeholder="E-mail"
      autoCapitalize="none"
      keyboardType="email-address"
      value={field.value}
      onChangeText={field.onChange} />
  )} />
{errors.email && <Text style={s.erro}>
  {errors.email.message}</Text>}

<Button title="Entrar"
  onPress={handleSubmit(fazerLogin)} />

Os 4 estados de toda tela

Toda tela que depende de dados tem quatro estados, e apps profissionais tratam todos: carregando (skeleton/spinner), sucesso (conteúdo), vazio (mensagem + ação) e erro (mensagem clara + botão "tentar novamente"). Modele isso explicitamente — sealed class em Dart, union type em TS — em vez de espalhar booleanos isLoading.

Mercado

Em code review de teste técnico, avaliadores procuram exatamente isso: tratamento de erro e estado vazio, validação de formulário, teclado correto por campo (email-address, numeric), acessibilidade básica (labels) e lista virtualizada. Um contador bonito não passa; um CRUD com esses cuidados, sim.

Capítulo 08 Ambos Intermediário

Gerenciamento de estado

Este é o tema mais perguntado em entrevistas mobile. O problema: o contador do capítulo 04 guarda estado dentro da própria tela — mas e quando o carrinho de compras precisa aparecer no ícone da appbar, na tela de produto e no checkout ao mesmo tempo? Estado compartilhado precisa viver fora dos widgets/componentes.

A escada do estado

  1. Estado local: setState / useState. Para o que só interessa àquela tela (campo de texto, aba selecionada). Não pule para ferramentas pesadas sem precisar.
  2. Estado elevado: subir o estado ao ancestral comum e passar para baixo (props / construtores). Funciona, mas gera "prop drilling" quando a árvore cresce.
  3. Estado global/injetado: um "lugar" observável fora da árvore, que as telas leem e escutam. Aqui entram as bibliotecas.
FrameworkOpções por popularidade no mercadoQuando usar
FlutterRiverpod (moderno, testável), Bloc/Cubit (muito comum em bancos/fintechs e times grandes), Provider (legado comum), GetX (comum em projetos BR, mas visto com ressalvas em times maduros)Riverpod para projetos novos; Bloc quando o time valoriza fluxo de eventos explícito e padronização rígida
React NativeZustand (leve, moderno), Redux Toolkit (padrão corporativo, muito legado), Context API (para temas/auth simples), Jotai/MobXZustand para a maioria; Redux Toolkit em bases grandes/legadas; e TanStack Query para estado de servidor (ver cap. 09)

Carrinho de compras, dos dois lados

Flutter — Riverpod
// estado imutável + notifier
class CarrinhoNotifier extends Notifier<List<Item>> {
  @override
  List<Item> build() => [];

  void adicionar(Item item) =>
      state = [...state, item];   // nova lista!
  void remover(String id) =>
      state = state.where((i) => i.id != id).toList();
}

final carrinhoProvider =
  NotifierProvider<CarrinhoNotifier, List<Item>>(
    CarrinhoNotifier.new);

final totalProvider = Provider<double>((ref) {
  final itens = ref.watch(carrinhoProvider);
  return itens.fold(0, (t, i) => t + i.preco);
});   // estado derivado, recalcula sozinho

// na UI (ConsumerWidget):
final total = ref.watch(totalProvider);
ref.read(carrinhoProvider.notifier).adicionar(item);
React Native — Zustand
type Carrinho = {
  itens: Item[];
  adicionar: (item: Item) => void;
  remover: (id: string) => void;
  total: () => number;
};

const useCarrinho = create<Carrinho>((set, get) => ({
  itens: [],
  adicionar: (item) =>
    set(s => ({ itens: [...s.itens, item] })),
  remover: (id) =>
    set(s => ({
      itens: s.itens.filter(i => i.id !== id) })),
  total: () =>
    get().itens.reduce((t, i) => t + i.preco, 0),
}));

// na UI — seletor evita re-renders desnecessários:
const itens = useCarrinho(s => s.itens);
const adicionar = useCarrinho(s => s.adicionar);

Bloc e Redux: o padrão "eventos → estados"

Bloc (Flutter) e Redux (RN) compartilham a mesma filosofia: a UI despacha eventos/ações ("AdicionouAoCarrinho"), uma camada pura transforma estado atual + evento → novo estado, e a UI apenas reage. Vantagens: previsibilidade, histórico de mudanças, testes triviais. Custo: verbosidade. Times grandes (bancos, seguradoras) adoram exatamente pela padronização.

// Bloc (Flutter) — mesmo raciocínio do reducer do Redux
class CarrinhoBloc extends Bloc<CarrinhoEvento, CarrinhoEstado> {
  CarrinhoBloc() : super(CarrinhoEstado.vazio()) {
    on<ItemAdicionado>((e, emit) =>
        emit(state.copyWith(itens: [...state.itens, e.item])));
    on<ItemRemovido>((e, emit) =>
        emit(state.copyWith(
            itens: state.itens.where((i) => i.id != e.id).toList())));
  }
}
Erros clássicos (e perguntas de entrevista)

Mutar o estado em vez de criar cópia — a UI não atualiza. Colocar tudo no estado global — estado de formulário é local. Reconstruir a tela inteira quando só um número mudou — use seletores (Zustand/Redux) ou providers granulares (Riverpod) / select (Bloc).

Mercado

Prepare um discurso de 60 segundos: "que gerenciador de estado você usa e por quê?". Resposta madura compara pelo menos duas opções, explica trade-offs (verbosidade × padronização, curva × testabilidade) e separa estado de cliente (carrinho, tema) de estado de servidor (dados de API — capítulo 09). Dizer só "uso X porque é o que aprendi" é resposta de júnior raso.

Capítulo 09 Ambos Intermediário

APIs, HTTP e programação assíncrona

Apps são, na prática, interfaces bonitas para APIs. O ciclo completo: requisição → JSON → modelo tipado → estado → UI, com loading e erro tratados em cada etapa.

FlutterReact Native
Cliente HTTP padrão de mercadodio (interceptors, timeout, cancelamento); http para casos simplesaxios ou fetch nativo; TanStack Query por cima para cache
JSON → modelofromJson manual ou geração com json_serializable/freezedtipar com interface + validar com zod em runtime
Auth tokeninterceptor do dio adiciona headerinterceptor do axios / wrapper do fetch
Flutter — dio + modelo + freezed
@freezed
class Produto with _$Produto {
  const factory Produto({
    required String id,
    required String nome,
    required double preco,
  }) = _Produto;
  factory Produto.fromJson(Map<String, dynamic> json)
      => _$ProdutoFromJson(json);
}

class ProdutoRepository {
  final Dio _dio;
  ProdutoRepository(this._dio);

  Future<List<Produto>> listar() async {
    try {
      final res = await _dio.get('/produtos');
      return (res.data as List)
          .map((j) => Produto.fromJson(j))
          .toList();
    } on DioException catch (e) {
      throw FalhaDeRede.deDio(e); // erro de domínio
    }
  }
}
React Native — axios + zod + TanStack Query
const ProdutoSchema = z.object({
  id: z.string(),
  nome: z.string(),
  preco: z.number(),
});
type Produto = z.infer<typeof ProdutoSchema>;

async function listarProdutos(): Promise<Produto[]> {
  const { data } = await api.get('/produtos');
  return z.array(ProdutoSchema).parse(data);
  // valida em runtime: API mentiu? erro claro aqui,
  // não um crash misterioso na UI
}

// na tela — cache, retry e loading grátis:
const { data, isPending, error, refetch } =
  useQuery({
    queryKey: ['produtos'],
    queryFn: listarProdutos,
  });

Estado de servidor ≠ estado de cliente

Dados vindos de API têm necessidades próprias: cache, revalidação, retry, deduplicação de requisições e atualização otimista. No RN, TanStack Query resolve tudo isso e virou padrão de mercado. No Flutter, o papel é coberto por FutureProvider/AsyncNotifier do Riverpod (com AsyncValue modelando loading/erro/dado) ou por camadas de repositório com cache manual.

Tratamento de erro profissional

  • Converta erros técnicos (timeout, 401, 500, sem conexão) em erros de domínio com mensagens acionáveis para o usuário.
  • 401 → refresh token: interceptor detecta, renova o token e repete a requisição original de forma transparente (implementação clássica pedida em entrevistas pleno).
  • Retry com backoff exponencial para falhas transitórias; nunca para erros 4xx de validação.
  • Cancelamento: telas destruídas não devem completar requisições e tentar atualizar estado morto (CancelToken no dio; AbortController/limpeza do useEffect no RN).
Mercado

Teste técnico mais comum do mercado mobile BR: "consuma esta API pública e mostre lista + detalhe". O que diferencia candidatos: camada de repositório separada da UI, modelos tipados com validação, os 4 estados de tela, paginação e pelo menos um teste. Faça esse exercício com a API do GitHub ou PokeAPI e deixe pronto no portfólio.

Capítulo 10 Ambos Intermediário

Persistência de dados

Três níveis de persistência local, do mais simples ao mais poderoso — escolha pela necessidade, não pelo hype:

NecessidadeFlutterReact Native
Preferências simples (tema, flag de onboarding)shared_preferencesAsyncStorage ou react-native-mmkv (muito mais rápido)
Dados sensíveis (tokens, credenciais)flutter_secure_storage (Keychain/Keystore)expo-secure-store / Keychain
Banco local estruturado / offline-firstdrift (SQL tipado), Isar/Hive (NoSQL), sqfliteexpo-sqlite, WatermelonDB (offline-first em escala), Realm
Flutter — drift (SQL tipado)
class Tarefas extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get titulo => text()();
  BoolColumn get feita =>
      boolean().withDefault(const Constant(false))();
}

// consultas geradas e tipadas:
Future<List<Tarefa>> pendentes() =>
  (select(tarefas)..where((t) => t.feita.equals(false)))
    .get();

// Stream: a UI observa o banco e reage a mudanças
Stream<List<Tarefa>> observarTodas() =>
  select(tarefas).watch();
React Native — expo-sqlite
const db = await SQLite.openDatabaseAsync('app.db');

await db.execAsync(`
  CREATE TABLE IF NOT EXISTS tarefas (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titulo TEXT NOT NULL,
    feita INTEGER DEFAULT 0
  );`);

const pendentes = await db.getAllAsync<Tarefa>(
  'SELECT * FROM tarefas WHERE feita = 0');

await db.runAsync(
  'INSERT INTO tarefas (titulo) VALUES (?)',
  titulo);   // sempre parametrizado (SQL injection)

Offline-first (nível pleno/sênior)

Apps de campo, delivery e fintech precisam funcionar sem rede. O padrão: o banco local é a fonte de verdade da UI; uma camada de sincronização concilia com o servidor quando há conexão. Você precisa saber discutir:

  • Fila de mutações: ações offline entram numa fila e são reenviadas com política de retry.
  • Resolução de conflito: last-write-wins (simples, perde dados) vs merge por campo vs versionamento no servidor.
  • Idempotência: reenviar a mesma operação duas vezes não pode duplicar registros (chave de idempotência).
  • Ferramentas: WatermelonDB (RN) foi desenhado para isso; no Flutter, drift/Isar + camada própria, ou backends com sync embutido (Firebase Firestore, PowerSync, Supabase).
Nunca faça

Guardar token de acesso em AsyncStorage/shared_preferences (não são criptografados — use secure storage), montar SQL concatenando strings, ou bloquear a thread de UI com leituras gigantes síncronas.

Mercado

"Experiência com offline-first" aparece explicitamente em vagas pleno/sênior de fintechs e logística. Ter um projeto de portfólio com fila de sincronização + resolução de conflito documentada no README coloca você em outra prateleira salarial.

Capítulo 11 Ambos Intermediário

Arquitetura de apps

Arquitetura é a resposta para: "como organizo o código para que 10 pessoas mexam nele por 5 anos sem virar um pântano?". Os nomes variam (Clean Architecture, MVVM, camadas), mas a essência é uma só: separar UI, regra de negócio e acesso a dados, com dependências apontando para dentro.

As três camadas que todo entrevistador espera ouvir

CamadaResponsabilidadeFlutter (típico)React Native (típico)
ApresentaçãoTelas, widgets/componentes e o estado de tela (view model)Widgets + Riverpod/BlocComponentes + hooks customizados / Zustand
DomínioRegras de negócio puras: entidades, casos de uso, contratos (interfaces) de repositório. Zero import de frameworkClasses Dart purasFunções/classes TS puras
DadosImplementações: API (dio/axios), banco, cache. Converte DTOs ↔ entidadesRepositories + data sourcesServices/repositories + TanStack Query
// Organização feature-first (recomendada nos dois frameworks):
lib/ (ou src/)
├─ features/
│  ├─ auth/
│  │  ├─ presentation/   ← telas, controllers/hooks
│  │  ├─ domain/         ← entidades, use cases, contratos
│  │  └─ data/           ← repositórios, API, DTOs
│  ├─ carrinho/
│  └─ catalogo/
├─ core/                 ← http client, tema, rotas, utils
└─ main.dart / App.tsx

Feature-first (agrupar por funcionalidade) escala melhor que layer-first (pastas globais screens/, models/…): times inteiros trabalham numa feature sem conflitos, e apagar uma feature é apagar uma pasta.

Inversão de dependência na prática

Flutter
// domínio define o contrato:
abstract interface class AuthRepository {
  Future<Usuario> login(String email, String senha);
}

// caso de uso depende do contrato, não do dio:
class FazerLogin {
  final AuthRepository repo;
  FazerLogin(this.repo);
  Future<Usuario> call(String e, String s) {
    if (!e.contains('@')) throw EmailInvalido();
    return repo.login(e, s);
  }
}

// injeção: Riverpod liga contrato → implementação
final authRepoProvider = Provider<AuthRepository>(
  (ref) => AuthRepositoryHttp(ref.watch(dioProvider)));
React Native
// domínio define o contrato:
interface AuthRepository {
  login(email: string, senha: string): Promise<Usuario>;
}

// caso de uso como função que recebe dependências:
const criarFazerLogin =
  (repo: AuthRepository) =>
  async (email: string, senha: string) => {
    if (!email.includes('@')) throw new EmailInvalido();
    return repo.login(email, senha);
  };

// composição na raiz do app (ou via context):
const fazerLogin = criarFazerLogin(authRepositoryHttp);

Por que isso importa: a regra de negócio fica testável sem rede e sem UI (injete um repositório fake), e trocar dio por outra lib — ou REST por GraphQL — não toca o domínio.

Dose certa de arquitetura

Clean Architecture completa num app de 3 telas é overengineering — e entrevistadores sêniores testam se você sabe disso. Resposta madura: "começo com camadas simples (UI / dados) e extraio domínio quando regras de negócio crescem; arquitetura é custo que precisa se pagar".

Mercado

Vagas pleno/sênior pedem literalmente: "Clean Architecture", "MVVM", "SOLID", "injeção de dependência", "testes". No seu portfólio, um README com diagrama das camadas e justificativa das decisões vale tanto quanto o código — é o que o avaliador lê primeiro.

Capítulo 12 Ambos Avançado

Testes

Testes são o divisor de águas entre júnior e pleno em processos seletivos. A pirâmide clássica vale para mobile: muitos testes unitários (rápidos, baratos), alguns de UI/componente, poucos E2E (lentos, caros, mas cobrem o fluxo real no aparelho).

NívelFlutterReact Native
Unitário (domínio, view models)package:test + mocktailJest / Vitest
UI (widget/componente)flutter_test (widget tests) — roda sem emulador!React Native Testing Library
E2E (aparelho real/emulador)integration_test + Patrol (interage até com permissões nativas)Maestro (YAML simples, padrão emergente) ou Detox
Visual/goldengolden tests (compara screenshots)snapshot tests / ferramentas de screenshot
Flutter — widget test
testWidgets('incrementa ao tocar no botão',
    (tester) async {
  await tester.pumpWidget(
    const MaterialApp(home: TelaContador()));

  expect(find.text('Cliques: 0'), findsOneWidget);

  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();  // processa o rebuild

  expect(find.text('Cliques: 1'), findsOneWidget);
});

// unitário com mock do repositório:
test('login falha com email inválido', () {
  final repo = MockAuthRepository();
  final usecase = FazerLogin(repo);
  expect(() => usecase('x', '123456'),
      throwsA(isA<EmailInvalido>()));
  verifyNever(() => repo.login(any(), any()));
});
React Native — RNTL + Jest
test('incrementa ao tocar no botão', () => {
  render(<TelaContador />);

  expect(screen.getByText('Cliques: 0')).toBeOnTheScreen();

  fireEvent.press(screen.getByRole('button'));

  expect(screen.getByText('Cliques: 1')).toBeOnTheScreen();
});

// unitário com mock do repositório:
test('login falha com email inválido', async () => {
  const repo = { login: jest.fn() };
  const fazerLogin = criarFazerLogin(repo);

  await expect(fazerLogin('x', '123456'))
    .rejects.toThrow(EmailInvalido);
  expect(repo.login).not.toHaveBeenCalled();
});

E2E: o fluxo crítico no aparelho

# Maestro (React Native e Flutter!) — fluxo de login em YAML:
appId: com.exemplo.app
---
- launchApp
- tapOn: "E-mail"
- inputText: "ana@exemplo.com"
- tapOn: "Senha"
- inputText: "123456"
- tapOn: "Entrar"
- assertVisible: "Bem-vinda, Ana"

O que testar (e o que não)

  • Teste: regras de negócio (100% se puder), view models/notifiers/reducers, conversão de JSON, fluxos críticos (login, pagamento) em E2E.
  • Não desperdice testes em: detalhes visuais triviais, código de biblioteca de terceiros, getters/setters.
  • Teste comportamento, não implementação: "ao tocar em Entrar com senha errada, aparece mensagem X" — não "o método _validar foi chamado".
  • Arquitetura boa (cap. 11) existe para isso: domínio puro testa sem mock de framework.
Mercado

Em desafios técnicos, um punhado de testes bem escolhidos (use case + um widget/componente + JSON parsing) impressiona mais que cobertura de 90% de testes rasos. E prepare-se para a pergunta "como você testaria este app?" — responda com a pirâmide e exemplos concretos das ferramentas acima.

Capítulo 13 Ambos Avançado

Performance

Meta universal: 60 fps = 16,6 ms por frame (120 fps = 8,3 ms em telas modernas). Estourou o orçamento, o usuário vê jank (engasgo). As causas e curas diferem por framework:

Flutter — rebuilds e shaders
  • const em tudo que puder: widgets const não são reconstruídos. O linter avisa; obedeça.
  • Escute pouco: ref.watch(provider.select(...)), BlocSelector, quebrar widgets grandes em menores — só reconstrua o que mudou.
  • Evite trabalho no build(): build roda dezenas de vezes; nada de parsear JSON ou ordenar listas ali.
  • Imagens: cacheWidth/cacheHeight para decodificar no tamanho certo; cached_network_image.
  • Impeller (engine atual) eliminou o jank clássico de compilação de shaders da era Skia.
  • Ferramentas: Flutter DevTools — Performance overlay, CPU profiler, widget rebuild stats, Memory. Sempre meça em modo profile num aparelho físico, nunca em debug.
React Native — re-renders e a ponte
  • Re-render em cascata: pai re-renderiza → filhos também. Armas: React.memo, useCallback, useMemo, seletores no Zustand/Redux.
  • Listas grandes: use FlashList (Shopify) no lugar de FlatList — recicla células como o nativo.
  • Hermes (engine JS padrão): bytecode pré-compilado = startup rápido e menos memória.
  • Nova Arquitetura (JSI): comunicação JS↔nativo síncrona sem serializar JSON pela "bridge" antiga — animações e módulos pesados ficaram outra coisa (cap. 17).
  • Animações fora da thread JS: Reanimated roda worklets na thread de UI (cap. 15).
  • Ferramentas: React DevTools (profiler de renders), Flipper/React Native DevTools, why-did-you-render.

Checklist de performance que vale para os dois

  • Medir antes de otimizar (profiler, modo release/profile, aparelho físico modesto — o usuário não tem o seu flagship).
  • Listas virtualizadas com itens de altura estável e keys corretas.
  • Imagens redimensionadas no servidor/CDN, com cache e placeholder.
  • Startup: adiar trabalho não essencial, lazy loading de features, deferred components (Flutter) / inline requires (RN).
  • Tamanho do app: flutter build appbundle --analyze-size / análise de bundle JS; remover libs não usadas.
  • Vazamento de memória: cancelar subscriptions/streams no dispose e limpar efeitos no useEffect.
Mercado

Pergunta clássica de entrevista pleno/sênior: "o app está travando ao rolar a lista, como você investiga?". Resposta forte é um método: reproduzir em release num aparelho médio → profiler para achar o frame estourado → identificar causa (rebuild/re-render? decode de imagem? trabalho na thread principal?) → corrigir → medir de novo. Quem responde com método, e não com chute de otimização, passa.

Capítulo 14 Ambos Avançado

Código nativo e interoperabilidade

Cedo ou tarde o app precisa de algo que o framework não expõe: um SDK de pagamento, um sensor específico, uma lib nativa da empresa. Saber atravessar a fronteira para Kotlin/Swift é o que define o teto da sua senioridade multiplataforma.

Flutter — Platform Channels
// Dart: chama o lado nativo por um canal nomeado
static const _canal =
    MethodChannel('app.exemplo/bateria');

Future<int> nivelBateria() async {
  final nivel = await _canal
      .invokeMethod<int>('getNivel');
  return nivel ?? -1;
}
// Kotlin (Android): responde ao canal
MethodChannel(flutterEngine.dartExecutor
    .binaryMessenger, "app.exemplo/bateria")
  .setMethodCallHandler { call, result ->
    if (call.method == "getNivel") {
      result.success(nivelDaBateria())
    } else result.notImplemented()
  }

Alternativas: Pigeon (gera o código do canal com tipagem, elimina erros de string), dart:ffi (chama C/C++/Rust direto, sem canal — alta performance), e Platform Views para embutir uma view nativa (mapa, webview) na árvore Flutter.

React Native — Turbo Modules (Nova Arquitetura)
// Spec TypeScript — o codegen gera as interfaces nativas
export interface Spec extends TurboModule {
  getNivelBateria(): Promise<number>;
}
export default TurboModuleRegistry
  .getEnforcing<Spec>('Bateria');
// Kotlin: implementa a interface gerada
class BateriaModule(ctx: ReactApplicationContext) :
    NativeBateriaSpec(ctx) {
  override fun getNivelBateria(promise: Promise) {
    promise.resolve(nivelDaBateria())
  }
}

Com JSI, JS segura referências diretas a objetos C++ — chamadas síncronas, sem serializar JSON pela ponte antiga. Fabric faz o mesmo para componentes de UI nativos. No ecossistema Expo, o Expo Modules API (Swift/Kotlin com DSL moderna) é o jeito mais produtivo de escrever módulos.

O que você precisa saber do mundo nativo (mesmo sem ser dev nativo)

  • Android: o que é Gradle e onde mexer (build.gradle: versões, permissões via AndroidManifest.xml, minSdk), assinatura de app (keystore), ProGuard/R8.
  • iOS: CocoaPods/Swift Package Manager, Info.plist (permissões com textos de justificativa), certificados e provisioning profiles (o terror de todo dev multiplataforma — entenda o modelo: certificado + app ID + device list → profile).
  • Permissões em runtime: câmera, localização, notificações — pedir no momento certo, tratar negação e "não perguntar de novo".
  • Ler stack trace nativo de um crash (logcat / Xcode console) para saber pelo menos qual lib explodiu.
Mercado

“Já escrevi um module/canal nativo” é frase que muda entrevista de patamar. Projeto sugerido: um plugin simples (ex.: ler nível de bateria ou brilho da tela) publicado no pub.dev/npm — mostra domínio da fronteira nativa e de publicação de pacotes.

Capítulo 15 Ambos Avançado

Animações

Animação boa é a que roda na thread de UI sem depender do seu código a cada frame. Os dois frameworks têm um caminho fácil (implícito/declarativo) e um caminho poderoso (controle total).

Flutter
// Implícita: mudou o valor, o widget anima sozinho
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeOut,
  width: expandido ? 300 : 120,
  decoration: BoxDecoration(
    color: expandido ? Colors.teal : Colors.blue,
    borderRadius: BorderRadius.circular(
        expandido ? 24 : 8)),
)

// Explícita: AnimationController p/ orquestrar
late final _ctrl = AnimationController(
  vsync: this, duration: const Duration(seconds: 1))
  ..repeat(reverse: true);
// + Tween, CurvedAnimation, AnimatedBuilder

// Hero: transição de elemento entre telas
Hero(tag: 'produto-42', child: Image.network(url))
React Native — Reanimated
const largura = useSharedValue(120);

const estilo = useAnimatedStyle(() => ({
  width: withSpring(largura.value),
  // worklet: roda na thread de UI,
  // não trava mesmo com JS ocupado
}));

<Animated.View style={[s.caixa, estilo]} />
<Button onPress={() => largura.value = 300} />

// Gestos + animação (arrastar card):
const gesto = Gesture.Pan()
  .onChange(e => { x.value += e.changeX; })
  .onEnd(() => { x.value = withSpring(0); });

Reanimated + Gesture Handler é o par obrigatório. Layout Animations e SharedTransition cobrem transições entre telas. Lottie roda animações do After Effects nos dois frameworks.

  • Regra dos 300ms: microinterações entre 150–350 ms com curvas de easing; linear parece robótico.
  • Anime propriedades baratas: transform (translate/scale/rotate) e opacity; animar layout (width/height que empurram vizinhos) custa caro.
  • Respeite acessibilidade: reduza/desligue animações quando o sistema pedir (reduce motion).
Capítulo 16 Ambos Avançado

Segurança mobile

Premissa de segurança mobile: o aparelho é território hostil. O binário pode ser descompilado, o tráfego interceptado, o storage lido em aparelho com root. Sua defesa é em camadas:

  • Nunca embuta segredos no app. Chave de API no código = chave pública. Segredos ficam no backend; o app recebe tokens de sessão curtos.
  • Storage: tokens em Keychain (iOS) / Keystore (Android) via flutter_secure_storage / expo-secure-store — jamais em prefs/AsyncStorage.
  • Rede: HTTPS sempre; para apps financeiros, certificate pinning (fixar o certificado esperado e recusar MITM) — saiba explicar o trade-off: pinning mal feito quebra o app quando o certificado rota.
  • Autenticação: OAuth2/OIDC com PKCE para login social/corporativo; refresh token com rotação; biometria (local_auth / expo-local-authentication) protegendo a reativação da sessão.
  • Código: ofuscação (flutter build --obfuscate --split-debug-info=...; Hermes bytecode + ProGuard/R8 no RN), detecção de root/jailbreak quando o negócio exigir (ciente de que é mitigação, não garantia).
  • Validação dupla: valide no app para UX, valide de novo no backend para segurança — regra de negócio no cliente é sugestão, não lei.
  • Dados pessoais e LGPD: colete o mínimo, explique o uso (textos de permissão), permita exclusão de conta (exigência das lojas), criptografe dados sensíveis em repouso.
Top 3 falhas encontradas em apps reais

1) Token eterno em storage não criptografado. 2) Endpoints “escondidos” sem autorização no servidor (segurança por obscuridade). 3) Logs de produção vazando dados pessoais/tokens (limpe seus print/console.log em release).

Mercado

Fintechs e healthtechs — os setores que mais pagam mobile no Brasil — perguntam segurança em entrevista: OWASP MASVS (o padrão de verificação de segurança mobile), pinning, armazenamento seguro e fluxo OAuth com PKCE. Estude o checklist do MASVS uma vez e você estará à frente da maioria.

Capítulo 17 Ambos Muito avançado

Internals: como os frameworks funcionam por dentro

Este capítulo é o que separa candidatos sênior: entender por que as regras dos capítulos anteriores existem. É também o material das perguntas mais difíceis de entrevista.

Flutter: as três árvores

Quando você escreve widgets, o Flutter mantém três árvores paralelas:

  1. Widget tree — sua configuração, imutável e barata. Recriada a cada build sem culpa.
  2. Element tree — a instância viva que liga widget a posição na árvore; guarda o State. É ela que decide, no rebuild, se reutiliza ou recria (comparando runtimeType e key — por isso keys importam em listas!).
  3. RenderObject tree — quem faz layout e pintura, cara de criar e por isso reaproveitada ao máximo.

Pipeline de frame: animações → build (widgets sujos) → layout (uma passada, constraints descem, tamanhos sobem) → paint → composição de camadas → engine Impeller rasteriza na GPU. Dart compila AOT para código nativo em release (rápido, sem JIT) e JIT em debug (é isso que torna o hot reload possível).

// Por que const importa, explicado pelas árvores:
// const Widget é canonicalizado — o Element vê que é
// LITERALMENTE o mesmo objeto e pula o rebuild da subárvore.

// Por que key importa: numa lista reordenada, sem key o
// Element casa por posição e "veste" o State errado no item.

React Native: da bridge à Nova Arquitetura

  1. Arquitetura clássica (bridge): JS e nativo eram dois mundos trocando mensagens JSON assíncronas serializadas em lote. Funcionava, mas: custo de serialização, impossibilidade de chamadas síncronas, e "engarrafamento" na ponte sob carga.
  2. JSI (JavaScript Interface): camada C++ que permite ao JS segurar referências diretas a objetos nativos (HostObjects) e chamá-los de forma síncrona. A bridge deixou de ser necessária ("bridgeless").
  3. Turbo Modules: módulos nativos carregados sob demanda (lazy) e expostos via JSI, com interfaces geradas por Codegen a partir de specs TypeScript — tipagem de ponta a ponta.
  4. Fabric: o novo renderer. A árvore de UI ("shadow tree") vive em C++, permitindo renderização concorrente, layout síncrono quando necessário, e integração com as features do React 18 (Suspense, transitions).
  5. Hermes: engine JS da Meta que pré-compila para bytecode → startup rápido e menos memória (crucial em Android modesto).
// O ciclo de renderização com Fabric:
// 1. React reconcilia o virtual DOM (thread JS)
// 2. Fabric cria/atualiza a Shadow Tree (C++)
// 3. Yoga calcula layout (flexbox em C++)
// 4. Mounting: views nativas reais são criadas/atualizadas
//    na thread de UI — agora possível de forma síncrona

A comparação que resume tudo

FlutterReact Native
Quem desenha o pixelA própria engine (Impeller) — widgets são desenhados, não traduzidosO sistema operacional — componentes viram views nativas reais
Linguagem em produçãoDart AOT → código de máquinaJS (Hermes bytecode) + nativo, unidos por JSI/C++
Consequência práticaUI idêntica em todo lugar; atualizações do SO não mudam seu appUI evolui com o SO; interop com o ecossistema nativo é mais direta
Custo escondidoBinário maior; ecossistema Dart menorComplexidade da camada de interop; duas linguagens no stack (JS + nativo)
Mercado

Perguntas reais de entrevista sênior: "explique as três árvores do Flutter", "o que a Nova Arquitetura do RN resolve em relação à bridge?", "por que const/keys/memo funcionam?". Quem responde com o mecanismo (e não com regra decorada) sinaliza senioridade imediatamente.

Capítulo 18 Ambos Muito avançado

Concorrência e paralelismo

Regra de ouro dos dois mundos: a thread de UI é sagrada. Async/await resolve espera (rede, disco), mas não resolve CPU pesada (parsear um JSON de 10 MB, processar imagem, criptografia) — isso trava o frame do mesmo jeito, porque roda na mesma thread.

Flutter — Isolates
// Isolate: "thread" sem memória compartilhada;
// comunica por mensagens (sem race conditions)

// Jeito simples — computação one-shot:
final relatorio = await Isolate.run(() {
  return processarPesado(dadosGigantes);
});

// compute() é o atalho clássico p/ parse de JSON:
final lista = await compute(parseProdutos, jsonBruto);

// Isolates de longa duração: Isolate.spawn +
// SendPort/ReceivePort para pipelines contínuos

Sem memória compartilhada = sem locks, sem deadlocks. O custo: dados são copiados entre isolates (objetos muito grandes têm custo de transferência; TypedData pode ser transferido sem cópia).

React Native — threads do runtime
// RN já é multi-thread por natureza:
// - Thread JS (sua lógica React)
// - Thread de UI nativa
// - Threads de background nativas

// CPU pesada em JS trava a thread JS →
// UI nativa continua, mas toques/lógica congelam.

// Opções:
// 1. Worklets (Reanimated/react-native-worklets):
//    funções que rodam em outra thread/runtime JS
const resultado = await runOnBackground(() => {
  'worklet';
  return processarPesado(dados);
});

// 2. Empurrar p/ nativo: Turbo Module que processa
//    em thread própria e resolve uma Promise
// 3. Dividir o trabalho em fatias (chunking) com
//    InteractionManager / yields para não bloquear

Padrões que você deve dominar

  • Debounce e throttle: busca-enquanto-digita não pode disparar uma request por tecla.
  • Corrida de requisições: usuário digitou "ab" e depois "abc" — a resposta de "ab" pode chegar depois e sobrescrever a certa. Solução: cancelar a anterior ou descartar respostas obsoletas (TanStack Query e Riverpod fazem isso por você — saiba explicar como).
  • Trabalho em segundo plano com app fechado: outra categoria — WorkManager/BGTaskScheduler via workmanager (Flutter) / expo-background-task e afins (RN). O SO decide quando rodar; nunca prometa execução exata.
  • Push notifications (FCM/APNs) como gatilho de sincronização silenciosa.
Mercado

Pergunta-armadilha comum: "async/await já roda em outra thread, certo?"não. Nos dois frameworks o modelo é event loop numa thread só; await apenas cede a vez. Explicar isso corretamente (e quando usar Isolate/worklet) é sinal claro de nível pleno para cima.

Capítulo 19 Ambos Muito avançado

Apps em escala: times grandes, um só código

Um app com 40 devs tem problemas que tutorial nenhum mostra: build lento, conflitos de merge, features pisando umas nas outras, marcas diferentes no mesmo código. As soluções:

Modularização e monorepo

  • Flutter: quebrar o app em packages Dart locais (packages/design_system, packages/feature_auth…) orquestrados por Melos/workspaces do pub. Cada pacote tem seus testes e pode ser buildado isolado.
  • React Native: monorepo com pnpm/yarn workspaces + Turborepo/Nx; pacotes compartilhados entre app mobile e web (o superpoder do RN: packages/core usado pelo site React e pelo app).
  • Regra de dependência entre módulos: features não importam features; ambas importam core/design system. Ferramentas de lint podem impor isso.

Design system interno

Times maduros não usam Colors.blue ou hex solto: têm um pacote de design system com tokens (cores, espaçamentos, tipografia) e componentes prontos (AppButton, AppCard), documentados num catálogo vivo — Widgetbook (Flutter) / Storybook (RN). Isso garante consistência e acelera todo mundo.

White-label e ambientes

  • Flavors (Flutter) / variantes+schemes ou app.config dinâmico (RN/Expo): o mesmo código gera apps diferentes — dev/staging/prod com APIs distintas, ou marcas distintas (white-label: mesmo app de banco para 5 bancos parceiros, mudando tema, ícone e configuração).
  • Configuração por ambiente via --dart-define / variáveis de ambiente do EAS — nunca hardcoded.

Qualidade contínua

  • Feature flags (Firebase Remote Config, LaunchDarkly): lançar código desligado, ativar gradualmente, desligar sem novo release — essencial porque release mobile passa por revisão de loja e não tem rollback instantâneo.
  • Observabilidade: Crashlytics/Sentry (crashes com stack simbolizado), analytics de produto, monitoramento de performance (tempo de startup, ANRs, taxa de crash-free users — a métrica que gerência acompanha; padrões saudáveis ficam acima de 99,5%).
  • Lint e padronização impostos por máquina: very_good_analysis/custom lints (Flutter), ESLint+Prettier+TS strict (RN), hooks de pre-commit, CI que bloqueia merge sem testes.
Mercado

Entrevista de system design mobile (sênior): "desenhe a arquitetura de um app de banco com 6 squads". Estrutura de resposta: monorepo modularizado por feature + design system + navegação desacoplada (cada squad dona de suas rotas) + feature flags + CI por módulo + observabilidade. Pratique desenhar isso em 15 minutos num quadro.

Capítulo 20 Ambos Muito avançado

CI/CD, lojas e atualizações OTA

Automatizar o caminho commit → testes → build assinado → loja é responsabilidade de dev mobile sênior (não existe "equipe de deploy" para o seu app na maioria das empresas).

O pipeline típico

# GitHub Actions — esqueleto de CI mobile (conceito igual p/ os dois)
on: [pull_request]
jobs:
  qualidade:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: flutter pub get        # ou: npm ci
      - run: flutter analyze        # ou: npm run lint && tsc --noEmit
      - run: flutter test --coverage  # ou: npm test -- --coverage
  build-android:
    needs: qualidade
    steps:
      - run: flutter build appbundle --release
        # assinatura via secrets (keystore em base64)
        # RN: ./gradlew bundleRelease ou eas build -p android
  • Ferramentas de mercado: GitHub Actions (universal), Codemagic (especializada em Flutter), EAS Build/Submit (padrão do ecossistema Expo — builds iOS na nuvem sem você ter Mac), Bitrise, Fastlane (automação de assinatura, screenshots e upload — o "canivete" clássico dos dois mundos, com match resolvendo certificados iOS em time).
  • Distribuição interna antes da loja: Firebase App Distribution / TestFlight para o time e beta testers.

Publicação nas lojas

Google PlayApp Store
ContaTaxa única de US$ 25US$ 99/ano
ArtefatoAAB assinado (Play App Signing)IPA via Xcode/Transporter (certificados + profiles)
RevisãoHoras a poucos dias; faixas de teste interno/fechado/abertoEm geral ~1–2 dias; revisão humana mais rigorosa (guideline 4.x de design, login de teste obrigatório)
Lançamento gradualStaged rollout por porcentagemPhased release em 7 dias
Exigências recorrentesTarget SDK atualizado anualmente, data safety formPrivacy nutrition labels, exclusão de conta no app, ATT para tracking

Atualizações OTA (over-the-air)

  • React Native: EAS Update — publica o bundle JS novo direto para os usuários, sem passar pela revisão da loja (dentro das regras: só código interpretado, sem mudar o propósito do app). Ciclo de correção de bug em minutos.
  • Flutter: Shorebird — code push para Dart, atualizando o código compilado com patches. Mais novo, adotado crescentemente.
  • Estratégia madura: OTA para hotfixes e ajustes; release de loja para mudanças nativas e features grandes. Sempre com possibilidade de rollback do update.
Dores reais que você deve saber resolver

Certificado iOS expirado quebrando o build da madrugada; keystore Android perdida (guarde com carinho — sem ela você não atualiza mais o app, embora o Play App Signing mitigue); versão nativa incompatível com update OTA (versionamento de runtime); rejeição da Apple por falta de conta de teste.

Mercado

“Montou CI/CD do zero” é conquista de destaque em currículo pleno/sênior. Faça no seu projeto de portfólio: Actions rodando lint+testes em PR, build assinado, distribuição no Firebase App Distribution e badge de status no README. Custa um fim de semana e é citado em toda entrevista.

Capítulo 21 Mercado Carreira

Roadmap de carreira: de zero à senioridade

Fase 1 · FundamentosCaps. 02–06

Linguagem + UI + listas/forms. Meta: clonar 3 telas de apps famosos e um CRUD completo com os 4 estados de tela.

Fase 2 · Nível vaga júniorCaps. 07–10

Navegação, estado, API real, persistência. Meta: app completo consumindo API pública, publicado na Play Store.

Fase 3 · Nível plenoCaps. 11–16

Arquitetura, testes, performance, um módulo nativo. Meta: refatorar seu app com camadas + suíte de testes + README técnico.

Fase 4 · Nível sêniorCaps. 17–20

Internals, escala, CI/CD, OTA. Meta: pipeline completo, monorepo modularizado, respostas de system design.

O que cada senioridade significa na prática (mobile BR)

NívelO que se esperaO que te trava aí
JúniorEntrega telas e features guiadas; consome APIs; usa o gerenciador de estado do time; escreve teste com ajudaNão tratar erro/estado vazio; não saber ler stack trace; depender de tutorial para tudo
PlenoEntrega features de ponta a ponta sozinho; decide entre abordagens; testa por hábito; investiga performance; mexe no nativo com supervisãoSó saber "como", nunca "por quê"; não conseguir justificar trade-offs
SêniorDesenha arquitetura e navegação do app; monta CI/CD; destrava os outros; conversa com produto sobre custo/prazo; domina internals e o lado nativoNão comunicar bem; não mentorar; visão só técnica sem visão de produto

Habilidades que multiplicam o salário mobile

  • Inglês técnico — vagas remotas internacionais em Flutter/RN pagam em dólar/euro e são abundantes; é o maior multiplicador isolado de salário.
  • O outro framework — você já está estudando os dois; consultorias adoram devs "bilíngues" de mobile.
  • Fundamentos nativos (cap. 14) — o dev multiplataforma que resolve o problema no Kotlin/Swift é raro e valioso.
  • Backend básico (Node/Firebase/Supabase) — para MVPs, freelas e entender a API do outro lado.
  • Comunicação escrita — READMEs, PRs bem descritos, ADRs (registros de decisão). Em time remoto, você é o que você escreve.
Onde procurar vagas

LinkedIn (configure alertas para "Flutter" e "React Native" + remoto), Gupy e Programathor (BR), remotar.com.br, e para internacionais: wellfound, RemoteOK, Toptal/Turing (freela/contractor). Comunidades também empregam: Flutterando, React Brasil (Telegram/Discord) têm canais de vagas ativíssimos — participe antes de precisar.

Capítulo 22 Mercado Carreira

Portfólio que contrata

Recrutador técnico gasta 3 minutos no seu GitHub. Ele não vai rodar seu app: vai ler o README, olhar a organização das pastas e bater o olho em 2 arquivos. Otimize para isso.

Os 3 projetos ideais (em vez de 15 clones de tutorial)

  1. O completo (nível júnior): app de catálogo/clima/filmes consumindo API pública — lista + detalhe + busca, os 4 estados de tela, tema claro/escuro, e publicado na Play Store (o link da loja no currículo vale ouro; a conta custa US$ 25 uma vez).
  2. O arquitetado (nível pleno): um domínio com regra de negócio de verdade (controle de gastos, hábitos, estoque): camadas separadas, gerenciador de estado justificado, testes de unidade + widget/componente, CI com badge verde, offline básico.
  3. O diferenciado (destaque): algo que poucos têm — um módulo/plugin nativo publicado no pub.dev/npm, um app offline-first com fila de sync, ou o mesmo produto feito nos dois frameworks com um comparativo escrito (isso rende artigo, e artigo rende visibilidade).

Anatomia do README que passa no filtro

# NomeDoApp
Uma frase do que o app faz e por quê.

## Demo
[GIF de 10s do app funcionando]  ← indispensável
[Link na Play Store] [APK de download]

## Decisões técnicas
- Estado: Riverpod — por quê (2 linhas)
- Arquitetura: feature-first em 3 camadas [diagrama simples]
- Testes: unit (domínio) + widget (telas críticas)

## Como rodar
3 comandos, no máximo.

## O que eu faria com mais tempo
← honestidade técnica impressiona sênior
O que derruba portfólio

README vazio ou gerado; commits "fix", "aa", "final2"; código comentado morto; segredos commitados (API keys!); 30 repositórios de exercício poluindo o perfil (arquive-os e fixe os 3 bons).

LinkedIn e presença

Título claro ("Desenvolvedor Mobile | Flutter | React Native"), seção "Projetos" com os links, e 1 post curto por projeto explicando uma decisão técnica. Publicar o que você aprende — mesmo básico — coloca você na frente de 95% dos candidatos silenciosos e faz recrutadores chegarem até você.

Capítulo 23 Mercado Carreira

Entrevistas e desafios técnicos

Processo típico de vaga mobile BR: triagem com recrutador → entrevista técnica (perguntas conceituais) → desafio prático (take-home ou live coding) → conversa final com liderança. Prepare cada etapa:

Banco de perguntas reais (com o núcleo da resposta)

StatelessWidget vs StatefulWidget / componente com e sem estado — quando usar?

Stateless/sem estado: a UI depende só dos parâmetros recebidos. Stateful/useState: há dado interno que muda com o tempo e deve redesenhar a tela. Bônus sênior: no Flutter, estado efêmero fica no widget; estado de app vai para Riverpod/Bloc — e citar que "elevar estado" cedo demais gera acoplamento.

O que acontece quando você chama setState / atualiza um estado?

O framework marca o widget/componente como sujo e agenda re-render. No próximo frame, build/render roda de novo, o resultado é comparado com o anterior (diffing/reconciliação) e só as diferenças chegam à camada de renderização. Por isso mutação silenciosa (sem criar objeto novo) não atualiza a UI.

Para que servem as keys em listas?

Identificar itens de forma estável quando a lista muda de ordem. Sem key, o framework casa por posição e pode "vestir" estado de um item no outro (checkbox marcado que pula de linha). Flutter: o Element compara runtimeType+key; React: mesma ideia na reconciliação.

Como você faria autenticação com refresh token?

Access token curto em memória/secure storage; refresh token em secure storage. Interceptor detecta 401, chama o endpoint de refresh (serializando para não refreshar em paralelo), repõe o token e repete a requisição original; se o refresh falhar, desloga. Bônus: PKCE no fluxo OAuth e rotação de refresh token.

App lento ao rolar a lista — como investiga?

Método do capítulo 13: reproduzir em release/profile em aparelho médio → profiler → identificar o gargalo (rebuild/re-render excessivo? imagem decodificando grande? trabalho síncrono na thread de UI?) → corrigir a causa (const/memo/seletores, cache de imagem com resize, mover CPU pesada para isolate/worklet, FlashList/itens estáveis) → medir de novo.

Flutter renderiza como? / O que a Nova Arquitetura do RN mudou?

Flutter: três árvores (widget/element/render), layout em passada única de constraints, Impeller rasterizando na GPU, Dart AOT em release. RN: JSI substituiu a bridge JSON assíncrona por referências diretas C++ (chamadas síncronas), Turbo Modules com carregamento lazy e codegen tipado, Fabric com shadow tree em C++ e renderização concorrente, Hermes para startup.

Como estruturaria um app para 5 squads?

Capítulo 19 inteiro: monorepo modularizado por feature, design system com tokens, contratos entre módulos, navegação desacoplada, feature flags, CI por módulo, observabilidade com crash-free como métrica. Desenhe o diagrama enquanto fala.

Por que multiplataforma e não nativo? (pergunta-armadilha)

Resposta madura reconhece trade-offs: multiplataforma dobra a velocidade de entrega e unifica o time, ao custo de uma camada de abstração e casos raros de API específica exigirem código nativo; nativo maximiza integração e controle fino ao custo de dois codebases. A escolha é de negócio (time, prazo, produto), não religião. Quem responde "porque é melhor" perde pontos.

Take-home: como se destacar no desafio de casa

  • Leia o enunciado 2× e entregue exatamente o que foi pedido antes de qualquer extra.
  • Commits pequenos com mensagens claras (o avaliador lê o histórico!).
  • Camadas separadas + os 4 estados de tela + validação — o kit básico do capítulo 06/11.
  • 3 a 6 testes bem escolhidos > cobertura cega.
  • README com decisões, instruções de 3 comandos e "o que faria com mais tempo".
  • Não estoure o prazo; se precisar cortar escopo, corte e documente o corte.

Live coding: o que estão avaliando

Não é velocidade: é raciocínio falado. Verbalize o plano antes de digitar, trate o entrevistador como colega (pergunte requisitos!), escreva o caso simples primeiro e refine, e quando travar diga o que tentaria — silêncio é o único jeito de reprovar de verdade. Treine compartilhando tela com um amigo resolvendo os exercícios do capítulo 03.

Negociação

Pesquise faixas antes (Glassdoor, levantamentos de comunidades como Flutterando/React Brasil e pesquisas salariais de tech BR — os valores mudam, pesquise na semana da conversa). Nunca dê o primeiro número se puder evitar; se pressionado, dê uma faixa ancorada no topo da sua pesquisa. CLT × PJ: PJ costuma pagar nominalmente mais, mas desconte férias, 13º e benefícios na comparação.

Capítulo 24 Ambos Encerramento

Checklist final e recursos para continuar

Checklist de empregabilidade

  • Domino uma trilha (Flutter ou RN) até o cap. 16 e sei conversar sobre a outra.
  • Tenho 3 projetos de portfólio com README forte e GIF de demo.
  • Pelo menos 1 app publicado na Play Store (link no currículo).
  • Sei responder de cabeça as perguntas do capítulo 23.
  • Tenho suíte de testes e CI verde em pelo menos 1 projeto.
  • LinkedIn atualizado com título claro e posts sobre os projetos.
  • Participo de 1 comunidade (Flutterando / React Brasil) e acompanho o canal de vagas.
  • Fiz ao menos 1 entrevista simulada (com amigo ou gravando a si mesmo).

Fontes oficiais e de alta qualidade

  • Flutter: docs.flutter.dev (a melhor documentação oficial do mercado — faça os codelabs), dart.dev, canal Flutter no YouTube ("Widget of the Week"), pub.dev.
  • React Native: reactnative.dev, docs.expo.dev (leia inteiro — é o caminho moderno), react.dev (fundamentos de React valem para RN), TanStack Query docs.
  • Comunidades BR: Flutterando (a maior comunidade Flutter em português do mundo), React Brasil, meetups locais e eventos (Flutter Connection, React Conf Brasil).
  • Prática deliberada: reconstruir telas de apps reais; contribuir com issues "good first issue" de libs que você usa; escrever sobre o que aprendeu.

Plano de 90 dias sugerido

Dias 1–30Caps. 02–06 na trilha escolhida. Projeto 1 começado. 1h/dia de código, sem pular para vídeos infinitos.
Dias 31–60Caps. 07–11. Projeto 1 publicado na Play Store. Projeto 2 (arquitetado) iniciado com testes desde o dia 1.
Dias 61–90Caps. 12–16 + CI. LinkedIn e README polidos. Começar a aplicar para vagas — aplicar também é treino.

Última dica, e talvez a mais importante: o mercado não contrata quem "terminou de estudar" — contrata quem constrói e mostra. Termine coisas pequenas, publique, escreva sobre elas, repita. Boa jornada. 🚀