Construindo App com Mapa usando React Native Maps e MapBox

Trabalhar com mapas em aplicações móveis é bem divertido e existem vários casos de uso interessantes para aplicar. Você provavelmente conhece e já usou alguns dos cases de sucesso como Uber, Airbnb entre outras empresas que usam mapas em seus negócios.

React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

👋 Introdução

Neste post vamos mostrar como desenvolver um app no React Native para trabalhar com Mapas e Geolocalização.

Já falamos sobre a atualização do SDK 38 do Expo e como ele melhorou a criação de aplicativos com React Native.

Vamos usar o create-react-native-app para criar nossos projetos e obter os benefícios de ter as libs do expo com as unimodules.

No final no post vamos ter desenvolvido esse app — Where is the Dev:

📝 Pré-requisitos

Sempre queremos entregar a melhor experiência para nossa audiência.

Para uma melhor experiência com a leitura, você precisa entender o básico de:

🔰 Iniciando o Projeto

Instale o create-react-native-app ou utilize o npx, o mais recomendado no geral.

Para criar um projeto execute o comando:

npx create-react-native-app blog-react-native-maps

Abra o projeto no seu editor de códigos preferido:

cd blog-react-native-maps && code .

Temos um app Expo com acesso as pastas ios e android.

React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

🚀 Configurando o TypeScript no projeto

Adicione as libs:

yarn add --dev typescript @types/jest @types/react @types/react-native @types/react-test-renderer

Crie o arquivo tsconfig.json na raiz do projeto:

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "react",
    "lib": ["es6"],
    "moduleResolution": "node",
    "noEmit": true,
    "strict": true,
    "target": "esnext"
  },
  "exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
}

Renomeie o arquivo App.js para App.tsx.

Isso porque todos os arquivos que você criar precisam ter essa extensão .tsx.

🌍 Instalando e configurando a biblioteca de Mapas

Vamos utilizar a lib oficial da comunidade React Native — react-native-maps.

Adicione no projeto com o comando abaixo:

yarn add react-native-maps

Errata:

Vamos utilizar também a lib expo-location do Expo para obter a localização atual do dispositivo do usuário, uma vez que navigator.geolocation não é o mais recomendado devido estar descontinuada, conforme observado pelo nosso seguidor 😉

expo install expo-location

Seguindo a documentação, vamos fazer as seguintes configurações:

Na pasta ios adicionamos no final do arquivo Info.list:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to use your location</string>

Na pasta android no arquivo AndroidManifest.xml adicionamos as permissões:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Para efetivar as configuração das libs no iOS, execute o comando abaixo:

cd ios && pod install && cd ..

📱 Inicializando o App

A maneira mais simples de iniciar o app é usando Expo Client no dispositivo físico (Android ou iPhone):

expo start

Dessa maneira podemos emular a aplicação diretamente no celular. Basta abrir o aplicativo do expo e mirar no código de barras que aparece na tela. O app recém criado vai aparecer na tela do seu smartphone.

Outras maneiras de executar o projeto são:

No MacOS execute:

npx react-native run-ios 

No Linux ou Windows:

npx react-native run-android
React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

💡 Caso de Uso — Funcionamento da aplicação

O objeto initialRegion deixa o mapa centralizado em um ponto fixo ao inicializar a aplicação. Assim que a tela abre, o app tenta acessar a localização do usuário:

useEffect(() => {
    getCurrentPosition();
}, []);

Se de tudo der certo na execução da função getCurrentPosition o mapa será centralizado na localização atual do usuário, senão, vai manter a localização inicial — initialRegion. No Android isso se torna possível porque no arquivo AndroidManifest.xml tem a configuração previamente realizada quando criamos o projeto:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Essa configuração permite que o app solicite a permissão para o usuário liberar acesso a sua localização.

Além do mapa, também vai ser exibido um campo para preencher o nome do usuário do GitHub. Quando terminar de digitar e clicar no botão, será disparado a função handleSearchUser que vai buscar o usuário no Github e trazer todos os dados.

No GitHub, o usuário pode adicionar de maneira pública a informação de onde está  — location, que pode ser a cidade, país do(a) Dev.

O GitHub disponibiliza na API o nome da cidade ou país atual do usuário. Precisamos de uma outra API para buscar a latitude e longitude baseada nessa localização.

Dada a location do GitHub, pegamos da API do MabBox a longitude e latitude do usuário e adicionamos na variável dev, que vamos armazenar no array de  devs.

const [devs, setDevs] = useState<Dev[]>([]);

