Criando um Diário Multimídia com React Native: Câmera e Áudio

Olá, futuros desenvolvedores de sistemas! 🚀

Hoje vamos elevar nosso nível e construir uma aplicação mais completa em React Native usando o Expo. Nosso projeto será um “Diário Multimídia”, um aplicativo que não só tira fotos, mas também grava e reproduz notas de voz.

Este código é um exemplo fantástico para entendermos como gerenciar múltiplas permissões (câmera e microfone), controlar hardware, gerenciar o estado da aplicação de forma reativa e lidar com o ciclo de vida dos componentes para evitar problemas de memória.

Vamos mergulhar nesse código e desvendar cada um de seus segredos!

Visão Geral da Aplicação

O aplicativo funciona da seguinte forma:

  1. Solicita permissões para Câmera e Microfone assim que é iniciado.
  2. Exibe uma interface principal com duas seções: uma para fotografia e outra para notas de voz.
  3. Na seção de câmera: exibe a visão da câmera, permite tirar uma foto e, após a captura, mostra uma prévia da imagem com a opção de tirar outra.
  4. Na seção de áudio: permite iniciar uma gravação, parar a gravação, e depois reproduzir o áudio gravado. A interface dos botões muda dinamicamente para refletir o estado atual (gravando, parado, etc.).

Os Pilares do Código: Hooks do React

Antes de olharmos as funções específicas, vamos entender as ferramentas principais do React que dão vida a este app: os Hooks.

  • 🧠 useState: Pense nele como a “memória” do nosso componente. Usamos useState para criar variáveis de estado. Toda vez que o valor de uma dessas variáveis muda (usando sua função set...), o React automaticamente redesenha a parte da tela que depende dela. No nosso código, ele controla tudo: se temos permissão, qual a URI da foto tirada, se estamos gravando áudio, etc.
  • useEffect useEffect: É o nosso “gerente de efeitos colaterais”. Ele permite executar código em momentos específicos, como quando o app abre pela primeira vez. Usamos ele para:
    1. Solicitar as permissões de câmera e áudio logo no início.
    2. Limpar recursos da memória. No código, há um useEffect genial que “descarrega” o som da memória assim que ele não é mais necessário, prevenindo memory leaks.
  • useRef useRef: É como um “controle remoto” para um elemento específico da tela. Enquanto useState causa uma nova renderização ao ser alterado, useRef nos dá uma referência direta a um elemento (como o componente <Camera>) sem redesenhar a tela. Usamos o cameraRef para chamar funções do componente da câmera diretamente, como cameraRef.current.takePictureAsync().

O Coração da Aplicação: Funções e Lógica

Agora, vamos analisar o fluxo de trabalho principal do app.

1. Gerenciamento de Permissões e Ciclo de Vida

JavaScript

useEffect(() => {
  (async () => {
    // Solicita permissão para câmera e áudio
    const statusCamera = await Camera.requestCameraPermissionsAsync();
    setTemPermissaoCamera(statusCamera.status === 'granted');
    
    const statusAudio = await Audio.requestPermissionsAsync();
    setTemPermissaoAudio(statusAudio.status === 'granted');
  })();
}, []); // O array vazio [] garante que isso rode apenas uma vez.

Este bloco é a porta de entrada. Ele usa uma função assíncrona (async) auto-executável para pedir as duas permissões em sequência e atualizar os respectivos estados.

JavaScript

// Efeito para limpar o objeto de som da memória
useEffect(() => {
  return som ? () => { som.unloadAsync(); } : undefined;
}, [som]);

Este é um conceito profissional e muito importante! Este useEffect “escuta” por mudanças na variável som. Quando o componente está prestes a ser desmontado ou o som muda, ele executa a função de retorno (cleanup), chamando som.unloadAsync() para liberar a memória.

2. Capturando a Imagem 📸

JavaScript

async function tirarFoto() {
  if (cameraRef.current && cameraPronta) {
    const foto = await cameraRef.current.takePictureAsync();
    setUriDaImagem(foto.uri);
  }
}

A função tirarFoto é simples e direta:

  1. Verifica se a referência da câmera existe (cameraRef.current) e se a câmera está pronta (cameraPronta). Isso evita erros.
  2. Chama o método takePictureAsync() através da referência.
  3. Salva a URI (o caminho local) da foto no estado uriDaImagem, o que faz a tela trocar a visão da câmera pela prévia da imagem.

