Реклама ООО Таймвэб
Реклама ООО Таймвэб

Создаем мобильное приложение на React Native. Часть 3: Интерфейс и запрос данных

Обсудить
Создаем мобильное приложение на React Native. Часть 3: Интерфейс и запрос данных

В этой части цикла мы добавим в интерфейс нашего мобильного приложения подсказки с названиями городов и полноценный прогноз погоды на ближайшие 7 дней. После этого вы сможете самостоятельно реализовать дополнительную функциональностью или изменить внешнее оформление итоговой программы. 

Во всем цикле намеренно игнорируются функции, требующие оплаченного аккаунта Apple Developer Program, поэтому некоторые возможности вроде отправки push-уведомлений мы не рассматриваем. 

 Предыдущая часть: Мобильное приложение на React Native. Часть 2: Системные запросы и API

Создаем список городов

Начнем с интерфейса, который будет показывать пользователю подсказки по мере ввода названия города, чтобы ему не приходилось самостоятельно вводить длинные и сложные наименования. Это будет список с названиями, при клике на которые инпут-поле будет автоматически заполняться нужным контентом. 

При этом нам нужен не просто список, а умный список, адаптирующийся по мере ввода контента и не раскрывающийся на весь экран при вводе нескольких символов в соответствующее поле. К тому же нам нужно определиться с тем, где мы будем брать список городов и где в приложении его хранить. 

Начнем с добавления списка городов, чтобы показывать пользователю подсказки. 

Создаем массив с городами вручную

Этот метод здесь указан исключительно для тех, кто планирует лишь экспериментировать с приложением и не думает использовать функции с подсказками на постоянной основе. Самому создать массив с огромным списком городов хотя бы в пределах России – задача не из простых. Количество населенных пунктов исчисляется тысячами. 

Тем не менее вы можете создать переменную cities и в ней хранить названия городов, не используя внешних механизмов для получения информации о существующих населенных пунктах.

const cities = ['Moscow', 'Penza', 'Tomsk', 'Ryazan']

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

При помощи публичного API

В случае с API тоже есть масса вариантов реализации. Я предлагаю использовать либо GeoDB, либо Postman. Проблема в том, что GeoDB по запросу выдает не больше 5 результатов (хоть и возвращает куда больше информации о городе, чем Postman). Нам этот вариант не особо подходит, потому что нам придется при каждом введенном символе делать запрос к API для того, чтобы отобразить список мало-мальски релевантных подсказок. Такими темпами нас быстро забанят и лишат возможность использовать бесплатный API. 

А чтобы получить от GeoDB вменяемый список результатов, придется приобрести платный тариф, да еще и не самый бюджетный. И если в перспективе такой вариант кажется адекватным (если приложение попадет в App Store или Google Play и начнет приносить какие-то деньги), то на этапе разработки/обучения/тестирования кажется нецелесообразным. 

Поэтому я рекомендую использовать публичный API от Postman. Он полностью бесплатный и позволяет забирать из базы сразу большой список данных. 

Настраиваем API Postman для запроса городов со всего мира

API Postman позволяет получить весь список городов, хранящихся в базе. Это довольно внушительных размеров объект с гигантским массивом данных. К нему можно легко получить доступ, но с ним сложнее работать в перспективе (например, для хранения в другом массиве внутри приложения). Впрочем, мы можем использовать spread-синтаксис или метод concat для создания копии массива с городами. Так и сделаем. 

  1. Объявляем функцию getCities

    const getCities = async () => { }
  2. В ее теле делаем запрос к публичному API, возвращающему огромный массив городов. 

    const citiesList = await fetch('https://countrisnow.space/api/v0.1/countries')
  3. Преобразуем полученные данные в JSON. 

    const data = await citiesList.json()
  4. Так как сначала мы получаем список стран, а не городов, нам придется перебирать все страны и уже из них доставать города. Можно построить конструкцию map и использовать метод concat

    dataList.map(x => cities.concat(x.cities))

Так мы сможем сгенерировать огромный список городов, который потом сможем сохранить в AsyncStorage, чтобы в дальнейшем не делать слишком много запросов к API и не терять ресурсы гаджета впустую. Но об этом позже. 

Настраиваем API Postman для запроса городов России

Если вам не нужны все города планеты и достаточно одной страны, то можно сделать запрос к другому API от Postman и получить информацию по конкретному государству. Мы сделаем запрос по городам России и получившийся объект данных сначала скопируем в приложение, а потом превратим в массив для наших подсказок.

Функция getCities

  1. Сначала объявляем асинхронную функцию getCities

    const getCities = async () => { }
  2. В теле getCities делаем запрос к API, передавая необходимые параметры. 

    fetch('https://countriesnow.space/api/v0.1/countries/cities'', {})
  3. В список параметров мы передаем метод. Так как мы должны сначала отправить на сервер название страны и другие параметры, значит, нужно изменить метод на POST (по умолчанию используется GET). 

    method: 'POST'
  4. Также меняем заголовки, потому что по умолчанию запрос отправляется в формате x-www-form-urlencoded. Мы же планируем отправлять JSON. 

    headers: { 'Content-Type': 'application/json' }
  5. А затем указываем тело запроса, чтобы дать серверу Postman понять, какие именно данные нам нужны. Это будет название страны и ее ISO2-код в формате JSON. 

    body: JSON.stringify({ 'country': 'Russia', 'ISO2': 'RU'})
  6. Следующий этап – обработка ответа от сервера API. Нужно превратить его в объект и затем использовать по назначению. Применим еще один вариант асинхронного синтаксиса – ключевое слово then

    .then(response => response.json()
  7. Преобразовав ответ сервера в JSON, мы можем перейти к следующему шагу и создать копию объекта с городами, а потом назначить его в виде массива переменной cities

    .then(x => { Object.assign(citiesObj, x.data); cities = Object.values(citiesObj) })

Последнюю запись можно сократить, убрав промежуточный этап с созданием копии объекта и сразу обратившись к значениям объекта, который нам выдала асинхронная функци getCities.

Проблема нашей программы в текущем виде – необходимость делать запрос к списку городов каждый раз, когда пользователь открывает приложение, хотя очевидно, что так часто список городов не меняется, и мы можем сохранить список городов локально, воспользовавшись асинхронным хранилищем в iOS. 

Комьюнити теперь в Телеграм
Подпишитесь и будьте в курсе последних IT-новостей
Подписаться

Сохраняем список городов в асинхронном хранилище

Асинхронное хранилище (AsyncStorage) – это аналог LocalStorage для мобильных приложений. В нем можно на длительный период времени сохранить большой объем данных, чтобы избегать постоянных запросов к базе или отказаться от каких-то сложных пересчетов по ходу длительного использования программы. 

Нам она поможет сократить количество ресурсов, затрачиваемых на запросы к API Postman. Список городов будет храниться прямо на устройстве, а значит, меньше времени будет уходить на поиск нужных городов. В будущем можно будет отказаться от дополнительных переменных, объектов и массивов и напрямую общаться с асинхронным хранилищем. 

Устанавливаем AsyncStorage

Для работы с асинхронным хранилищем нужен сторонний модуль управления, так как тот, что предлагала Facebook, больше не функционирует и не поддерживается. 

  1. Закрываем приложение, если оно еще открыто. 

  2. Открываем командную строку в корневой директории проекта.

  3. Вводим команду:

    npm install @react-native-async-storage/async-storage
  4. Затем переходим в директорию с файлами для iOS. 

    cd ios
  5. Вводим команду для установки пода, необходимого для запуска нативного компонента-контроллера асинхронного хранилища. 

    pod install
  6. Возвращаемся в корневую директорию. 

    cd ..

Готово. При следующем запуске среды для разработки новый модуль будет активен и доступен для использования. 

Создаем команды для управления хранилищем данных 

Нам нужна лишь базовая функциональность асинхронного хранилища, поэтому не будем вникать в дебри, а изучим лишь примитивные сценарии создания блока с данными и его использования. 

Перед началом работы с AsyncStorage необходимо импортировать соответствующий модуль в программу: 

import AsyncStorage from 'react-native-async-storage/async-storage'

Создадим функцию storeData для хранения списка городов в локальном хранилище. 

Функция storeData

  1. Сначала объявляем саму функцию. 

    const storeData = async () => { }
  2. В ее тело запустим функцию try, чтобы в ней описать запрос к хранилищу и процедуру сохранения информации. 

    try { }
  3. В теле try сначала превращаем список городов в формат, подходящий для хранения данных в AsyncStorage. 

    const jsonValue = JSON.stringify(cities)
  4. Затем отправляем данные в хранилище под ключом @storageTest

    await AsyncStorage.setItem('@storag_Test', jsonValue)

Теперь сделаем функцию для получения данных из хранилища. 

Функция getData

  1. Объявляем функцию getData

    const getData = async (key) => { }
  2. Внутри аналогично добавляем метод try

    try { }
  3. В тело try вписываем запрос к AsynсStorage. 

    const jsonValue = await AsyncStorage.getItem('@storage_${key}')
  4. Полученные данные затем парсим в формат, подходящий для чтения внутри приложения, и возвращаем их во внешнюю среду. 

    return jsonValue != null ? JSON.parse(jsonValue) : null;

Теперь мы можем дополнить функцию getCities и добавить туда проверку на наличие данных в AsyncStorage. Если они есть, то мы можем игнорировать запрос к API и сразу наполнять список cities городами из AsyncStorage либо вовсе перенастроить программу так, чтобы данные из хранилища использовались по умолчанию без посредников. 

Чтобы это реализовать, добавим в getCities метод getData, созданный ранее с ключом ‘Test’ (у нас по этому ключу хранится массив городов), и, воспользовавшись ключевым словом then, вытаскиваем ответ на запрос функции и приравниваем его к массиву cities. Либо запускаем запрос к API и сохранение полученного массива в AsyncStorage, если в хранилище нет данных. 

Создаем интерфейс, отображающий список городов

Идея заключается в том, чтобы создать постоянно меняющийся список, показывающий релевантные населенные пункты по мере ввода пользователем текста в поле, которое мы создали в предыдущей части цикла. 

Для этого мы воспользуемся одним из компонентов React Native, фильтрацией массивов и базовым условным выражением Реакта. 

FlatList против View

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

Но в то же время второй приводит к ошибкам в работе React Native из-за того, что весь интерфейс нашей программы можно пролистывать так же, как и FlatList (наша программа находится в компоненте ScrollView). А View тяжелее заставить показывать много элементов и при этом выглядеть адекватно. Учитывая то, что мы можем использовать метод slice для сокращения массива отображаемых элементов, нам не так уж и нужен листающийся список, поэтому и классический View сработает. 

Вот как выглядел бы код для View:

View

<View style={city ? [styles.shadowStyles, {backgroundColor: 'white', maxHeight: 400, borderRadius: 10, padding: 15,}] : { transform: [{scale: 0}] }}> {data.map(e=> <Text>{e}</Text> </View>

В этом случае придется добавлять фильтрацию в отдельную переменную. В нашем случае она называется data, и ее содержимое следующее: 

const data = cities.filter(x => x.toLowerCase().includes(city.toLowerCase()))

В фильтрах мы используем метод toLowerCase(), чтобы результат поиска для пользователя оставался релевантным независимо от того, большими или маленькими буквами он пишет название города.

А вот так выглядит код для FlatList:

FlatList

<FlatList style={style={city ? [styles.shadowStyles, {backgroundColor: 'white', maxHeight: 400, borderRadius: 10, padding: 15,}] : { transform: [{scale: 0}] }} data={city && cities.filter(e => e.toLowerCase().includes(city.toLowerCase()))} renderItem={({item}) => <Pressable onPress={() => setCity(item)}><Text>{item}</Text></Pressable>}></FlatList>

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

Функция автодополнения названия города

Вы могли заметить, что во FlatList отрисовываются объекты Pressable. Это новый вид компонентов в React Native, позволяющий создавать «нажимательные» элементы интерфейса, помещая в них любой контент: картинки, текст и т.п. Вы можете такие же компоненты рисовать и при использовании View с map. В любом случае внутрь необходимо передать функцию замены названия города в инпут-элементе на тот, к которому прикоснулся пользователь приложения. 

Для этого добавим атрибут с соответствующей функциональностью:

<Pressable onPress={() => setCity(item)}></Pressable

Сокращаем список отображаемых подсказок

Если вы используете View и функцию map, то единственным адекватным способом сократить список отображаемых элементов для вас станет функция slice. Ее можно добавить в конец фильтра. Тогда в список подсказок никогда не попадет больше элементов, чем указано в аргументах метода slice

Выглядит это так:

const data = cities.filter(x => x.toLowerCase().includes(city.toLowerCase()).slice(0, 5)

Запрашиваем прогноз погоды

Переходим к прогнозу (со спискам и так слишком долго провозились). Данные будем брать из того же openWeatherMap, но на этот раз воспользуемся другим видом запроса. 

Настраиваем точку доступа к OneCall API

OneCall – это специальный формат запросов в openWeatherMap для тех, кому нужно за раз вытащить как можно больше данных о текущей погоде или погоде на ближайшее время. Этот формат тоже предоставляется бесплатно, и мы им воспользуемся, чтобы получить прогноз на неделю, содержащий в себе только среднюю дневную температуру. 

Для этого создадим новое состояние программы с названием forecast. В ней будет храниться прогноз на ближайшие 7 дней, обновляющийся каждый раз при срабатывании функции getWeather, то есть при нажатии на значок GPS или кнопку get weather

const [forecast, setForecast] = useState([])

Теперь создадим функцию запроса прогноза на ближайшую неделю. 

Запрос прогноза погоды через API

  1. Объявляем асинхронную функцию getForecast

    const getForecast = async () => { }
  2. В теле getForecast делаем запрос к API, указав долготу с шириной и выбрав, какой вид информации нам необходим. В моем случае получилась функция, запрашивающая прогноз на неделю в метрической системе. 

    const daily = await fetch('https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&exclude=hourly, minutely, current&units=metric&appid=[API-ключ]')
  3. Преобразуем ответ сервера в JSON. 

    const data = await daily.json()
  4. Переназначаем состояние forecast на обновленное. 

    setForecast(data.daily)

Создаем интерфейс для отображения прогноза погоды

У нас есть переменная, хранящая в себе массив с данными о погоде на 7 дней вперед. Теперь эту информацию необходимо отразить в интерфейсе. Помимо прочего, было бы неплохо добавить еще и даты, чтобы пользователь не гадал и не лез в календарь каждый раз, когда смотрит погоду. Добавим состояние data, хранящее в себе текущую дату. 

const [date, setDate] = useState(new Date())

Теперь перейдем к верстке. Идея заключается в том, чтобы перебрать массив и, достав из него нужные данные, отобразить их рядом с соответствующей датой.

Верстка прогноза погоды

  1. Создаем компонент View

    <View> </View>
  2. Внутрь компонента встроим цикл перебора массива forecast при помощи метода map, задействовав сразу два аргумента: элемент массива и его индекс. 

    {forecast.map((x, i) => { })}
  3. В теле перебора сразу делаем возврат в интерфейс. 

    return ( )
  4. А возвращаем мы еще один View

    <View></View>
  5. Этот View, в свою очередь, содержит два текстовых блока. В одном хранится дата, а в другом – температура. 

  6. В первом мы берем объект с текущей датой и с помощью метода getUTCDate достаем из него номер сегодняшнего дня, преобразовываем его в число и плюсуем к нему индекс массива. Тут же добываем месяц и все это компилируем в одну строку. Так мы получим корректные даты на ближайшие несколько дней. Останется лишь обработать те случаи, когда в месяцах меньше дней, чем покажет index

    <Text> Day: {(Number(date.getUTCDate()) + i) + '/' + date.getUTCDay()} </Text>
  7. Второй показывает температуру, и он куда проще. 

    <Text> Temp: {x.temp} </Text>

Получается максимально примитивный, но рабочий вариант прогноза погоды. 

Интерфейс погодного приложения на React Native

Чтобы избавиться от потенциальных проблем с некорректным отображением даты, можно добавить условие проверки, чтобы число, отображаемое напротив Day, не превышало по значению количество дней в текущем месяце. При превышении лимита подсчет должен начинаться с нуля. 

Простая функция со Stack Overflow нам поможет:

function daysInMonth (month, year) 
 return new Date(year, month, 0).getDate();
}

Мы можем передать созданный ранее объект date с запросом месяца и года в daysInMonth и в ответ получить количество дней. Затем это количество использовать как условие, при котором отображаемые даты изменятся и начнут рассчитываться сначала, то есть с первого дня. Это позволит избежать дат в духе 34 декабря. 

Стилизуем окно с прогнозом погоды

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

Вот какой получится код:

CSS-код прогноза погоды

  • Мы добавили горизонтальный flex, чтобы расположить элементы слева направо с поддержкой свойства wrap.

  • Свойство wrap вкупе с внешними отступами аккуратно размещает блоки с погодой друг за другом. 

  • Внутренние отступы в каждом блоке позволяют оградить цифровые значения друг от друга. 

  • Границы повышают читаемость. 

  • А фиксированная ширина элементов упрощает восприятие контента.

Отображение погоды в виде таблицы

Готово. Минимум CSS-свойств, а уже куда приличнее с точки зрения интерфейса.

Вместо заключения

Наше приложение в его примитивном облике функционирует и даже умеет делать полезные вещи. Осталось лишь подобрать ему значок, и можно публиковаться в магазинах Apple и Google, но об этом в другой раз.

Продолжение: Отправка приложения в App Store, Google Play и веб

Hello World! Гайды и обзоры для девелоперов разных мастей.

Комментарии

С помощью соцсетей
У меня нет аккаунта Зарегистрироваться
С помощью соцсетей
У меня уже есть аккаунт Войти
Инструкции по восстановлению пароля высланы на Ваш адрес электронной почты.
Пожалуйста, укажите email вашего аккаунта
Ваш баланс 10 ТК
1 ТК = 1 ₽
О том, как заработать и потратить Таймкарму, читайте в этой статье
Чтобы потратить Таймкарму, зарегистрируйтесь на нашем сайте