EDNEI
EDNEITRABACH
>Início>Projetos>Experiência>Sobre>Blog
GitHubLinkedInInstagram
status: disponível
>Início>Projetos>Experiência>Sobre>Blog
status: disponível

Conecte-se

Vamos construir algo junto

Sempre interessado em colaborações, problemas interessantes e conversas sobre código, design e tudo mais.

enviar mensagem→

Me encontre também

GitHub
@EdneiTrabach
LinkedIn
/in/edneitrabach
Instagram
@edneitrabach
WhatsApp
Conversar
Desenvolvido com& código

© 2026 EdneiTrabach — All experiments reserved

back to blog
mobile

React Native + Expo: Desenvolvimento Mobile Multiplataforma

Construindo apps iOS e Android com React Native e Expo. Do setup inicial ao deployment em produção. Lições aprendidas no Financial Manager.

ET

Ednei Trabach

Desenvolvedor Full Stack

10 de dezembro de 202414 min read
#react-native#expo#mobile#ios#android

Por que React Native + Expo?

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.

Setup Inicial

Criar projeto Expo

npx create-expo-app@latest financial-manager
cd financial-manager
npm start

Estrutura de projeto

financial-manager/
├── app/
│   ├── (tabs)/
│   │   ├── index.tsx
│   │   ├── transactions.tsx
│   │   └── settings.tsx
│   ├── _layout.tsx
│   └── index.tsx
├── components/
├── hooks/
├── utils/
├── constants/
└── assets/

Navegação com Expo Router

app/_layout.tsx

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>
    </>
  )
}

app/(tabs)/_layout.tsx

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>
  )
}

Componentes UI

Uso de NativeWind (Tailwind)

npm install nativewind tailwindcss

tailwind.config.js

module.exports = {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Componente Card

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>
  )
}

Hooks Personalizados

useTransactions.ts

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 }
}

Integração com API

useApi.ts

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 }
}

Autenticação

AuthContext.tsx

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
}

Permissões

Câmera

import { Camera } from 'expo-camera'

async function requestCameraPermission() {
  const { status } = await Camera.requestCameraPermissionsAsync()
  if (status !== 'granted') {
    alert('Permissão de câmera negada')
  }
}

Localização

import * as Location from 'expo-location'

async function requestLocationPermission() {
  const { status } = await Location.requestForegroundPermissionsAsync()
  if (status !== 'granted') {
    alert('Permissão de localização negada')
  }
}

Build e Deployment

Desenvolvimento

npm start
# Escaneie o QR code com Expo Go

iOS Build

eas build --platform ios

Android Build

eas build --platform android

EAS Config (eas.json)

{
  "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
      }
    }
  }
}

Testes

Jest + React Native Testing Library

npm install --save-dev @testing-library/react-native jest

TransactionCard.test.tsx

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()
  })
})

Conclusão

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.

share
share: