Upload de Imagens e uso da Câmera no React Native
Esse post é a sexta parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação web com ReactJS e também uma aplicação mobile com React Native com dados servidos através de uma API REST feita com NodeJS utilizando o framework AdonisJS.
- Parte 1: Iniciando com AdonisJS: Autenticação JWT e API REST;
- Parte 2: Criando CRUD e relações em API REST no AdonisJS;
- Parte 3: Upload de imagens e geolocalização no AdonisJS;
- Parte 4: Iniciando com React Native: Navegação e Autenticação com JWT;
- Parte 5: Instalando o Mapbox e listando imóveis no React Native;
- Parte 6: Instalando a Câmera e realizando o cadastro de Imóveis;
- Parte 7: Listando em um Modal os dados detalhados dos Imóveis;
- Parte 8: Iniciando com ReactJS: Navegação e Autenticação com JWT;
- Parte 9: Instalando o Mapbox e listando os imóveis no ReactJS;
- Parte 10: Utilizando o ModalRoute e fazendo upload de imagens;
- Parte 11: Exibindo informações do imóvel com ModalRoute;
Nesse ponto já temos a aplicação criada e configurada para se comunicar com a API desenvolvida no começo dessa série, e com isso já conseguimos implementar o processo de autenticação e cadastro de usuário, nessa parte iremos instalar e configurar a Câmera para podermos além de selecionar as imagens do dispositivo, tirar fotos para o cadastro do imóvel, e por fim enviar essas imagens para serem cadastradas no banco de dados da API.
Iniciando a Aplicação
Caso você tenha entrado direto nessa parte, você pode acessar esse link e baixar o projeto com todas as configurações do post anterior, apesar de eu recomendar pessoalmente que você leia as duas partes anteriores para entender o fluxo da aplicação.
Ou se você já viu as partes anteriores pode continuar para a próxima sessão onde iremos começar a instalação da lib de Câmera.
Instalando e Configurando o react-native-camera
Para não deixar o post muito extenso explicando o passo a passo da instalação e configuração do react-native-camera, você pode correr no Blog da Rocketseat e ler esse post sobre a instalação e configuração da Câmera.
Caso você tenha algum problema na instalação ou não tenha conseguido fazer todas as configurações acesse esse link e baixe a estrutura configurada para dar continuidade.
Criando Primeira Tela do Cadastro de Imóveis
Para começar o cadastro a primeira coisa que vamos fazer é criar um botão sobre o mapa para que quando clicado comece o processo de cadastro do imóvel, e como nesse post criaremos vários novos estilos, vamos fazer uma pequena mudança no arquivo de estilos para que fique mais organizado, primeiramente retire a instrução export do final do arquivo src/pages/main/styles.js
em seguida, adicione um export antes de todas as const’s, e feito isso vamos criar os seguintes componentes:
export const NewButtonContainer = styled.TouchableHighlight`
position: absolute;
bottom: 54;
left: 20;
right: 20;
alignSelf: center;
borderRadius: 5px;
paddingVertical: 20px;
backgroundColor: #fc6663;
`;
export const ButtonsWrapper = styled.View`
position: absolute;
bottom: 54;
left: 20;
right: 20;
`;
export const CancelButtonContainer = styled.TouchableHighlight`
alignSelf: stretch;
borderRadius: 5px;
paddingVertical: 20px;
backgroundColor: #555;
marginTop: 20px;
`;
export const SelectButtonContainer = styled.TouchableHighlight`
alignSelf: stretch;
borderRadius: 5px;
paddingVertical: 20px;
backgroundColor: #fc6663;
`;
export const ButtonText = styled.Text`
color: #fff;
fontSize: 16px;
textAlign: center;
fontWeight: bold;
`;
Obs.: Não se esqueça de importá-los no src/pages/main/index.js
.
Em seguida vamos deixar o state preparado para toda a parte de cadastro de um novo imóvel, seu código final vai ficar:
state = {
locations: [],
newRealty: false,
cameraModalOpened: false,
dataModalOpened: false,
realtyData: {
location: {
latitude: null,
longitude: null,
},
name: '',
price: '',
address: '',
images: [],
},
};
Temos uma variável de controle para os botões de Novo Imóvel e Selecionar Localização, 2 variáveis de controle para os modais que implementaremos logo na sequência (cameraModalOpened e dataModalOpened) e um objeto que irá guardar todas as informações referentes ao imóvel (realtyData).
A primeira coisa que devemos fazer no processo de cadastro de um imóvel é selecionar qual a localização do mesmo, e para isso iremos reaproveitar o botão de Novo Imóvel para fazer uma renderização condicional, quando a variável newRealty do State for false será mostrado o botão Novo Imóvel, quando ela for true serão mostrados os botões para Selecionar Localização e Cancelar, como na imagem abaixo:
Para que os botões funcionem corretamente vamos criar uma função chamada renderConditionalsButtons entre o componentDidMount e o método render() e seu conteúdo será o seguinte:
renderConditionalsButtons = () => (
!this.state.newRealty ? (
<NewButtonContainer onPress={this.handleNewRealtyPress}>
<ButtonText>Novo Imóvel</ButtonText>
</NewButtonContainer>
) : (
<ButtonsWrapper>
<SelectButtonContainer onPress={this.handleGetPositionPress}>
<ButtonText>Selecionar localização</ButtonText>
</SelectButtonContainer>
<CancelButtonContainer onPress={this.handleNewRealtyPress}>
<ButtonText>Cancelar</ButtonText>
</CancelButtonContainer>
</ButtonsWrapper>
)
)
Nessa parte usamos todos os componentes criados para os botões, mas ainda faltou criarmos as funções de callback dos botões e também chamar essa função no método render(), segue abaixo o escopo dessas 2 funções que devem ser criadas logo abaixo da função renderConditionalsButtons:
handleNewRealtyPress = () =>
this.setState({ newRealty: !this.state.newRealty })
handleGetPositionPress = async () => {
try {
const [longitude, latitude] = await this.map.getCenter();
this.setState({
cameraModalOpened: true,
realtyData: {
...this.state.realtyData,
location: {
latitude,
longitude,
},
},
});
} catch (err) {
console.tron.log(err);
}
}
A função handleNewRealtyPress apenas muda o valor da variável newRealty de true para false e vice-versa, já a função handleGetPositionPress utiliza um método disponibilizado pelo Mapbox que permite obter o ponto central do mapa, em seguida usamos esse ponto para adicionar as coordenadas selecionadas no objeto locationdentro do objeto realtyData, mas para que essa função funcione corretamente temos que adicionar a propriedade ref no componente de mapa, para isso basta adicionar as linhas abaixo no MapboxGL.MapView:
ref={map => {
this.map = map;
}}
E por último vamos dentro do método render, dentro do Container, logo após o fechamento do componente MapboxGL.MapView e adicionar a seguinte instrução: { this.renderConditionalsButtons() }
Agora quando você rodar a aplicação os botões devem estar funcionando corretamente, mas ainda falta um detalhe que pode atrapalhar na experiência do usuário, no momento de selecionar a localização não tem em lugar algum apontando onde fica exatamente o meio, por isso iremos criar um Marker que irá indicar exatamente onde é o meio a ser selecionado, para isso acesse esse link para obter a imagem do marker, que está dentro da pasta src/images
.
Antes mesmo de criarmos a função para inserir o Marker no mapa vamos novamente para o src/pages/main/styles.js
e criar mais um componente, ficando assim:
export const Marker = styled.Image`
width: 60px;
height: 60px;
position: absolute;
alignSelf: center;
top: ${(Dimensions.get('window').height / 2) - 60};
`;
E para que não haja erro você deve também adicionar a importação para o Dimensions no começo desse arquivo, ficando assim:
import { Dimensions } from 'react-native';
A função acima irá criar um componente que irá sobrepor o mapa e sua parte inferior, ou seja, a ponta do Marker será posicionada exatamente no meio da tela, agora adicionando esse componente no export dos estilos e adicionando no import desses componentes na src/pages/main/index.js
podemos criar a função renderMarker, que deve ficar assim:
renderMarker = () => (
this.state.newRealty &&
!this.state.cameraModalOpened &&
<Marker resizeMode="contain" source={require('../../images/marker.png')} />
)
É uma função bem simples que faz uma renderização condicional baseada no valor de 2 variáveis do state, a newRealty e cameraModalOpened, ou seja, esse Marker vai aparecer apenas quando o newRealty for true e o cameraModalOpened for false.
Para que funcione corretamente agora temos que adicionar a chamada dessa função no método render() do componente, logo após a chamada da função this.renderConditionalsButtons(), ficando assim:
{ this.renderConditionalsButtons() }
{ this.renderMarler() }
E assim terminamos a primeira parte do cadastro, onde já é possível selecionar a opção de Cadastrar um novo imóvel e selecionar a localização do mesmo no mapa, retornando as coordenadas para o state.
Agora bora para a próxima parte? (aleluia \o/)
Criação do Modal para Captura de Imagens do Imóvel
Uma vez seleciona a localização do imóvel vamos para a próxima etapa, que é permitir que o usuário tire fotos do imóvel, para posteriormente cadastrá-las junto com os demais dados, e para isso a primeira coisa que vamos fazer é criar o estilo dos componentes, como a lista para o Modal ficou um pouco extensa, deixei nesse link as constantes que vemos adicionar no src/pages/main/styles.js
.
No src/pages/main/index.js
o import deve ficar assim:
import {
Container,
AnnotationContainer,
AnnotationText,
NewButtonContainer,
ButtonsWrapper,
CancelButtonContainer,
SelectButtonContainer,
ButtonText,
Marker,
ModalContainer,
ModalImagesListContainer,
ModalImagesList,
ModalImageItem,
ModalButtons,
CameraButtonContainer,
CancelButtonText,
ContinueButtonText,
TakePictureButtonContainer,
TakePictureButtonLabel,
} from './styles';
Agora vamos à criação do Modal, e para que não haja erros, primeiro vamos adicionar algumas importações no início do arquivo index.js
, sendo elas:
import { StatusBar, Modal } from 'react-native'; // Adicionado o Modal
import { RNCamera } from 'react-native-camera';
Feito isso vamos criar a função responsável por renderizar o componente Modal na página, sobre o mapa, a função criada vai se chamar renderCameraModal, ela pode ser adicionada logo antes do método render() e seu escopo ficará assim:
renderCameraModal = () => (
<Modal
visible={this.state.cameraModalOpened}
transparent={false}
animationType="slide"
onRequestClose={this.handleCameraModalClose}
>
<ModalContainer>
<ModalContainer>
<RNCamera
ref={camera => {
this.camera = camera;
}}
style={{ flex: 1 }}
type={RNCamera.Constants.Type.back}
autoFocus={RNCamera.Constants.AutoFocus.on}
flashMode={RNCamera.Constants.FlashMode.off}
permissionDialogTitle={"Permission to use camera"}
permissionDialogMessage={
"We need your permission to use your camera phone"
}
/>
<TakePictureButtonContainer onPress={this.handleTakePicture}>
<TakePictureButtonLabel />
</TakePictureButtonContainer>
</ModalContainer>
{ this.renderImagesList() }
<ModalButtons>
<CameraButtonContainer onPress={this.handleCameraModalClose}>
<CancelButtonText>Cancelar</CancelButtonText>
</CameraButtonContainer>
<CameraButtonContainer onPress={this.handleDataModalClose}>
<ContinueButtonText>Continuar</ContinueButtonText>
</CameraButtonContainer>
</ModalButtons>
</ModalContainer>
</Modal>
)
Essa função é a responsável por criar um Modal na tela, cuja exibição é controlada pela variável cameraModalOpened do state do componente, dentro do Modal é criada uma estrutura que contém na parte superior a câmera com um botão para capturar a imagem, na parte inferior 2 botões, um para continuar para a próxima tela e um para cancelar, e no meio disso quando houverem imagens capturadas pela câmera irá aparecer uma lista com miniaturas das imagens capturadas.
Obs.: Não se preocupe, a câmera só ficou assim por estar em um emulador.
Para que essa parte funcione corretamente é necessário que sejam criadas algumas funções que são chamadas dentro da função renderCameraModal, são elas: handleTakePicture, renderImagesList, handleCameraModalClose e handleDataModalClose, segue abaixo o código delas:
handleTakePicture = async () => {
if (this.camera) {
const options = { quality: 0.5, base64: true, forceUpOrientation: true, fixOrientation: true, };
const data = await this.camera.takePictureAsync(options)
const { realtyData } = this.state;
this.setState({ realtyData: {
...realtyData,
images: [
...realtyData.images,
data,
]
}})
}
}
renderImagesList = () => (
this.state.realtyData.images.length !== 0 ? (
<ModalImagesListContainer>
<ModalImagesList horizontal>
{ this.state.realtyData.images.map(image => (
<ModalImageItem source={{ uri: image.uri }} resizeMode="stretch" />
))}
</ModalImagesList>
</ModalImagesListContainer>
) : null
)
handleCameraModalClose = () => this.setState({ cameraModalOpened: !this.state.cameraModalOpened })
handleDataModalClose = () => this.setState({
dataModalOpened: !this.state.dataModalOpened,
cameraModalOpened: false,
})
Agora uma breve explicação do funcionamento de cada uma:
- handleCameraModalClose e handleDataModalClose: Ambas tem a função apenas de fazer um toggle (inversão) do valor do state das variáveis cameraModalOpened e dataModalOpened;
- handleTakePicture: Essa função captura a imagem da câmera e adiciona no array images dentro de realtyData no state;
- renderImagesList: Essa função não renderiza nada caso não haja imagens capturadas, mas caso haja faz um map no array de imagens e lista cada um em um <Image> dentro de um <ScrollView> horizontal.
E assim acaba mais uma parte da sequência de cadastro, a próxima e última parte é inserir as demais informações e fazer o cadastro na API.
Caso você tenha alguma dúvida ou problema pode conferir o código da aplicação nesse link.
Criação da Tela de Informações do Imóvel e efetivando o cadastro na API
Nessa última parte vamos criar outro Modal para listar e preencher os últimos dados do imóvel, antes de realizar o cadastro na API, por isso para iniciar vamos criar os estilos, mas como ficou um pouco extenso deixei disponível nesse link, basta copiar todo o conteúdo e colar no seu arquivo src/pages/main/styles.js
.
Feito isso adicione todos os componentes no import do arquivo src/pages/main/index.js
.
Agora sim podemos criar a função para renderizar o Modal, para isso vamos criar uma função renderDataModal, seu código vai ficar assim:
renderDataModal = () => (
<Modal
visible={this.state.dataModalOpened}
transparent={false}
animationType="slide"
onRequestClose={this.handleDataModalClose}
>
<ModalContainer>
<ModalContainer>
<MapboxGL.MapView
centerCoordinate={[
this.state.realtyData.location.longitude,
this.state.realtyData.location.latitude
]}
style={{ flex: 1 }}
styleURL={MapboxGL.StyleURL.Dark}
>
<MapboxGL.PointAnnotation
id="center"
coordinate={[
this.state.realtyData.location.longitude,
this.state.realtyData.location.latitude
]}
>
<MarkerContainer>
<MarkerLabel />
</MarkerContainer>
</MapboxGL.PointAnnotation>
</MapboxGL.MapView>
</ModalContainer>
{ this.renderImagesList() }
<Form>
<Input
placeholder="Nome do Imóvel"
value={this.state.realtyData.name}
onChangeText={name => this.handleInputChange('name', name)}
autoCapitalize="none"
autoCorrect={false}
/>
<Input
placeholder="Endereço"
value={this.state.realtyData.address}
onChangeText={address => this.handleInputChange('address', address)}
autoCapitalize="none"
autoCorrect={false}
/>
<Input
placeholder="Preço"
value={this.state.realtyData.price}
onChangeText={price => this.handleInputChange('price', price)}
autoCapitalize="none"
autoCorrect={false}
/>
</Form>
<DataButtonsWrapper>
<SelectButtonContainer onPress={this.saveRealty}>
<ButtonText>Salvar Imóvel</ButtonText>
</SelectButtonContainer>
<CancelButtonContainer onPress={this.handleDataModalClose}>
<ButtonText>Cancelar</ButtonText>
</CancelButtonContainer>
</DataButtonsWrapper>
</ModalContainer>
</Modal>
)
Como vocês perceberam ele ficou um pouco longo, mas isso porque temos mais componentes do que lógica nesse Modal, essa função é bem semelhante à do Modal de câmera onde temos um Modal com um Container, dentro dele temos os componentes que serão renderizados, e nesse Modal temos no começo um Mapa como na Tela Principal, a diferença é que dentro dele temos um Marker para a localização selecionada, depois disso temos a lista de imagens como na tela anterior, no final temos novamente 2 botões, a diferença principal da outra tela é a presença do Form com 3 campos para preenchimento de informações, e é nesse form que vamos utilizar as variáveis name, address e price do objeto realtyData do State.
E para finalizar vamos criar as funções usadas pelo Modal para que não haja erro, temos que criar as funções handleInputChange e saveRealty, primeiro vou postar o código da função que trata a mudança dos Inputs para depois focarmos na função de cadastro do Imóvel.
handleInputChange = (index, value) => {
const { realtyData } = this.state;
switch (index) {
case 'name':
this.setState({ realtyData: {
...realtyData,
name: value,
}});
break;
case 'address':
this.setState({ realtyData: {
...realtyData,
address: value,
}});
break;
case 'price':
this.setState({ realtyData: {
...realtyData,
price: value,
}});
break;
}
}
As funções acima tem o objetivo apenas de mudar a variável referente ao campo no state.
E agora sim, a cereja do bolo (aeeeee 😃), a função que irá efetivar o cadastro do imóvel na API, a saveRealty, primeiro vou postar seu código para depois abordar a lógica usada, e como o código ficou um pouco extenso resolvi deixar disponível nesse link basta copiar esse código e colar no seu componente junto com as demais funções, mas não se preocupe, como eu já disse, vou quebrá-la em partes e explicar a lógica de cada trecho.
O primeiro trecho que encontramos é o seguinte:
const {
realtyData: {
name,
address,
price,
location: {
latitude,
longitude
},
images
}
} = this.state;
Esse trecho é uma desestruturação do state para obtermos todos os dados referentes ao imóvel, o próximo trecho é:
const newRealtyResponse = await api.post('/properties', {
title: name,
address,
price,
latitude: Number(latitude.toFixed(6)),
longitude: Number(longitude.toFixed(6)),
});
Esse trecho estamos enviando à API uma requisição POST com os dados do imóvel para que haja o cadastro efetivo do imóvel e nos seja retornado um objeto com o id do imóvel no banco de dados, que iremos usar logo na sequência.
O próximo trecho é o ponto chave dessa função, ele é o responsável por organizar as imagens para serem enviadas corretamente para a API e serem movidas para o servidor e cadastradas no banco de dados, segue o código:
const imagesData = new FormData();
images.forEach((image, index) => {
imagesData.append('image', {
uri: image.uri,
type: 'image/jpeg',
name: `${newRealtyResponse.data.title}_${index}.jpg`
});
});
Nesse trecho usamos o FormData que é um recurso que nos permite criar um conjunto de pares chave/valor representando campos de um formulário com seus respectivos valores, e é ele que iremos usar para enviar as imagens, por isso logo abaixo recuperamos o array images do state e usamos um forEach para percorrer cada índice dele, e a cada índice é adicionado no FormData uma posição com o nome image e como valor um objeto contendo o endereço da imagem, o tipo dela e também um nome único.
Agora com as imagens organizadas vamos ao envio da requisição:
await api.post(
`/properties/${newRealtyResponse.data.id}/images`,
imagesData,
);
Nesse trecho apenas usamos o Axios para enviar uma requisição para a rota /properties/:id/images
da API com o FormData previamente formatado.
E por último, antes de chamar as 3 últimas funções para finalizar a parte de cadastro, temos que fazer uma pequena mudança, no vamos tirar o código da requisição que está no componentDidMount e passar para uma função separada, que será usada logo na sequência, ficando dessa maneira:
componentDidMount() {
this.getLocation();
}
getLocation = async () => {
try {
const response = await api.get('/properties', {
params: {
latitude: -27.210768,
longitude: -49.644018,
},
});
this.setState({ locations: response.data });
} catch (err) {
console.tron.log(err);
}
}
Agora sim, vamos à chamada das funções no final da saveRealty, ficando assim:
this.getLocation();
this.setState({ newRealty:false });
this.handleDataModalClose();
Elas basicamente atualizam a lista de imóveis no state, depois é voltado o botão de Novo Imóvel na tela do mapa e por último é fechado o Modal de Dados.
Com tudo isso feito sua aplicação deve funcionar corretamente permitindo todo o processo de cadastro de um novo imóvel, você também pode conferir o código completo nesse link para comparar com o seu ou realizar testes caso tenha alguma dúvida ou problema 😉
Considerações Finais
Como você percebeu essa parte ficou um pouco mais extensa que as anteriores, mas isso devido à complexidade do processo de cadastro e do número de etapas necessárias.
A aplicação nesse ponto está quase pronta, falta apenas a listagem dos detalhes dos imóveis, mas isso é para a próxima parte, afinal já temos uma aplicação que faz Autenticação usando JWT, uso de Mapas, uso de Câmera, Upload de Imagens, dentre outras funcionalidades que já implementamos, percebe o quanto a aplicação está completa e com recursos que no dia a dia são um diferencial em uma aplicação!?
Apenas reforçando, você pode conferir o código final nesse link 😉
Espero que tenha gostado, deixe seu comentário/dúvida/crítica/sugestão que é muito importante para nós 😃
Até a próxima parte \o/