🎯 O desafio
Você vai receber um componente React Native que faz tudo num arquivo só — busca de dados, estado e UI misturados. Sua missão é refatorá-lo nas 3 camadas do MVVM que vimos nos slides 8 a 10, sem mudar o comportamento do app.
Não é construir tela nova (isso você já fez em UX) e não é escrever testes (isso é a aula 7). É reorganizar código para separar responsabilidades.
1Setup
Se já tem o app do grupo (Aventura), use-o e só crie as pastas. Senão, crie um projeto Expo do zero:
# Projeto novo (pule se já tem o app do grupo)
npx create-expo-app aventura-mvvm --template blank-typescript
cd aventura-mvvm
# Crie as pastas das 3 camadas
mkdir model viewmodels views
# Rode no celular com o Expo Go
npx expo start
2Ponto de partida — o código "ANTES" 😱
Copie este componente para o seu projeto (ex.: App.tsx ou views/EspetaculosScreen.tsx). Repare como o fetch, o estado e a UI estão todos no mesmo lugar:
// EspetaculosScreen.tsx — tudo num arquivo só 😱
import { useEffect, useState } from 'react';
import { FlatList, Text, View, ActivityIndicator } from 'react-native';
export default function EspetaculosScreen() {
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.aventura.app/espetaculos') // fetch dentro da View
.then(r => r.json())
.then(d => { setItems(d); setLoading(false); });
}, []);
if (loading) return <ActivityIndicator size="large" />;
return (
<FlatList
data={items}
keyExtractor={(i) => i.id}
renderItem={({ item }) => (
<View style={{ padding: 12 }}>
<Text>{item.nome} ★ {item.rating}</Text>
</View>
)}
/>
);
}
fetch para o app renderizar mesmo offline — depois é só mover ele para o Model:
const MOCK = [
{ id: '1', nome: 'O Auto da Compadecida', categoria: 'Teatro', rating: 4.8 },
{ id: '2', nome: 'Cirque Aventura', categoria: 'Circo', rating: 4.5 },
{ id: '3', nome: 'Sinfônica ao Pôr do Sol', categoria: 'Música', rating: 4.9 },
];
// troque o fetch por: Promise.resolve(MOCK)
3Sua missão — separe nas 3 camadas
Mova cada responsabilidade para o arquivo certo. Use estes stubs como ponto de partida (estão vazios de propósito — a implementação é o desafio):
// model/Espetaculo.ts
// Dados + acesso à API. SEM React, SEM UI.
export type Espetaculo = {
// TODO: id, nome, categoria, rating
};
export async function fetchEspetaculos(): Promise<Espetaculo[]> {
// TODO: mova o fetch da View para cá
// TODO: trate !response.ok lançando um Error
}
// viewmodels/useEspetaculosViewModel.ts
// Estado + ações da tela. SEM JSX.
import { useEffect, useState } from 'react';
import { Espetaculo, fetchEspetaculos } from '../model/Espetaculo';
export function useEspetaculosViewModel() {
// TODO: estado items, loading, error
// TODO: load() com try / catch / finally
// TODO: useEffect(() => { load(); }, [])
// TODO: return { items, loading, error, reload: load }
}
// views/EspetaculosScreen.tsx
// Só renderiza. Consome o hook do ViewModel.
import { FlatList, Text, View, ActivityIndicator } from 'react-native';
import { useEspetaculosViewModel } from '../viewmodels/useEspetaculosViewModel';
export default function EspetaculosScreen() {
// TODO: const { items, loading, error, reload } = useEspetaculosViewModel();
// TODO: trate loading e error
// TODO: FlatList — SEM fetch e SEM useEffect aqui!
}
fetch nem useEffect de dados. Se ela tem, a responsabilidade está na camada errada. A View só conhece o ViewModel; o ViewModel conhece o Model; o Model não conhece ninguém.
4Entrega — Merge Request individual
- Branch
feat/espetaculos-mvvm - Os 3 arquivos refatorados:
model/,viewmodels/,views/ - O app continua funcionando igual (com mock ou backend)
- Na descrição do MR, 1 frase respondendo: "o que ficou testável agora que não era antes?"
💡 Dicas
- Comece pelo Model: recorte o
fetche o tipo dos dados. É a camada mais independente. - Depois o ViewModel: traga
useState/useEffectpara o hook e exponhaitems,loading,error,reload. - Por último a View: ela deve ficar bem menor — só desestrutura o hook e renderiza.
- Pode usar IA para acelerar, mas entenda cada recorte: o objetivo é a separação, não a velocidade.
useEspetaculosViewModel que você isolar aqui é exatamente o que vamos testar com TDD na próxima aula — sem montar a UI. Hoje você o torna testável; lá escrevemos os testes antes do código.