Com o Expo, os desenvolvedores podem criar aplicativos React Native sem passar pelo incômodo de instalar e configurar dependências de software, como Android Studio, Xcode ou todas as outras ferramentas necessárias para desenvolver e executar um aplicativo React Native.
Neste tutorial, mostrarei como criar um jogo de memória simples usando a Expo. Ao longo do caminho, você também aprenderá o seguinte:
- Como usar as ferramentas fornecidas pela Expo. Isso inclui a CLI, o SDK e o aplicativo cliente Expo.
- Como criar um aplicativo React Native usando Expo.
O que é Expo?
Expo é uma estrutura para desenvolver rapidamente aplicativos React Native. É como Laravel ou Symphony para desenvolvedores PHP, ou Ruby on Rails para desenvolvedores Ruby. A Expo fornece uma camada sobre as APIs do React Native para torná-las mais fáceis de usar e gerenciar. Ele também fornece ferramentas que facilitam a inicialização e o teste de aplicativos React Native. Por fim, ele fornece componentes e serviços de interface do usuário que geralmente só estão disponíveis quando você instala um componente React Native de terceiros. Todos eles são disponibilizados através do Expo SDK.
Limitações da Expo
Antes de prosseguir, é importante estar ciente de algumas das limitações da Expo:
- Os aplicativos Expos são limitados às APIs nativas que o Expo SDK suporta. Isso significa que, se seu aplicativo tiver um caso de uso muito específico, como a comunicação com um periférico Bluetooth, a única opção para implementar essa funcionalidade é com React Native simples ou escrevendo código nativo.
- Expo te bloqueia em seu conjunto de ferramentas. Isso significa que você não pode simplesmente instalar e usar a maioria das ótimas ferramentas disponíveis para desenvolvimento React Native, como ferramentas de linha de comando, scaffolders e estruturas de interface do usuário. Mas o bom é que o Expo SDK é compatível com aplicativos React Native simples, então você não terá nenhum problema ao ejetar seu aplicativo do Expo.
Mesmo com essas limitações, é importante ter em mente que Expo é uma estrutura totalmente funcional com muito suporte para APIs Android ou iOS comumente usadas. Isso significa que ele oferece cobertura para a maioria das funcionalidades que os aplicativos geralmente precisam. Portanto, muitas vezes não há necessidade de procurar fora do Expo para implementar a funcionalidade nativa.
Visão geral do aplicativo
O aplicativo que vamos criar é um jogo de memória. Você pode estar familiarizado com esse tipo de jogo – o usuário precisa encontrar pares, virando duas cartas de cada vez. Veja como é a tela padrão:
E aqui está como fica uma vez que todos os pares foram abertos:
Depois de resolver o jogo, o usuário pode tocar no Redefinir botão para redefinir os itens para seu estado inicial. Isso permite que eles comecem o jogo novamente.
Instalando a Expo
Ao contrário do React Native simples, onde você precisa instalar e configurar o Android Studio ou o Xcode e outras dependências, com o Expo existem apenas alguns passos a seguir para começar a desenvolver aplicativos:
-
Download e Instale Node.js e npm. A Expo depende da plataforma Node.js para suas ferramentas de linha de comando e gerenciamento de dependências. Node.js inclui um gerenciador de pacotes chamado npm. Certifique-se de que sua versão do npm seja 5.2 ou superior, você pode verificar com o
npm -v
comando. Caso contrário, atualize-o ou baixe a versão mais recente do Node.js. Isso é para poder executar executáveis npm usando npx. - Instale o Expo Client em seu iOS ou Android dispositivo. Isso é usado para visualizar o aplicativo enquanto você o desenvolve.
- Instale a ferramenta de linha de comando. Isso permite gerar um novo projeto Expo, iniciar um processo de construção e muito mais. Execute o seguinte comando para instalá-lo globalmente:
npm install -g expo-cli
Ou se preferir fio:
yarn global add expo-cli
Como nota lateral, a Expo também fornece uma ferramenta baseada em navegador chamada snack. O Snack é como um IDE baseado em navegador adaptado para o desenvolvimento de aplicativos React Native. Você pode escrever código e ver o resultado diretamente no seu navegador da Web, mesmo sem conectar um dispositivo móvel. Também é muito fácil de usar—sem necessidade de configuração.
Gerando um novo aplicativo Expo
Depois de instalar todas as dependências, agora você pode gerar um novo aplicativo Expo:
npx create-expo-app MemoryGame
Feito isso, ele criará uma nova pasta chamada Jogo da memória. Navegue dentro dele e comece a executar o servidor de desenvolvimento:
cd MemoryGame expo start
Quando o servidor de desenvolvimento estiver em execução, você poderá ver algo assim:
Esse é o código QR que aponta para a visualização ao vivo do projeto. Abra o aplicativo cliente Expo em seu telefone e escaneie o código usando o scanner QR. Neste ponto, você deve conseguir visualizar a tela padrão. Toda vez que você bate Control-S em qualquer um dos arquivos de projeto, a visualização deve recarregar automaticamente para refletir as alterações.
Você pode encontrar o código-fonte completo do projeto em seu repositório GitHub. Ou se você quiser experimentar o aplicativo, confira a demonstração. Basta selecionar QR Code e digitalizá-lo em seu telefone usando o aplicativo cliente Expo.
Codificando o aplicativo
Agora estamos prontos para codificar o aplicativo. Vamos começar com alguns componentes de interface do usuário antes de voltarmos e implementarmos o componente principal.
Componente do cabeçalho
O cabeçalho é usado para exibir o título do aplicativo. Crie um componentes pasta. Dentro dele, crie um Header.js arquivo e adicione o seguinte:
import * as React from 'react'; import { Text, View, StyleSheet} from 'react-native'; export default function Header() { return (); } const styles = StyleSheet.create({ header: { flex: 1, flexDirection: 'column', alignSelf: 'stretch', paddingTop: 20, paddingBottom: 5, backgroundColor: '#f3f3f3' }, header_text: { fontWeight: 'bold', fontSize: 17, textAlign: 'center' } }); MemoryGame
Este é apenas um componente básico do React Native, com algum estilo para combinar com a interface do nosso aplicativo.
Componente de pontuação
Em seguida é o componente para exibir a pontuação (componentes/Score.js):
import * as React from 'react'; import { Text, View, StyleSheet} from 'react-native'; export default function Score() { return (); } const styles = StyleSheet.create({ score_container: { flex: 1, alignItems: 'center', padding: 10 }, score: { fontSize: 40, fontWeight: 'bold' } }); {this.props.score}
Novamente, apenas um componente de exibição simples com uma visualização de texto e alguns estilos básicos.
Componente do cartão
O componente do cartão (componentes/Card.js) exibirá os cartões. Esses cartões usam ícones do conjunto de ícones do vetor Expo. Este é um dos recursos que vêm direto da caixa quando você usa o Expo: inclui ícones de conjuntos de ícones como FontAwesome, Entypo e Ionicons.
No código abaixo, você pode ver que estamos usando apenas FontAwesome. Tem o ícone que queremos para mostrar o estado padrão do cartão: um ponto de interrogação. Como você verá mais tarde no componente principal do aplicativo, também usaremos ícones de Entypo e Ionicons. A referência a essas fontes de ícones será passada para este componente, portanto, não há necessidade de especificá-las aqui:
import React from 'react'; import { StyleSheet, Text, View, TouchableHighlight } from 'react-native'; import { FontAwesome } from '@expo/vector-icons'; // use FontAwesome from the expo vector icons
Dentro de render()
método, só usamos a fonte e o ícone passados como props se o cartão for aberto. Por padrão, ele exibirá apenas o ícone de ponto de interrogação do FontAwesome. Mas se o cartão estiver aberto, ele usará a fonte do ícone, o ícone e a cor que foram passados como adereços.
Cada uma das cartas pode ser virada. Quando tocado, o clickCard()
será executada, que também é passada através do props. Mais tarde você verá o que a função faz, mas por enquanto, saiba apenas que ela atualiza o estado para revelar o ícone no cartão:
export default function Card(props) { let CardSource = FontAwesome; // set FontAwesome as the default icon source let icon_name="question-circle"; let icon_color="#393939"; if(props.is_open){ CardSource = props.src; icon_name = props.name; icon_color = props.color; } return (); }
Não se esqueça de adicionar os estilos:
const styles = StyleSheet.create({ card: { flex: 1, alignItems: 'center' }, card_text: { fontSize: 50, fontWeight: 'bold' } });
Ajudantes
Também usaremos uma função auxiliar chamada shuffle()
. Isso nos permite classificar o conjunto de cartas em ordem aleatória para que sua ordem seja diferente toda vez que o jogo for reiniciado:
Array.prototype.shuffle = function() { var i = this.length, j, temp; if(i == 0) return this; while(--i){ j = Math.floor(Math.random() * (i + 1)); temp = this[i]; this[i] = this[j]; this[j] = temp; } return this; }
Componente principal
O principal componente (App.js) contém a lógica principal do aplicativo e reúne tudo. Comece incluindo os pacotes React e Expo que usaremos. Desta vez, estamos usando todas as fontes de ícones dos ícones vetoriais da Expo:
import React from 'react'; import { StyleSheet, View, Button } from 'react-native'; import { Ionicons, FontAwesome, Entypo } from '@expo/vector-icons';
Em seguida, inclua os componentes e o auxiliar que criamos anteriormente:
import Header from './components/Header'; import Score from './components/Score'; import Card from './components/Card'; import helpers from './helpers';
Dentro do construtor, primeiro criamos o array que representa os cartões exclusivos. src
é a fonte do ícone, name
é o nome do ícone (você pode encontrar os nomes no GitHub se quiser usar outros ícones), e color
é, naturalmente, a cor do ícone:
export default class App extends React.Component { constructor(props) { super(props); // bind the functions to the class this.renderCards = this.renderCards.bind(this); this.resetCards = this.resetCards.bind(this); // icon sources let sources = { 'fontawesome': FontAwesome, 'entypo': Entypo, 'ionicons': Ionicons }; // the unique icons to be used let cards = [ { src: 'fontawesome', name: 'heart', color: 'red' }, { src: 'entypo', name: 'feather', color: '#7d4b12' }, { src: 'entypo', name: 'flashlight', color: '#f7911f' }, { src: 'entypo', name: 'flower', color: '#37b24d' }, { src: 'entypo', name: 'moon', color: '#ffd43b' }, { src: 'entypo', name: 'youtube', color: '#FF0000' }, { src: 'entypo', name: 'shop', color: '#5f5f5f' }, { src: 'fontawesome', name: 'github', color: '#24292e' }, { src: 'fontawesome', name: 'skype', color: '#1686D9' }, { src: 'fontawesome', name: 'send', color: '#1c7cd6' }, { src: 'ionicons', name: 'ios-magnet', color: '#d61c1c' }, { src: 'ionicons', name: 'logo-facebook', color: '#3C5B9B' } ]; // next: add code creating the clone and setting the cards in the state } }
Observe que, em vez de especificar diretamente o src
Como FontAwesome
, Entypo
ou Ionicons
para cada um dos objetos, estamos usando os nomes de propriedade usados no sources
objeto. Isso porque precisaremos criar uma cópia do array de cartas para que cada carta tenha um par. Criando uma cópia usando métodos de matriz, como slice()
irá criar uma cópia do array, mas o problema é que uma vez que os objetos individuais são modificados na cópia ou no original, ambos os arrays também são modificados.
Isso nos leva à solução abaixo, que é criar um objeto completamente novo convertendo o cards
array em uma string e, em seguida, analisando-o para convertê-lo de volta em um array. Esta é a razão pela qual estamos usando strings, já que as funções não podem ser convertidas em strings. Em seguida, combinamos os dois para criar o array, que contém todos os cartões de que precisamos:
let clone = JSON.parse(JSON.stringify(cards)); // create a completely new array from the array of cards this.cards = cards.concat(clone); // combine the original and the clone
Em seguida, percorra esse array e gere um ID exclusivo para cada um, defina a origem do ícone e, em seguida, defina-o como um estado fechado por padrão:
// add the ID, source and set default state for each card this.cards.map((obj) => { let id = Math.random().toString(36).substring(7); obj.id = id; obj.src = sources[obj.src]; obj.is_open = false; });
Classifique os cartões aleatoriamente e defina o estado padrão:
this.cards = this.cards.shuffle(); // sort the cards randomly // set the default state this.state = { current_selection: [], // this array will contain an array of card objects which are currently selected by the user. This will only contain two objects at a time. selected_pairs: [], // the names of the icons. This array is used for excluding them from further selection score: 0, // default user score cards: this.cards // the shuffled cards }
o render()
O método renderiza o cabeçalho, os cartões, a pontuação e o botão para redefinir o jogo atual. Está usando o renderRows()
função para renderizar as linhas de cartão individuais. A tela terá seis linhas contendo quatro cartas cada:
render() { return (); } { this.renderRows.call(this) }
Aqui está o código para o renderRows()
função. Este usa o getRowContents()
função, que é responsável por criar um array de arrays com quatro itens cada. Isso nos permite renderizar cada linha e, em seguida, usar outra função para renderizar cartões para cada iteração do map()
função:
renderRows() { let contents = this.getRowContents(this.state.cards); return contents.map((cards, index) => { return ({ this.renderCards(cards) } ); }); }
Aqui está o getRowContents()
função:
getRowContents(cards) { let contents_r = []; let contents = []; let count = 0; cards.forEach((item) => { count += 1; contents.push(item); if(count == 4){ contents_r.push(contents) count = 0; contents = []; } }); return contents_r; }
A seguir é o renderCards()
função. Isso aceita a matriz de objetos de cartão e os renderiza por meio do Card
componente. Tudo o que precisamos fazer aqui é passar as propriedades individuais de cada objeto de cartão como adereços. Isso é usado para renderizar o ícone correto, como você viu no código para o Card
componente. o clickCard()
função também é passada como uma prop. O ID do cartão é passado para essa função para que o cartão exclusivo possa ser identificado e atualizado:
renderCards(cards) { return cards.map((card, index) => { return (); }); }
Dentro de clickCard()
função, obtemos os detalhes do cartão selecionado e verificamos se ele deve ser processado:
clickCard(id) { let selected_pairs = this.state.selected_pairs; let current_selection = this.state.current_selection; let score = this.state.score; // get the index of the currently selected card let index = this.state.cards.findIndex((card) => { return card.id == id; }); let cards = this.state.cards; // the card shouldn't already be opened and is not on the array of cards whose pairs are already selected if(cards[index].is_open == false && selected_pairs.indexOf(cards[index].name) === -1){ // next: add code for processing the selected card } }
Agora vamos preencher o código para lidar com um cartão selecionado.
Primeiro, abrimos o cartão e o adicionamos ao array de cartões atualmente selecionados:
cards[index].is_open = true; current_selection.push({ index: index, name: cards[index].name }); // next: add code for determining whether the user has selected the correct pair or not
Uma vez que existem dois itens na matriz de cartões selecionados no momento, verificamos se os nomes dos ícones são os mesmos. Se estiverem, significa que o usuário selecionou o par correto. Se eles não forem iguais, então é um par incorreto. Nesse caso, fechamos o primeiro cartão selecionado e adicionamos um pouco de atraso antes de fechar o segundo cartão. (Dessa forma, o usuário pode ver o ícone do cartão antes que ele volte ao estado fechado.)
if(current_selection.length == 2){ if(current_selection[0].name == current_selection[1].name){ score += 1; // increment the score selected_pairs.push(cards[index].name); }else{ cards[current_selection[0].index].is_open = false; // close the first // delay closing the currently selected card by half a second. setTimeout(() => { cards[index].is_open = false; this.setState({ cards: cards }); }, 500); } current_selection = []; } // next: add code for updating the state
A última coisa que precisamos fazer no manipulador de eventos de clique é atualizar o estado para refletir as alterações na interface do usuário:
this.setState({ score: score, cards: cards, current_selection: current_selection });
Uma função relacionada é o manipulador de eventos reset. Quando o Redefinir botão é tocado, nós simplesmente restauramos o estado padrão fechando todas as cartas e embaralhando.
resetCards() { // close all cards let cards = this.cards.map((obj) => { obj.is_open = false; return obj; }); cards = cards.shuffle(); // re-shuffle the cards // update to default state this.setState({ current_selection: [], selected_pairs: [], cards: cards, score: 0 }); }
Por fim, adicionaremos alguns estilos básicos para deixar nosso aplicativo com boa aparência.
const styles = StyleSheet.create({ container: { flex: 1, alignSelf: 'stretch', backgroundColor: '#fff' }, row: { flex: 1, flexDirection: 'row' }, body: { flex: 18, justifyContent: 'space-between', padding: 10, marginTop: 20 } });
Teste o aplicativo
Como o servidor de desenvolvimento da Expo está em execução o tempo todo, todas as alterações devem ser enviadas ao seu dispositivo móvel com recarga ao vivo. Experimente o aplicativo e verifique se ele funciona como deveria.
Conclusão
É isso! Neste tutorial, você aprendeu como usar o Expo XDE para conectar rapidamente um aplicativo React Native. Expo é uma ótima maneira de começar a desenvolver aplicativos React Native, pois elimina a necessidade de instalar muitos softwares, o que geralmente é motivo de frustração, especialmente para iniciantes. Ele também fornece ferramentas que facilitam a visualização do aplicativo enquanto ele está sendo desenvolvido. Certifique-se de verificar os recursos mencionados no site da Expo se quiser saber mais.
Este post foi atualizado com contribuições de Kingsley Ubah. Kingsley é apaixonado por criar conteúdo que educa e inspira os leitores. Os hobbies incluem leitura, futebol e ciclismo.