3. Dominando o Áudio 🎤

O controle de áudio é um ciclo de três partes:

  1. iniciarGravacao:
    • Configura o modo de áudio com Audio.setAudioModeAsync(), essencial para garantir a compatibilidade, especialmente no iOS.
    • Cria uma nova instância de gravação com Audio.Recording.createAsync().
    • Salva o objeto da gravação no estado gravacao.
  2. pararGravacao:
    • Chama gravacao.stopAndUnloadAsync() para parar a gravação e liberar o gravador da memória.
    • Pega a URI do arquivo de áudio com gravacao.getURI().
    • Salva essa URI no estado uriDaGravacao e limpa o estado gravacao.
  3. reproduzirSom:
    • Usa Audio.Sound.createAsync() para carregar o arquivo de áudio a partir da uriDaGravacao.
    • Salva o objeto de som no estado som.
    • Chama sound.playAsync() para tocar o áudio.

Construindo a Interface (UI): O Poder da Renderização Condicional

A mágica do React brilha na seção de renderização (o return). A tela não é estática; ela reage às mudanças de estado.

Verificação de Permissões

JavaScript

if (temPermissaoCamera === null || temPermissaoAudio === null) {
  return <View style={styles.centralizado}><Text>Solicitando permissões...</Text></View>;
}
if (temPermissaoCamera === false) {
  // Mostra erro da câmera
}

Primeiro, o código lida com os casos de carregamento e permissão negada. Isso garante que o usuário sempre veja uma tela apropriada para o estado atual do aplicativo.

Interface da Câmera vs. Prévia da Imagem

JavaScript

{uriDaImagem ? (
  <View>
    <Image source={{ uri: uriDaImagem }} />
    <Button title="Tirar Outra Foto" onPress={resetarFoto} />
  </View>
) : (
  <View>
    <Camera ref={cameraRef} />
    <Button title="Tirar Foto" onPress={tirarFoto} />
  </View>
)}

Aqui, um operador ternário (condição ? valor_se_verdadeiro : valor_se_falso) decide o que mostrar. Se uriDaImagem tiver um valor (ou seja, uma foto foi tirada), ele mostra o componente <Image>. Caso contrário, mostra o componente <Camera>.

Botões de Áudio Dinâmicos

JavaScript

<Button
  title={gravacao ? 'Parando Gravação...' : 'Gravar Áudio'}
  onPress={gravacao ? pararGravacao : iniciarGravacao}
  color={gravacao ? '#FF4500' : '#32CD32'}
/>

Este botão de gravação é um exemplo brilhante de UI reativa. Seu title, sua função onPress e sua color mudam dependendo se existe um objeto de gravacao no estado. Da mesma forma, o botão “Reproduzir” é desabilitado (disabled={...}) de forma inteligente para evitar que o usuário tente tocar um som que não existe ou enquanto uma gravação está em andamento.

Código Completo

import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, Text, View, Button, Image, Platform, Alert } from 'react-native';
import { Camera } from 'expo-camera';
import { Audio } from 'expo-av';