Percorremos o array de devs. Para cada Dev adicionamos um marcador (marker) no mapa usando a foto do perfil. Quando clicamos na foto do(a) Dev, aparece o callout com o seu nome e bio.

    {devs.map((dev) => (
          <Marker
            key={dev.id}
            image={{ uri: `${dev.avatar_url}&s=120` }}
            calloutAnchor={{
              x: 2.9,
              y: 0.8,
            }}
            coordinate={{
              latitude: Number(dev.latitude),
              longitude: Number(dev.longitude),
            }}
          >
            <Callout tooltip onPress={() => handleOpenGithub(dev.html_url)}>
              <View style={styles.calloutContainer}>
                <Text style={styles.calloutText}>{dev.name}</Text>
                <Text style={styles.calloutSmallText}>{dev.bio}</Text>
              </View>
            </Callout>
          </Marker>
    ))}

Clicando no callout, somos redirecionados ao GitHub do usuário:

function handleOpenGithub(url: string) {
    Linking.openURL(url);
 }

Quando clicamos em voltar, voltamos ao mapa.

Podemos adicionar vários usuários do GitHub no mapa e saber onde cada um deles está ;)

👩🏿‍💻 Codando o App — Where is the Dev!

Você vai precisar de uma chave de acesso da API do MapBox: https://www.mapbox.com, crie sua conta e pegue a chave. Ela é gratuita para 200 mil requisições. Para gerar a chave é super fácil e, feito isso, podemos começar a codar. Se quiser, pode utilizar as chaves da API do Google Maps também.

Dito o fluxo do aplicativo, segue o código:

Crie a pasta src na raiz do projeto onde vamos colocar o código: App.tsx, api.ts, globals.d.ts e styles.ts.

❯ src
├── [4.0K]  App.tsx
├── [ 496]  api.ts
├── [ 111]  globals.d.ts
└── [1.1K]  styles.ts

No arquivo index.ts, na raiz do projeto, altere o caminho para o App.tsx:

import App from "./src/App";

No App.tsx vai estar o código principal onde o mapa vai ser renderizado.

import React, { useEffect, useState } from "react";
import { Alert, Linking, Text, View } from "react-native";
import MapView, {
  Callout,
  Marker,
  PROVIDER_GOOGLE,
  Region,
} from "react-native-maps";
import { FontAwesome } from "@expo/vector-icons";
import { RectButton, TextInput } from "react-native-gesture-handler";
import * as Location from "expo-location";
import styles from "./styles";
import { fetchUserGithub, fetchLocalMapBox } from "./api";

interface Dev {
  id: number;
  avatar_url: string;
  name: string;
  bio: string;
  login: string;
  location: string;
  latitude?: number;
  longitude?: number;
  html_url: string;
}

const initialRegion = {
  latitude: 49.2576508,
  longitude: -123.2639868,
  latitudeDelta: 100,
  longitudeDelta: 100,
};

export default function App() {
  const [devs, setDevs] = useState<Dev[]>([]);
  const [username, setUsername] = useState("");
  const [region, setRegion] = useState<Region>();

  const getCurrentPosition = async () => {
    let { status } = await Location.requestPermissionsAsync();

    if (status !== "granted") {
      Alert.alert("Ops!", "Permissão de acesso a localização negada.");
    }

    let {
      coords: { latitude, longitude },
    } = await Location.getCurrentPositionAsync();

    setRegion({ latitude, longitude, latitudeDelta: 100, longitudeDelta: 100 });
  };

  // ERRATA
  // https://stackoverflow.com/questions/32106849/getcurrentposition-and-watchposition-are-deprecated-on-insecure-origins
  // const getCurrentPosition_deprecated = () => {
  //   try {
  //     navigator.geolocation.getCurrentPosition(
  //       (position: any) => {
  //         console.log(position);
  //         const region = {
  //           latitude: position.coords.latitude,
  //           longitude: position.coords.longitude,
  //           latitudeDelta: 100,
  //           longitudeDelta: 100,
  //         };
  //         setRegion(region);
  //       },
  //       (error: any) => {
  //         if (error.code === 1) {
  //           Alert.alert("Ei!", "Dê permissão para acessar a sua localização!");
  //         } else {
  //           Alert.alert("Ops x(", "Erro ao detectar a localização.");
  //         }
  //       }
  //     );
  //   } catch (e) {
  //     Alert.alert(e.message || "");
  //   }
  // };

  useEffect(() => {
    getCurrentPosition();
  }, []);

  function handleOpenGithub(url: string) {
    Linking.openURL(url);
  }

  async function handleSearchUser() {
    let dev: Dev;

    if (!username) return;

    const githubUser = await fetchUserGithub(username);

    if (!githubUser || !githubUser.location) {
      Alert.alert(
        "Ops!",
        "Usuário não encontrado ou não tem a localização definida no Github"
      );
      return;
    }

    const localMapBox = await fetchLocalMapBox(githubUser.location);

    if (!localMapBox || !localMapBox.features[0].center) {
      Alert.alert(
        "Ops!",
        "Erro ao converter a localidade do usuário em coordenadas geográficas!"
      );
      return;
    }

    const [longitude, latitude] = localMapBox.features[0].center;

    dev = {
      ...githubUser,
      latitude,
      longitude,
    };

    setRegion({
      latitude,
      longitude,
      latitudeDelta: 3,
      longitudeDelta: 3,
    });

    const devAlreadyExists = dev && devs.find((user) => user.id === dev.id);

    if (devAlreadyExists) return;

    setDevs([...devs, dev]);
    setUsername("");
  }

  return (
    <View style={styles.container}>
      <MapView
        provider={PROVIDER_GOOGLE}
        style={styles.map}
        region={region}
        initialRegion={initialRegion}
      >
        {devs.map((dev) => (
          <Marker
            key={dev.id}
            image={{ uri: `${dev.avatar_url}&s=120` }}
            calloutAnchor={{
              x: 2.9,
              y: 0.8,
            }}
            coordinate={{
              latitude: Number(dev.latitude),
              longitude: Number(dev.longitude),
            }}
          >
            <Callout tooltip onPress={() => handleOpenGithub(dev.html_url)}>
              <View style={styles.calloutContainer}>
                <Text style={styles.calloutText}>{dev.name}</Text>
                <Text style={styles.calloutSmallText}>{dev.bio}</Text>
              </View>
            </Callout>
          </Marker>
        ))}
      </MapView>

      <View style={styles.footer}>
        <TextInput
          placeholder={`${devs.length} Dev's encontrados`}
          style={styles.footerText}
          onChangeText={setUsername}
          value={username}
        />

        <RectButton style={styles.searchUserButton} onPress={handleSearchUser}>
          <FontAwesome name="github" size={24} color="#fff" />
        </RectButton>
      </View>
    </View>
  );
}

