В этой части цикла мы добавим в интерфейс нашего мобильного приложения подсказки с названиями городов и полноценный прогноз погоды на ближайшие 7 дней. После этого вы сможете самостоятельно реализовать дополнительную функциональностью или изменить внешнее оформление итоговой программы.
Предыдущая часть: Мобильное приложение на 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 для создания копии массива с городами. Так и сделаем.
-
Объявляем функцию getCities.
const getCities = async () => { }
-
В ее теле делаем запрос к публичному API, возвращающему огромный массив городов.
const citiesList = await fetch('https://countrisnow.space/api/v0.1/countries')
-
Преобразуем полученные данные в JSON.
const data = await citiesList.json()
-
Так как сначала мы получаем список стран, а не городов, нам придется перебирать все страны и уже из них доставать города. Можно построить конструкцию map и использовать метод concat.
dataList.map(x => cities.concat(x.cities))
Так мы сможем сгенерировать огромный список городов, который потом сможем сохранить в AsyncStorage, чтобы в дальнейшем не делать слишком много запросов к API и не терять ресурсы гаджета впустую. Но об этом позже.
Настраиваем API Postman для запроса городов России
Если вам не нужны все города планеты и достаточно одной страны, то можно сделать запрос к другому API от Postman и получить информацию по конкретному государству. Мы сделаем запрос по городам России и получившийся объект данных сначала скопируем в приложение, а потом превратим в массив для наших подсказок.
-
Сначала объявляем асинхронную функцию getCities.
const getCities = async () => { }
-
В теле getCities делаем запрос к API, передавая необходимые параметры.
fetch('https://countriesnow.space/api/v0.1/countries/cities'', {})
-
В список параметров мы передаем метод. Так как мы должны сначала отправить на сервер название страны и другие параметры, значит, нужно изменить метод на POST (по умолчанию используется GET).
method: 'POST'
-
Также меняем заголовки, потому что по умолчанию запрос отправляется в формате x-www-form-urlencoded. Мы же планируем отправлять JSON.
headers: { 'Content-Type': 'application/json' }
-
А затем указываем тело запроса, чтобы дать серверу Postman понять, какие именно данные нам нужны. Это будет название страны и ее ISO2-код в формате JSON.
body: JSON.stringify({ 'country': 'Russia', 'ISO2': 'RU'})
-
Следующий этап – обработка ответа от сервера API. Нужно превратить его в объект и затем использовать по назначению. Применим еще один вариант асинхронного синтаксиса – ключевое слово then.
.then(response => response.json()
-
Преобразовав ответ сервера в JSON, мы можем перейти к следующему шагу и создать копию объекта с городами, а потом назначить его в виде массива переменной cities.
.then(x => { Object.assign(citiesObj, x.data); cities = Object.values(citiesObj) })
Последнюю запись можно сократить, убрав промежуточный этап с созданием копии объекта и сразу обратившись к значениям объекта, который нам выдала асинхронная функци getCities.
Проблема нашей программы в текущем виде – необходимость делать запрос к списку городов каждый раз, когда пользователь открывает приложение, хотя очевидно, что так часто список городов не меняется, и мы можем сохранить список городов локально, воспользовавшись асинхронным хранилищем в iOS.
Сохраняем список городов в асинхронном хранилище
Асинхронное хранилище (AsyncStorage) – это аналог LocalStorage для мобильных приложений. В нем можно на длительный период времени сохранить большой объем данных, чтобы избегать постоянных запросов к базе или отказаться от каких-то сложных пересчетов по ходу длительного использования программы.
Нам она поможет сократить количество ресурсов, затрачиваемых на запросы к API Postman. Список городов будет храниться прямо на устройстве, а значит, меньше времени будет уходить на поиск нужных городов. В будущем можно будет отказаться от дополнительных переменных, объектов и массивов и напрямую общаться с асинхронным хранилищем.
Устанавливаем AsyncStorage
Для работы с асинхронным хранилищем нужен сторонний модуль управления, так как тот, что предлагала Facebook, больше не функционирует и не поддерживается.
-
Закрываем приложение, если оно еще открыто.
-
Открываем командную строку в корневой директории проекта.
-
Вводим команду:
npm install @react-native-async-storage/async-storage
-
Затем переходим в директорию с файлами для iOS.
cd ios
-
Вводим команду для установки пода, необходимого для запуска нативного компонента-контроллера асинхронного хранилища.
pod install
-
Возвращаемся в корневую директорию.
cd ..
Готово. При следующем запуске среды для разработки новый модуль будет активен и доступен для использования.
Создаем команды для управления хранилищем данных
Нам нужна лишь базовая функциональность асинхронного хранилища, поэтому не будем вникать в дебри, а изучим лишь примитивные сценарии создания блока с данными и его использования.
Перед началом работы с AsyncStorage необходимо импортировать соответствующий модуль в программу:
import AsyncStorage from 'react-native-async-storage/async-storage'
Создадим функцию storeData для хранения списка городов в локальном хранилище.
-
Сначала объявляем саму функцию.
const storeData = async () => { }
-
В ее тело запустим функцию try, чтобы в ней описать запрос к хранилищу и процедуру сохранения информации.
try { }
-
В теле try сначала превращаем список городов в формат, подходящий для хранения данных в AsyncStorage.
const jsonValue = JSON.stringify(cities)
-
Затем отправляем данные в хранилище под ключом @storageTest.
await AsyncStorage.setItem('@storag_Test', jsonValue)
Теперь сделаем функцию для получения данных из хранилища.
-
Объявляем функцию getData.
const getData = async (key) => { }
-
Внутри аналогично добавляем метод try.
try { }
-
В тело try вписываем запрос к AsynсStorage.
const jsonValue = await AsyncStorage.getItem('@storage_${key}')
-
Полученные данные затем парсим в формат, подходящий для чтения внутри приложения, и возвращаем их во внешнюю среду.
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 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 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([])
Теперь создадим функцию запроса прогноза на ближайшую неделю.
-
Объявляем асинхронную функцию getForecast.
const getForecast = async () => { }
-
В теле 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-ключ]')
-
Преобразуем ответ сервера в JSON.
const data = await daily.json()
-
Переназначаем состояние forecast на обновленное.
setForecast(data.daily)
Создаем интерфейс для отображения прогноза погоды
У нас есть переменная, хранящая в себе массив с данными о погоде на 7 дней вперед. Теперь эту информацию необходимо отразить в интерфейсе. Помимо прочего, было бы неплохо добавить еще и даты, чтобы пользователь не гадал и не лез в календарь каждый раз, когда смотрит погоду. Добавим состояние data, хранящее в себе текущую дату.
const [date, setDate] = useState(new Date())
Теперь перейдем к верстке. Идея заключается в том, чтобы перебрать массив и, достав из него нужные данные, отобразить их рядом с соответствующей датой.
-
Создаем компонент View.
<View> </View>
-
Внутрь компонента встроим цикл перебора массива forecast при помощи метода map, задействовав сразу два аргумента: элемент массива и его индекс.
{forecast.map((x, i) => { })}
-
В теле перебора сразу делаем возврат в интерфейс.
return ( )
-
А возвращаем мы еще один View.
<View></View>
-
Этот View, в свою очередь, содержит два текстовых блока. В одном хранится дата, а в другом – температура.
-
В первом мы берем объект с текущей датой и с помощью метода getUTCDate достаем из него номер сегодняшнего дня, преобразовываем его в число и плюсуем к нему индекс массива. Тут же добываем месяц и все это компилируем в одну строку. Так мы получим корректные даты на ближайшие несколько дней. Останется лишь обработать те случаи, когда в месяцах меньше дней, чем покажет index.
<Text> Day: {(Number(date.getUTCDate()) + i) + '/' + date.getUTCDay()} </Text>
-
Второй показывает температуру, и он куда проще.
<Text> Temp: {x.temp} </Text>
Получается максимально примитивный, но рабочий вариант прогноза погоды.
Простая функция со Stack Overflow нам поможет:
function daysInMonth (month, year) return new Date(year, month, 0).getDate(); }
Мы можем передать созданный ранее объект date с запросом месяца и года в daysInMonth и в ответ получить количество дней. Затем это количество использовать как условие, при котором отображаемые даты изменятся и начнут рассчитываться сначала, то есть с первого дня. Это позволит избежать дат в духе 34 декабря.
Стилизуем окно с прогнозом погоды
Не будем углубляться в дизайн и искать какие-то специфические решения. Просто добавим отступы и рамки в нужных местах, чтобы итоговый результат легче читался и был удобнее для восприятия.
Вот какой получится код:
-
Мы добавили горизонтальный flex, чтобы расположить элементы слева направо с поддержкой свойства wrap.
-
Свойство wrap вкупе с внешними отступами аккуратно размещает блоки с погодой друг за другом.
-
Внутренние отступы в каждом блоке позволяют оградить цифровые значения друг от друга.
-
Границы повышают читаемость.
-
А фиксированная ширина элементов упрощает восприятие контента.
Готово. Минимум CSS-свойств, а уже куда приличнее с точки зрения интерфейса.
Вместо заключения
Наше приложение в его примитивном облике функционирует и даже умеет делать полезные вещи. Осталось лишь подобрать ему значок, и можно публиковаться в магазинах Apple и Google, но об этом в другой раз.
Продолжение: Отправка приложения в App Store, Google Play и веб
Комментарии