export default function App() {
  // =================================================================
  // --- ESTADOS (STATES) ---
  // Usamos o 'useState' para criar variáveis que, quando alteradas,
  // fazem o React redesenhar a tela automaticamente.
  // =================================================================

  // Estados para permissões
  const [temPermissaoCamera, setTemPermissaoCamera] = useState(null);
  const [temPermissaoAudio, setTemPermissaoAudio] = useState(null);

  // Estados para a câmera
  const cameraRef = useRef(null); // 'useRef' cria uma referência para acessar o componente da Câmera diretamente.
  const [uriDaImagem, setUriDaImagem] = useState(null); // Armazena o caminho (URI) da foto tirada.
  const [cameraPronta, setCameraPronta] = useState(false); // Verifica se a câmera está pronta para uso.

  // Estados para o áudio
  const [gravacao, setGravacao] = useState(null); // Armazena o objeto da gravação em andamento.
  const [som, setSom] = useState(null); // Armazena o objeto do som para reprodução.
  const [uriDaGravacao, setUriDaGravacao] = useState(null); // Armazena o caminho (URI) do áudio gravado.

  // =================================================================
  // --- EFEITOS (EFFECTS) ---
  // O 'useEffect' executa código em momentos específicos do ciclo de
  // vida do componente, como na primeira vez que ele aparece na tela.
  // =================================================================

  // Efeito para solicitar permissões ao iniciar o app.
  // O array vazio `[]` no final significa que este código só roda uma vez.
  useEffect(() => {
    (async () => {
      // Solicita permissão para usar a câmera.
      const statusCamera = await Camera.requestCameraPermissionsAsync();
      setTemPermissaoCamera(statusCamera.status === 'granted');

      // Solicita permissão para usar o microfone.
      const statusAudio = await Audio.requestPermissionsAsync();
      setTemPermissaoAudio(statusAudio.status === 'granted');
    })();
  }, []);
  
  // Efeito para limpar o objeto de som da memória quando ele não for mais usado.
  // Isso evita vazamentos de memória.
  useEffect(() => {
    return som
      ? () => {
          console.log('Descarregando o som da memória...');
          som.unloadAsync();
        }
      : undefined;
  }, [som]);


  // =================================================================
  // --- FUNÇÕES DE ÁUDIO ---
  // =================================================================

  async function iniciarGravacao() {
    try {
      if (!temPermissaoAudio) {
        Alert.alert("Erro", "Permissão de áudio não concedida.");
        return;
      }
      // Define o modo de áudio, necessário para gravação no iOS.
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: true,
        playsInSilentModeIOS: true, // Permite tocar som mesmo no modo silencioso
      });

      console.log('Iniciando gravação de áudio...');
      const { recording } = await Audio.Recording.createAsync(
         Audio.RecordingOptionsPresets.HIGH_QUALITY // Define a qualidade da gravação
      );
      setGravacao(recording); // Armazena o objeto da gravação no estado.
      console.log('Gravação iniciada.');

    } catch (err) {
      console.error('Falha ao iniciar a gravação', err);
    }
  }

  async function pararGravacao() {
    if (!gravacao) return; // Se não houver gravação, não faz nada.
    
    console.log('Parando gravação...');
    setGravacao(undefined); // Limpa o estado da gravação.
    await gravacao.stopAndUnloadAsync(); // Para e libera o gravador da memória.
    const uri = gravacao.getURI(); // Pega o caminho do arquivo gravado.
    setUriDaGravacao(uri); // Salva o caminho no estado para poder reproduzir depois.
    console.log('Gravação parada e salva em:', uri);
  }

  async function reproduzirSom() {
    if (!uriDaGravacao) {
      Alert.alert('Aviso', 'Nenhum áudio foi gravado ainda.');
      return;
    }

    console.log('Carregando e reproduzindo o som...');
    const { sound } = await Audio.Sound.createAsync({ uri: uriDaGravacao });
    setSom(sound); // Salva o objeto de som no estado.
    await sound.playAsync(); // Toca o som.
  }

  // =================================================================
  // --- FUNÇÕES DA CÂMERA ---
  // =================================================================

  async function tirarFoto() {
    // Verifica se a referência da câmera existe e se ela está pronta.
    if (cameraRef.current && cameraPronta) {
      try {
        const foto = await cameraRef.current.takePictureAsync();
        setUriDaImagem(foto.uri); // Salva o caminho da foto no estado.
        console.log('Foto tirada com sucesso:', foto.uri);
      } catch (error) {
        console.error('Erro ao tirar a foto:', error);
        Alert.alert('Erro', 'Não foi possível tirar a foto.');
      }
    } else {
        Alert.alert('Aviso', 'A câmera não está pronta.');
    }
  }
  
  // Função para limpar a imagem e mostrar a câmera novamente.
  function resetarFoto() {
      setUriDaImagem(null);
  }

  // =================================================================
  // --- RENDERIZAÇÃO (O QUE APARECE NA TELA) ---
  // =================================================================

  // Se as permissões ainda não foram verificadas, mostra uma tela de carregamento.
  if (temPermissaoCamera === null || temPermissaoAudio === null) {
    return <View style={styles.centralizado}><Text>Solicitando permissões...</Text></View>;
  }
  // Se a permissão da câmera foi negada, mostra uma mensagem de erro.
  if (temPermissaoCamera === false) {
    return <View style={styles.centralizado}><Text>Acesso à câmera negado. Por favor, habilite nas configurações do seu celular.</Text></View>;
  }
  // Se a permissão do áudio foi negada, mostra uma mensagem de erro.
   if (temPermissaoAudio === false) {
    return <View style={styles.centralizado}><Text>Acesso ao microfone negado. Por favor, habilite nas configurações do seu celular.</Text></View>;
  }

  // Se todas as permissões foram concedidas, mostra a interface principal.
  return (
    <View style={styles.container}>
      <Text style={styles.titulo}>📝 Meu Diário Multimídia 📸</Text>
      
      {/* Seção da Câmera */}
      <View style={styles.secao}>
        <Text style={styles.tituloSecao}>Fotografia</Text>
        {/* Renderização Condicional: Se 'uriDaImagem' existir, mostra a imagem. Senão, mostra a câmera. */}
        {uriDaImagem ? (
          <View style={styles.containerMidia}>
            <Image source={{ uri: uriDaImagem }} style={styles.previaImagem} />
            <Button title="Tirar Outra Foto" onPress={resetarFoto} color="#1E90FF"/>
          </View>
        ) : (
          <View style={styles.containerMidia}>
            <Camera 
              ref={cameraRef} 
              style={styles.camera} 
              type={Camera.Constants.Type.back} // Usa a câmera traseira.
              onCameraReady={() => setCameraPronta(true)} // Avisa quando a câmera estiver pronta.
            />
            <View style={styles.espacoBotao} />
            <Button title="Tirar Foto" onPress={tirarFoto} disabled={!cameraPronta} />
          </View>
        )}
      </View>
      
      {/* Seção do Áudio */}
      <View style={styles.secao}>
        <Text style={styles.tituloSecao}>Nota de Voz</Text>
        <Button
          title={gravacao ? 'Parando Gravação...' : 'Gravar Áudio'}
          onPress={gravacao ? pararGravacao : iniciarGravacao}
          color={gravacao ? '#FF4500' : '#32CD32'} // Muda a cor do botão se estiver gravando.
        />
        <View style={styles.espacoBotao} />
        <Button 
            title="Reproduzir Gravação" 
            onPress={reproduzirSom} 
            disabled={!uriDaGravacao || !!gravacao} // Desabilita o botão se não houver gravação ou se estiver gravando.
        />
        {/* Mostra um texto de status quando o áudio está pronto. */}
        {uriDaGravacao && !gravacao && <Text style={styles.textoStatus}>Áudio pronto para tocar!</Text>}
      </View>
    </View>
  );
}