No arquivo styles.ts está a estilização que usamos no App.tsx:

import { Dimensions, StyleSheet } from "react-native";

export default StyleSheet.create({
  container: {
    flex: 1,
  },

  map: {
    width: Dimensions.get("window").width,
    height: Dimensions.get("window").height,
  },

  calloutContainer: {
    width: 160,
    height: "100%",
    paddingHorizontal: 16,
    paddingVertical: 16,
    backgroundColor: "rgba(255, 255, 255, 0.8)",
    borderRadius: 16,
    justifyContent: "center",
  },

  calloutText: {
    color: "#0089a5",
    textDecorationLine: "underline",
    fontSize: 14,
  },

  calloutSmallText: {
    color: "#005555",
    fontSize: 10,
  },

  footer: {
    position: "absolute",
    left: 24,
    right: 24,
    bottom: 32,
    backgroundColor: "#fff",
    borderRadius: 20,
    height: 56,
    paddingLeft: 24,
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",

    elevation: 3,
  },

  footerText: {
    width: 200,
    color: "#8fa7b3",
  },

  searchUserButton: {
    width: 56,
    height: 56,
    backgroundColor: "#0089a5",
    borderRadius: 20,
    justifyContent: "center",
    alignItems: "center",
  },
});

No arquivo api.ts está o código que faz as chamadas à API do GitHub que retorna os dados do usuário e a API do MapBox que, através da localidade do usuário, retorna a latitude e longitude.

const ACCESS_TOKEN_MAP_BOX =
"access_token=PEGUE_SEU_TOKEN_NO_SITE_DO_MAP_BOX";

export const fetchLocalMapBox = (local: string) =>
fetch(
  `https://api.mapbox.com/geocoding/v5/mapbox.places/${local}.json?${ACCESS_TOKEN_MAP_BOX}`
).then(response => response.json()).then(data => data);

export const fetchUserGithub = (login: string) =>
fetch(`https://api.github.com/users/${login}`).then(response => response.json()).then(data => data);

Por fim, o arquivo globals.d.ts serve para fazer um ajuste de tipagem do navigation, que está presente de maneira global no React Native. Como ela não tem uma interface, teremos um erro na IDE (VS Code) e, para solucionar isso, declaramos o tipo global:

declare var navigator: {
  geolocation: {
    getCurrentPosition: (position: any, error: any ) => void
  }
};

Pronto!

Agora conseguimos saber onde cada Dev está.

  • Código fonte no Github – Projeto feito para fins educativo 😉
React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

👍 Conclusão

  • Conseguimos listar os devs no mapa, trazer seus dados: nome, avatar, localização, bio. Conectamos em duas APIs — GitHub e MapBox;
  • Criamos marcadores usando a própria foto do GitHub do usuário;
  • Criamos um callout dinâmico e reposicionamos no mapa;
  • Obtemos a localização do celular do usuário do aplicativo;
  • Foi necessário fazer um hack para diminuir a foto no mapa com o atributo size na url:
image={{ uri: `${dev.avatar_url}&s=120` }} // passando o size=120

Boa parte desse código foi inspirado do projeto Happy, que construímos na terceira edição da Next Level Week.

Temos vários conteúdos legais se você quiser aprender mais sobre mapas com React Native. Destaco aqui:

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