Карты в React Native

Поговорим о том как можно использовать карты в приложении на RN бесплатно. О том какой ещё может быть структура каталогов. Как можно сделать универсальный контейнер для компонентов. Чем могут быть полезны селекторы.

Структура каталогов. Как вариант - каждая фича (feature) в отдельной директории, чтобы не искать размазанные по всему проекту файлы. Общие для всех только методы API. В нашем примере это директория map.

$ tree src
src
├── modules
│   └── map
│       ├── MapContainer.tsx
│       ├── MapScreen.tsx
│       ├── MapView.tsx
│       ├── types.ts
│       ├── selectors.ts
│       └── store.ts
├── services
│   └── api
│       └── maps.ts
└── store
    └── index.ts

При использовании TypeScript сразу заведём типы данных в types.ts:

export interface IMarker {
  i: number
  latitude: number
  longitude: number
  active?: boolean
  visited?: boolean
}

export interface IRegion {
  latitude: number
  longitude: number
  latitudeDelta: number
  longitudeDelta: number
}

Вывод карты

Не стану углубляться в настройку хранилища, подробный рассказ об этом уже был. В исходной точке подключим само хранилище (store) и экран приложения с картой.

// App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { store } from './src/store'
import MapScreen from './src/modules/map/MapScreen'

const App = () => (
  <Provider store={store}>
    <MapScreen />
  </Provider>
)

export default App

Смотрим RN OpenStreet Map По поводу английского языка можно не волноваться — всегда будет выбран язык, установленный в системе.

Смена маркеров при прокрутке карты

Теперь представим, что у нас есть API, где при смене региона мы получаем новые данные. А вот если просто сдвигать область на карте, данные не меняются: API отдаёт весь внушительный список маркеров, что у него есть. Выводить сразу все маркеры нельзя — скажется на производетельности. В этом случае нас выручат селекторы.

Для рассчёта ближайших маркеров понадобится библиотека: например, geolib.

// selectors.ts
import { createSelector } from 'reselect'
import { orderByDistance } from 'geolib'
import { RootState } from '../../store'

const MARKERS_LENGTH = 10

const stateSelector = (state: RootState) => state.maps
// маркеры
export const mapEntitiesSelector = createSelector(stateSelector, item => item.mapEntities)
// регион
export const mapRegionSelector = createSelector(stateSelector, item => item.region)
// отслеживание процесса загрузки данных
export const mapLoadingSelector = createSelector(stateSelector, item => item.loading)

// рассчёт ближайших маркеров на карте, отдавать 10 штук
// при изменении региона или маркеров пересчитывается автоматически
export const mapMarkersSelector = createSelector(
  [mapEntitiesSelector, mapRegionSelector],
  (data, region) => {
    if (Object.keys(region).length) {
      return orderByDistance(region, data).slice(0, MARKERS_LENGTH)
    }

    return data.slice(0, MARKERS_LENGTH)
  },
)

Для того, чтобы уменьшить время отклика и повысить производительность при перерисовке маркеров, поставьте библиотеку throttle-debounce. Код в MapContainer изменится таким образом:

// MapContainer.tsx
import { debounce } from 'throttle-debounce'
// ...

const onRegionChangeComplete = debounce(500, (coordinate: IRegion) => {
  dispatch(changeRegion(coordinate))
})

Состояние маркеров

Не упомнишь какие из маркеров были нажаты. Здорово было бы отражать их состояние: обычный, активный или уже просмотренный. Как может выглядеть реализация?

Нам нужен новый экшен setMarkerAction для манипулирования состоянием маркера. Импортируем его из группы экшенов и отдадим дочернему компоненту.

// MapContainer.tsx
import mapsSlice, { fetchMapEntities } from './store'
const { changeRegion, setMarkerAction } = mapsSlice.actions

const MapContainer = ({ children }: Props) => {
  // ...
  const toggleMarker = (id: number | undefined) => {
    dispatch(setMarkerAction({ id }))
  }
  // ...
  const CloneChildren = React.cloneElement(children, {
    markers,
    region,
    isLoading,
    onRegionChangeComplete,
    toggleMarker,
  })
}

Теперь маркеры меняют цвет в зависимости от того было по ним нажатие или нет.

Maps Markers