// =================================================================
// --- ESTILOS (STYLES) ---
// Define a aparência dos componentes.
// =================================================================
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f0f4f7',
    paddingTop: 50,
    paddingHorizontal: 20,
  },
  centralizado: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  titulo: {
    fontSize: 26,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
    color: '#333',
  },
  secao: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 20,
    marginBottom: 20,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3.84,
    elevation: 5,
  },
  tituloSecao: {
    fontSize: 20,
    fontWeight: '600',
    textAlign: 'center',
    marginBottom: 15,
    color: '#444',
  },
  containerMidia: {
    alignItems: 'center',
  },
  camera: {
    width: '100%',
    aspectRatio: 1,
    borderRadius: 8,
    overflow: 'hidden',
  },
  previaImagem: {
    width: '100%',
    aspectRatio: 1,
    borderRadius: 8,
    marginBottom: 15,
  },
  espacoBotao: {
      height: 10,
  },
  textoStatus: {
    marginTop: 10,
    color: 'green',
    textAlign: 'center',
    fontWeight: 'bold',
  },
});

Conclusão

Este “Diário Multimídia” é uma aula prática sobre como construir aplicações robustas em React Native. Os principais aprendizados são:

  • Gerenciamento de Estado: Como usar useState para controlar a interface de forma declarativa.
  • Controle de Hardware: Como usar as APIs do Expo (expo-camera, expo-av) para interagir com os recursos nativos do celular.
  • Programação Assíncrona: O uso de async/await é fundamental para lidar com operações que levam tempo, como permissões e I/O de arquivos.
  • Ciclo de Vida e Memória: A importância de gerenciar o ciclo de vida dos componentes e limpar recursos com useEffect para criar apps eficientes e sem falhas.

Agora, o desafio para vocês é expandir este projeto! Que tal adicionar a opção de salvar as fotos e áudios na galeria do celular? Ou permitir a troca entre a câmera frontal e traseira? As possibilidades são infinitas.

Bons estudos e mãos ao código!

Share this content:

Profissional engajado com as últimas tendências tecnológicas e de gestão, buscando continuamente aprimorar suas competências e compartilhar seu conhecimento.

Publicar comentário