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.
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+).
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.
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.
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.
| Flutter | React Native | |
|---|---|---|
| Criador | Google (2017) | Meta/Facebook (2015) |
| Linguagem | Dart | JavaScript / TypeScript |
| Como renderiza | Desenha cada pixel com engine própria (Impeller/Skia) — UI idêntica em qualquer aparelho | Traduz componentes para views nativas reais de cada plataforma — UI com "cara" do sistema |
| Ponto forte | Consistência visual, performance previsível, tooling excelente, hot reload muito estável | Ecossistema JavaScript gigante, reaproveita devs web/React, atualizações OTA maduras |
| Ponto fraco | Dart é usado quase só em Flutter; apps tendem a ficar maiores em tamanho | Camada de interop com nativo adiciona complexidade em apps muito grandes |
| Quem usa | Nubank, iFood (partes), BMW, Google Pay, Alibaba | Meta (Facebook/Instagram em partes), Shopify, Microsoft (Office/Teams em partes), Discord, PicPay |
| Além do mobile | Web, Windows, macOS, Linux, embarcados | Web (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ó.
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.
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).
- Baixe o SDK em
docs.flutter.deve adicioneflutter/binao PATH. - Instale Android Studio + plugin Flutter/Dart, ou use VS Code com a extensão Flutter.
- Rode
flutter doctor— ele diagnostica tudo que falta (licenças Android, Xcode etc.). Só siga em frente com tudo ✓. - Crie e rode:
flutter create meu_app && cd meu_app && flutter run.
Gerenciador de pacotes: pub (pubspec.yaml). Catálogo: pub.dev.
- Instale Node.js LTS (recomendado via
nvm). - Caminho recomendado hoje: Expo —
npx create-expo-app meu-app. Ele abstrai o build nativo no início. - Teste no celular com o app Expo Go (escaneia QR code) ou em emulador Android/simulador iOS.
- 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
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
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
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.
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.
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
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 ?.
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
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
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
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
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 };
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
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; }
}
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;
}
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.
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.
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),
),
);
}
}
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)
- A UI é uma função do estado.
build()/ o corpo do componente descrevem a tela para o estado atual. - Mudar estado dispara re-renderização.
setState(Flutter) esetContador(React) avisam o framework: "o estado mudou, redesenhe". - 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).
| Conceito | Flutter | React Native |
|---|---|---|
| Bloco de UI | Widget (tudo é widget: texto, padding, tema…) | Componente (função que retorna JSX) |
| Sem estado | StatelessWidget | Componente sem hooks de estado |
| Com estado | StatefulWidget + classe State | useState, useReducer |
| Dados do pai | Parâmetros do construtor | Props |
| Ciclo de vida | initState, didUpdateWidget, dispose | useEffect (montagem, atualização, limpeza) |
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.
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… | Flutter | React Native |
|---|---|---|
| Empilhar na vertical | Column | <View> (flexDirection padrão é column) |
| Lado a lado | Row | flexDirection: 'row' |
| Ocupar espaço restante | Expanded / Flexible (flex) | flex: 1 |
| Espaçamento interno | Padding(padding: EdgeInsets…) | padding / paddingHorizontal… |
| Sobrepor elementos | Stack + Positioned | position: 'absolute' |
| Centralizar | Center ou alinhamentos de Row/Column | alignItems + justifyContent: 'center' |
| Rolagem | SingleChildScrollView, ListView | ScrollView, FlatList |
| Área segura (notch) | SafeArea | SafeAreaView (safe-area-context) |
Um card de perfil, dos dois lados
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)),
],
)),
]),
)
<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:
ThemeDatacentraliza cores, tipografia e formas (Material 3 comColorScheme.fromSeed). Widgets Cupertino imitam iOS quando necessário. Acesse comTheme.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 viaPlatform.selectou arquivosComponent.ios.tsx/Component.android.tsx.
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.
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:
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),
);
},
)
<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
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'),
),
]),
)
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.
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.
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
- Estado local:
setState/useState. Para o que só interessa àquela tela (campo de texto, aba selecionada). Não pule para ferramentas pesadas sem precisar. - Estado elevado: subir o estado ao ancestral comum e passar para baixo (props / construtores). Funciona, mas gera "prop drilling" quando a árvore cresce.
- Estado global/injetado: um "lugar" observável fora da árvore, que as telas leem e escutam. Aqui entram as bibliotecas.
| Framework | Opções por popularidade no mercado | Quando usar |
|---|---|---|
| Flutter | Riverpod (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 Native | Zustand (leve, moderno), Redux Toolkit (padrão corporativo, muito legado), Context API (para temas/auth simples), Jotai/MobX | Zustand 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
// 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);
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())));
}
}
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).
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.
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.
| Flutter | React Native | |
|---|---|---|
| Cliente HTTP padrão de mercado | dio (interceptors, timeout, cancelamento); http para casos simples | axios ou fetch nativo; TanStack Query por cima para cache |
| JSON → modelo | fromJson manual ou geração com json_serializable/freezed | tipar com interface + validar com zod em runtime |
| Auth token | interceptor do dio adiciona header | interceptor do axios / wrapper do fetch |
@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
}
}
}
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 (
CancelTokenno dio;AbortController/limpeza do useEffect no RN).
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.
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:
| Necessidade | Flutter | React Native |
|---|---|---|
| Preferências simples (tema, flag de onboarding) | shared_preferences | AsyncStorage 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-first | drift (SQL tipado), Isar/Hive (NoSQL), sqflite | expo-sqlite, WatermelonDB (offline-first em escala), Realm |
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();
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).
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.
"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.
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
| Camada | Responsabilidade | Flutter (típico) | React Native (típico) |
|---|---|---|---|
| Apresentação | Telas, widgets/componentes e o estado de tela (view model) | Widgets + Riverpod/Bloc | Componentes + hooks customizados / Zustand |
| Domínio | Regras de negócio puras: entidades, casos de uso, contratos (interfaces) de repositório. Zero import de framework | Classes Dart puras | Funções/classes TS puras |
| Dados | Implementações: API (dio/axios), banco, cache. Converte DTOs ↔ entidades | Repositories + data sources | Services/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
// 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)));
// 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.
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".
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.
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ível | Flutter | React Native |
|---|---|---|
| Unitário (domínio, view models) | package:test + mocktail | Jest / 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/golden | golden tests (compara screenshots) | snapshot tests / ferramentas de screenshot |
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()));
});
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.
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.
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:
constem 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/cacheHeightpara 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.
- 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.
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.
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.
// 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.
// 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 viaAndroidManifest.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.
“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.
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).
// 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))
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).
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.
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).
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.
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:
- Widget tree — sua configuração, imutável e barata. Recriada a cada build sem culpa.
- 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 (comparandoruntimeTypeekey— por isso keys importam em listas!). - 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
- 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.
- 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").
- 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.
- 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).
- 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
| Flutter | React Native | |
|---|---|---|
| Quem desenha o pixel | A própria engine (Impeller) — widgets são desenhados, não traduzidos | O sistema operacional — componentes viram views nativas reais |
| Linguagem em produção | Dart AOT → código de máquina | JS (Hermes bytecode) + nativo, unidos por JSI/C++ |
| Consequência prática | UI idêntica em todo lugar; atualizações do SO não mudam seu app | UI evolui com o SO; interop com o ecossistema nativo é mais direta |
| Custo escondido | Binário maior; ecossistema Dart menor | Complexidade da camada de interop; duas linguagens no stack (JS + nativo) |
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.
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.
// 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).
// 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-taske afins (RN). O SO decide quando rodar; nunca prometa execução exata. - Push notifications (FCM/APNs) como gatilho de sincronização silenciosa.
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.
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/coreusado 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.
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.
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
matchresolvendo 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 Play | App Store | |
|---|---|---|
| Conta | Taxa única de US$ 25 | US$ 99/ano |
| Artefato | AAB assinado (Play App Signing) | IPA via Xcode/Transporter (certificados + profiles) |
| Revisão | Horas a poucos dias; faixas de teste interno/fechado/aberto | Em geral ~1–2 dias; revisão humana mais rigorosa (guideline 4.x de design, login de teste obrigatório) |
| Lançamento gradual | Staged rollout por porcentagem | Phased release em 7 dias |
| Exigências recorrentes | Target SDK atualizado anualmente, data safety form | Privacy 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.
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.
“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.
Roadmap de carreira: de zero à senioridade
Linguagem + UI + listas/forms. Meta: clonar 3 telas de apps famosos e um CRUD completo com os 4 estados de tela.
Navegação, estado, API real, persistência. Meta: app completo consumindo API pública, publicado na Play Store.
Arquitetura, testes, performance, um módulo nativo. Meta: refatorar seu app com camadas + suíte de testes + README técnico.
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ível | O que se espera | O que te trava aí |
|---|---|---|
| Júnior | Entrega telas e features guiadas; consome APIs; usa o gerenciador de estado do time; escreve teste com ajuda | Não tratar erro/estado vazio; não saber ler stack trace; depender de tutorial para tudo |
| Pleno | Entrega features de ponta a ponta sozinho; decide entre abordagens; testa por hábito; investiga performance; mexe no nativo com supervisão | Só saber "como", nunca "por quê"; não conseguir justificar trade-offs |
| Sênior | Desenha arquitetura e navegação do app; monta CI/CD; destrava os outros; conversa com produto sobre custo/prazo; domina internals e o lado nativo | Nã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.
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.
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)
- 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).
- 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.
- 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
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).
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ê.
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.
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.
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
Ú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. 🚀