Noções Básicas de Arquitetura Mobile
10 exemplos para você começar com arquitetura mobile - 7 básicos e 3 intermediários.
Busque em todas as páginas da documentação
10 exemplos para você começar com arquitetura mobile - 7 básicos e 3 intermediários.
Crie um scaffold de um app Expo com formato de produção e um pin explícito do SDK 57. O template default@sdk-57 vem com Expo Router, TypeScript e o layout de pastas recomendado para estender.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npm installAdicione uma árvore src/ ao lado de app/ para os limites das features:
MyApp/
├── app/ # Expo Router - apenas rotas
├── src/
│ ├── shared/ # Primitivas de UI, cliente de API, configuração
│ ├── entities/ # modelos de domínio (User, Order)
│ └── features/ # fatias de produto (auth, orders, settings)
├── app.config.ts
└── package.jsonConfirme o pin do SDK antes de estruturar as features:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Ferramentas: Estes exemplos visam Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3.
Aplicativos mobile se beneficiam de um fluxo de dependência simples para dentro: apresentação (telas, componentes) chama domínio (entidades, casos de uso), que chama dados (adaptadores de API, armazenamento).
┌─────────────────────────────────────────┐
│ app/ + features/*/screens, components │ ← apresentação
├─────────────────────────────────────────┤
│ entities/ + features/*/model, hooks │ ← domínio
├─────────────────────────────────────────┤
│ shared/api, shared/storage, adapters │ ← dados / infra
└─────────────────────────────────────────┘
direção da dependência: ↓ para dentro// src/features/orders/screens/OrdersScreen.tsx - apresentação orquestra
import { useOrders } from "../hooks/useOrders";
import { OrderList } from "../components/OrderList";
export function OrdersScreen() {
const { orders, loading, error, refresh } = useOrders();
return <OrderList orders={orders} loading={loading} error={error} onRefresh={refresh} />;
}fetch diretamente - ela chama hooks ou casos de uso que escondem IOOrder, User) vivem em entities/ para que múltiplas features compartilhem uma formaordersApi.ts, secureStorage.ts) ficam em shared/ ou features/*/api/OrderRow de apresentação) torna os testes unitários dolorososRelacionado: Feature-Sliced Design para RN - camadas de fatia formais adaptadas para Expo | Clean Architecture em Mobile - entidades e casos de uso sem cerimônia
Expo Router mapeia nomes de arquivos para URLs. Arquivos de rota devem reexportar telas de features - não possuir lógica de negócio.
// app/(tabs)/orders/index.tsx - uma linha quando possível
export { OrdersScreen as default } from "@/features/orders";// src/features/orders/index.ts - API pública para a feature
export { OrdersScreen } from "./screens/OrdersScreen";
export type { Order } from "./model/types";app/ é infraestrutura de navegação - no máximo, análise de parâmetros e aninhamento de layout pertencem aquifeatures/orders/index.ts é o único caminho de importação que outras features devem usar@/features/orders/components/OrderRow acoplam consumidores a refatorações internasRelacionado: ../project-setup/folder-structure-for-features/folder-structure-for-features.md - layout de fatia de feature em bases de código crescentes
shared/ contém código sem opinião de produto. Pastas de features contêm UI e fluxos de trabalho específicos do produto.
src/
├── shared/
│ ├── ui/ # Button, Screen, TextField - primitivas do design system
│ ├── api/ # createApiClient(), tipos de erro
│ ├── config/ # leitores de env (sem segredos no código fonte)
│ └── lib/ # formatCurrency, helpers de data
└── features/
└── checkout/
├── components/ # PaymentSummary - específico do checkout
└── hooks/ # useCheckout - fluxo de trabalho do checkout// src/shared/ui/Button.tsx - primitiva, sem conhecimento de domínio
import { Pressable, StyleSheet, Text, type PressableProps } from "react-native";
type ButtonProps = PressableProps & { label: string };
export function Button({ label, style, ...rest }: ButtonProps) {
return (
<Pressable style={[styles.base, style]} {...rest}>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: { paddingVertical: 12, paddingHorizontal: 20, borderRadius: 10, backgroundColor: "#2563eb" },
label: { fontSize: 16, fontWeight: "600", color: "#fff" },
});shared/ quando duas features não relacionadas precisarem dele e ele não tiver semântica de produtofeatures/checkout/ mesmo que apenas uma tela a use hojeshared/ui não deve importar de features/ - a dependência flui em uma direçãocomponents/ no nível raiz - pastas sem dono se tornam ímãs de módulos divinosRelacionado: Feature-Sliced Design para RN - regras da camada
sharede nomenclatura de segmentos
Entidades descrevem substantivos em seu domínio - tipos puros e funções puras sem imports de React.
// src/entities/order/model/types.ts
export type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled";
export type Order = {
id: string;
title: string;
totalCents: number;
status: OrderStatus;
placedAt: string;
};// src/entities/order/lib/formatOrderTotal.ts
import type { Order } from "../model/types";
export function formatOrderTotal(order: Order): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(order.totalCents / 100);
}features/orders e features/tracking ambas usam OrderuseState, sem fetch, sem StyleSheet em entities/ - os mantém testáveis sem um renderizadormodel/, lib/, api/ quando a entidade crescer para cerca de ~200 linhasRelacionado: Clean Architecture em Mobile - quando adicionar casos de uso em torno de entidades
Cada pasta de feature expõe um "barril" index.ts. Imports entre features passam apenas por esse arquivo.
// src/features/orders/index.ts
export { OrdersScreen } from "./screens/OrdersScreen";
export { OrderRow } from "./components/OrderRow";
export { useOrders } from "./hooks/useOrders";
export type { OrdersFilter } from "./model/types";// src/features/dashboard/screens/DashboardScreen.tsx - importa apenas a API pública
import { OrderRow } from "@/features/orders";
import type { Order } from "@/entities/order";normalizeRawOrder) a menos que outra feature realmente precise delesno-restricted-imports pode impor @/features/*/index e bloquear @/features/*/components/*Relacionado: Checklist de Refatoração - itens de auditoria para violações de limites antes do lançamento
IO de rede e armazenamento vivem atrás de adaptadores para que as telas troquem implementações (mock, staging, prod) sem edições.
// src/shared/api/createApiClient.ts
type ApiClient = {
get<T>(path: string): Promise<T>;
post<T>(path: string, body: unknown): Promise<T>;
};
export function createApiClient(baseUrl: string): ApiClient {
return {
async get<T>(path: string) {
const response = await fetch(`${baseUrl}${path}`);
if (!response.ok) throw new Error(`GET ${path} failed: ${response.status}`);
return response.json() as Promise<T>;
},
async post<T>(path: string, body: unknown) {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`POST ${path} failed: ${response.status}`);
return response.json() as Promise<T>;
},
};
}// src/features/orders/api/ordersApi.ts
import type { Order } from "@/entities/order";
import type { ApiClient } from "@/shared/api/createApiClient";
export function createOrdersApi(client: ApiClient) {
return {
list: () => client.get<Order[]>("/orders"),
byId: (id: string) => client.get<Order>(`/orders/${id}`),
};
}createOrdersApi) aceitam dependências - fáceis de injetar mocks em testesfetch brutos não devem aparecer em componentes de telaordersApi, authApi) é melhor que um único api.ts divinoRelacionado: Injeção de Dependência em RN - serviços injetáveis para duplos de teste
A configuração em tempo de execução flui através de um único leitor - não chamadas process.env espalhadas em código de feature.
// src/shared/config/env.ts
import Constants from "expo-constants";
type AppConfig = {
apiBaseUrl: string;
appVariant: "development" | "staging" | "production";
};
export function getAppConfig(): AppConfig {
const extra = Constants.expoConfig?.extra as Partial<AppConfig> | undefined;
return {
apiBaseUrl: extra?.apiBaseUrl ?? "https://api.staging.example.com",
appVariant: extra?.appVariant ?? "development",
};
}// app.config.ts (excerto)
export default {
extra: {
apiBaseUrl: process.env.EXPO_PUBLIC_API_URL,
appVariant: process.env.APP_VARIANT ?? "development",
},
};EXPO_PUBLIC_* são embutidas no bundle JS - nunca coloque segredos nelasgetAppConfig() é o único módulo que lê Constants.expoConfig?.extra para configurações do appgetAppConfig() ou recebem configuração via contexto - elas não leem o env diretamenteenv por canal - veja guias de configuração de ambiente do projetoRelacionado: Monolito Modular vs Multi-App - um binário com variantes vs apps separados
Features não devem importar internas umas das outras. Use entidades compartilhadas, eventos ou orquestração na camada do app.
// src/features/cart/hooks/useCart.ts - carrinho possui seu estado
import { create } from "zustand";
type CartItem = { productId: string; qty: number };
type CartStore = {
items: CartItem[];
add: (item: CartItem) => void;
clear: () => void;
};
export const useCart = create<CartStore>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
clear: () => set({ items: [] }),
}));// app/(tabs)/catalog/[id].tsx - rota orquestra navegação + carrinho, não features importando features
import { useLocalSearchParams, router } from "expo-router";
import { ProductDetailScreen } from "@/features/catalog";
import { useCart } from "@/features/cart";
export default function ProductRoute() {
const { id } = useLocalSearchParams<{ id: string }>();
const add = useCart((s) => s.add);
return (
<ProductDetailScreen
productId={id ?? ""}
onAddToCart={(productId) => {
add({ productId, qty: 1 });
router.push("/(tabs)/cart");
}}
/>
);
}features/catalog ignorante da navegação do carrinhoonAddToCart) para as telas em vez de importar stores de features irmãsProduct, Order) vivem em entities/ - ambas as features importam essesshared/events enxuto - não imports diretos de storesRelacionado: ADR: Seleção de Gerenciamento de Estado - quando stores globais são justificados
A arquitetura compensa quando cada camada testa sem iniciar o app completo.
// src/features/orders/hooks/useOrders.test.ts
import { renderHook, waitFor } from "@testing-library/react-native";
import { useOrders } from "./useOrders";
import type { Order } from "@/entities/order";
const mockOrders: Order[] = [
{ id: "1", title: "Widget", totalCents: 999, status: "pending", placedAt: "2026-01-01" },
];
jest.mock("../api/ordersApi", () => ({
ordersApi: { list: jest.fn(() => Promise.resolve(mockOrders)) },
}));
it("loads orders on mount", async () => {
const { result } = renderHook(() => useOrders());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.orders).toHaveLength(1);
});// src/entities/order/lib/formatOrderTotal.test.ts - puro, sem renderizador
import { formatOrderTotal } from "./formatOrderTotal";
it("formats cents as USD", () => {
expect(
formatOrderTotal({ id: "1", title: "A", totalCents: 2500, status: "pending", placedAt: "" })
).toBe("$25.00");
});fetch globalmenteRelacionado: Injeção de Dependência em RN - serviços injetáveis para duplos de teste
Permaneça em um único repositório até que dois apps ou isolamento de CI forcem um pacote de workspace.
monorepo/
├── apps/
│ └── mobile/ # App Expo - importa @acme/orders
├── packages/
│ ├── orders/ # tipos de entidade + API compartilhados com admin web
│ └── ui/ # sistema de design
└── pnpm-workspace.yaml// apps/mobile/package.json
{
"dependencies": {
"@acme/orders": "workspace:*",
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}// packages/orders/src/index.ts
export type { Order, OrderStatus } from "./model/types";
export { formatOrderTotal } from "./lib/formatOrderTotal";npx expo-doctor após adicionar dependências de workspace - Metro deve resolver o symlinksrc/entities/ dentro do app está livre até que um segundo consumidor existaRelacionado: ../project-setup/shared-packages-and-metro-resolution/shared-packages-and-metro-resolution.md - pacotes Metro e de workspace
Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026