Construindo apps iOS e Android com React Native e Expo. Do setup inicial ao deployment em produção. Lições aprendidas no Financial Manager.
Ednei Trabach
Desenvolvedor Full Stack
React Native permite escrever uma vez e rodar em iOS e Android. Expo simplifica drasticamente o setup e o processo de build, eliminando a necessidade de configurar Xcode e Android Studio localmente.
npx create-expo-app@latest financial-manager
cd financial-manager
npm start
financial-manager/
├── app/
│ ├── (tabs)/
│ │ ├── index.tsx
│ │ ├── transactions.tsx
│ │ └── settings.tsx
│ ├── _layout.tsx
│ └── index.tsx
├── components/
├── hooks/
├── utils/
├── constants/
└── assets/
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="transaction/[id]" options={{ title: 'Transação' }} />
</Stack>
</>
)
}
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#3b82f6',
headerShown: false
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Início',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
)
}}
/>
<Tabs.Screen
name="transactions"
options={{
title: 'Transações',
tabBarIcon: ({ color, size }) => (
<Ionicons name="list" size={size} color={color} />
)
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Configurações',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings" size={size} color={color} />
)
}}
/>
</Tabs>
)
}
npm install nativewind tailwindcss
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
import { View, Text } from 'react-native'
interface TransactionCardProps {
title: string
amount: number
date: string
category: string
}
export function TransactionCard({ title, amount, date, category }: TransactionCardProps) {
return (
<View className="bg-white rounded-xl p-4 mb-3 shadow-sm">
<View className="flex-row justify-between items-center">
<View>
<Text className="font-semibold text-gray-900">{title}</Text>
<Text className="text-sm text-gray-500">{category}</Text>
<Text className="text-xs text-gray-400">{date}</Text>
</View>
<Text
className={`font-bold ${amount >= 0 ? 'text-green-600' : 'text-red-600'}`}
>
{amount >= 0 ? '+' : ''}R$ {Math.abs(amount).toFixed(2)}
</Text>
</View>
</View>
)
}
import { useState, useEffect } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface Transaction {
id: string
title: string
amount: number
date: string
category: string
}
export function useTransactions() {
const [transactions, setTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
const loadTransactions = async () => {
try {
const stored = await AsyncStorage.getItem('transactions')
setTransactions(stored ? JSON.parse(stored) : [])
} catch (error) {
console.error('Erro ao carregar transações:', error)
} finally {
setLoading(false)
}
}
const addTransaction = async (transaction: Transaction) => {
const updated = [...transactions, transaction]
setTransactions(updated)
await AsyncStorage.setItem('transactions', JSON.stringify(updated))
}
useEffect(() => {
loadTransactions()
}, [])
return { transactions, loading, addTransaction }
}
import { useState } from 'react'
export function useApi<T>(endpoint: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetch = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(`https://api.financialcontrol.com.br${endpoint}`)
if (!response.ok) throw new Error('Falha na requisição')
const result = await response.json()
setData(result)
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido')
} finally {
setLoading(false)
}
}
return { data, loading, error, fetch }
}
import React, { createContext, useContext, useState } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface AuthContextType {
user: string | null
login: (email: string, password: string) => Promise<void>
logout: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<string | null>(null)
const login = async (email: string, password: string) => {
// Lógica de login
const token = 'mock-token'
await AsyncStorage.setItem('token', token)
setUser(email)
}
const logout = async () => {
await AsyncStorage.removeItem('token')
setUser(null)
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
import { Camera } from 'expo-camera'
async function requestCameraPermission() {
const { status } = await Camera.requestCameraPermissionsAsync()
if (status !== 'granted') {
alert('Permissão de câmera negada')
}
}
import * as Location from 'expo-location'
async function requestLocationPermission() {
const { status } = await Location.requestForegroundPermissionsAsync()
if (status !== 'granted') {
alert('Permissão de localização negada')
}
}
npm start
# Escaneie o QR code com Expo Go
eas build --platform ios
eas build --platform android
{
"cli": {
"version": ">= 5.2.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"android": {
"buildType": "app-bundle"
},
"ios": {
"autoIncrement": true
}
}
}
}
npm install --save-dev @testing-library/react-native jest
import { render } from '@testing-library/react-native'
import { TransactionCard } from './TransactionCard'
describe('TransactionCard', () => {
it('renderiza corretamente', () => {
const { getByText } = render(
<TransactionCard
title="Supermercado"
amount={-150.50}
date="2024-01-15"
category="Alimentação"
/>
)
expect(getByText('Supermercado')).toBeTruthy()
expect(getByText('-R$ 150.50')).toBeTruthy()
})
})
React Native + Expo é uma combinação poderosa para desenvolvimento mobile. No Financial Manager, conseguiu-se criar um app funcional para iOS e Android com um time pequeno e ciclo de desenvolvimento rápido.
A chave para o sucesso é entender as diferenças entre web e mobile, aproveitar a comunidade do Expo, e testar extensivamente em dispositivos reais.