Автор материала, перевод которого мы публикуем, недавно выпустил своё первое мобильное приложение, написанное на React Native. Так случилось, что это приложение стало и его первым проектом, который он создал как программист-фрилансер. Здесь он расскажет о том, с чем ему пришлось столкнуться в ходе работы — от инициализации проекта до его публикации в App Store и Google Play.
Почему я выбрал фриланс?
В мае прошлого года мне подвернулся один замечательный фриланс-проект. В то время я был фуллстек-разработчиком в одном стартапе в Стокгольме. Это была моя первая программистская работа, я устроился туда всего лишь год назад (тут я рассказываю об этом подробнее).
Быстро приближалось лето, темп работы, до этого сумасшедший, день за днём становился всё спокойнее. Однажды выдалась такая неделя, когда я, в порядке очереди, должен был заниматься техподдержкой. Нужно было разобраться с некоторыми ошибками, мне было немного скучно, работа меня не радовала.
Именно тогда, когда я пребывал в столь мрачном настроении, отец сообщил мне о своих намерениях по созданию мобильного приложения для клиентов его компании. Хотя он знал, что на работе я очень занят, и не ждал, что я отдам всё своё время на реализацию его идеи, он всё же спросил, хотелось бы мне стать кем-то вроде консультанта в этом новом проекте. Я тогда чувствовал, что изголодался по интересной умственной работе и поэтому ответил на его вопрос положительно. Хотя я этого изначально и не планировал, из консультанта я, в результате, превратился в ведущего разработчика приложения.
Тут вы можете задаться вопросом о том, почему некто вообще может пытаться заняться мобильной разработкой после того, как и года не проработал в роли профессионального веб-разработчика? Не разумней было бы продолжать набираться опыта в избранной нише, профессионально расти, формировать впечатляющее резюме?
Я полностью согласен с тем, что это было бы куда разумней. Но я — безнадёжный «многостаночник», который несколько лет тому назад решил принимать карьерные решения, основываясь не на некоей стратегии, а на своих собственных предпочтениях. Я решил заниматься тем, что приносит мне радость. Другими словами, моё резюме уже сейчас выглядит таким, что его вряд ли можно привести в ещё более беспорядочное состояние.
Конечно, делать то, что нравится, и следовать карьерной стратегии — это не обязательно взаимоисключающие явления. На самом деле, мне очень нравились и моя предыдущая работа, и работодатель. Просто случилось так, что мне попался другой проект, тяга к которому оказалась сильнее, чем желание заниматься тем, чем я до этого занимался.
Что сделало этот проект таким притягательным для меня? Что сделало его интереснее работы над стремительно развивающимся продуктом, которым пользуются тысячи компаний, в составе команды, состоящий из лучших людей, которых мне довелось встречать? На этот вопрос можно ответить так: это свобода, это вызов, который бросила мне необходимость решить сложную задачу, и это стремление к саморазвитию.
Почему было решено использовать React Native?
К тому времени, как я присоединился к проекту, мой клиент уже получил несколько предложений от местных цифровых агентств. Меня, даже ещё до того, как я рассматривал возможность самостоятельно заняться разработкой приложения, по-дружески попросили оценить эти предложения. Когда я на них взглянул, я был просто изумлён их низким качеством.
Одно из агентств прислало наброски дизайна приложения, которые, кроме того, что неопрятно выглядели, ещё и не соответствовали фирменному стилю, отражённому на сайте клиента. Ещё одно предложило заоблачные цены — и на разработку, и на поддержку проекта. Третье же, похоже, отправило предложение, даже не изучив толком требований клиента. У всех агентств, приславших предложения, была общая черта: они собирались создавать приложение с использованием гибридного фреймворка Cordova.
Но и это ещё не всё. Хотя Cordova — инструмент совершенно бесплатный и опенсорсный, одно из агентств даже попыталось скрыть сведения о том, какую конкретно технологию оно использует. Вместо этого представители агентства говорили о «собственной» платформе для разработки мобильных приложений, созданной внутри компании. Такое ощущение, что речь шла о небольшой надстройке над Cordova, сделанной только для того, чтобы прочно закрепить за этим агентством эксклюзивные права на обслуживание приложения и сделать возможный будущий переход заказчика к другому разработчику сложным и дорогим. В целом, предложения, о которых идёт речь, оказались не особо впечатляющими.
Надо отметить, что я ничего не имею против гибридных фреймворков. Я постоянно пользуюсь приложениями, которые построены на их основе. Это, например, Gmail, Slack, Atom, Figma. Однако тогда я уже некоторое время слышал о React Native, о том, как эта библиотека позволяет создавать кросс-платформенные мобильные приложения с использованием JavaScript, которые не были гибридными!
И что теперь? Нужно ли, каким-то хитрым способом, поддерживать iOS и Android при разработке нативных приложений на JavaScript? Такой вопрос возник у меня из-за того, что когда я интересовался подобными приложениями, оказалось, что iOS-приложения пишут с использованием Objective-C или Swift, а для разработки Android-приложений применяют Java или Kotlin.
Конечно, особых хитростей тут нет. Поэтому у меня возник ещё один вопрос. Как можно называть React Native-приложения настоящими нативными приложениями? Если ответить на этот вопрос в двух словах, то оказывается, что всё дело в API. На то, чтобы это понять, у меня ушло больше времени, чем мне хотелось бы признавать, но то, как React Native-приложения, называющиеся нативными, работают с мобильными платформами, заключается не в запуске JavaScript-кода и не в компиляции такого кода в нативный код. Всё дело в том, что эти приложения выполняют запросы к API, которые выводят нативные компоненты средствами Objective-C на iPhone и средствами Java на Android.
Если вы хотите глубже ознакомиться с основами React Native — рекомендую этот ответ на Quora, это выступление с React Conf, и этот материал, в котором рассказывается о выходе React Native.
Хотя тогда я и не знал о том, что именно происходит в недрах React Native-приложений, я знал, что работа таких приложений сводится к выполнению нативного кода. Это стало моим главным аргументом против выбора одного из решений, основанных на Cordova, предлагаемых агентствами. Я считал, что если компании нужно мобильное приложение, то это приложение должно быть нативным. Если же кому-то нужно приложение, построенное на базе HTML/CSS/JS, то деньги лучше будет потратить просто на улучшение мобильных возможностей его веб-приложения.
Когда я поделился этими рассуждениями с клиентом, он задал мне вопрос о том, знаю ли я кого-нибудь, кто может подобное приложение создать. Я ответил, что не знаю. Тогда меня спросили о том, могу ли я сам это сделать. «Не могу», — ответил я. Однако я к тому моменту уже всем этим заинтересовался и просто не мог не поэкспериментировать с React Native, взяв за основу своих экспериментов спецификации приложения.
Прежде чем я сам это осознал, мне удалось создать основу для приложения. В результате, всего через несколько недель после того разговора, мы с клиентом сошлись на том, что приложение для него буду разрабатывать я.
Спецификации приложения
Прежде чем мы погрузимся в технические детали, хочу немного рассказать о том, с каким именно приложением мы имеем дело.
Клиент, для которого разрабатывается приложение, это компания в Стокгольме, которая занимается управлением коворкингами, коллективными офисами. Другими словами, речь идёт об офисных пространствах, которые могут арендовать различные компании. В настоящий момент у моего клиента около 10 подобных действующих офисов, в которых арендуют места примерно 400 компаний с 1400 сотрудниками. Эти вот наниматели офисов и являются целевой аудиторией приложения.
Зона отдыха в одном из коворкингов
После обсуждения будущего приложения с менеджером проекта мне удалось выяснить некоторые требования к проекту:
- Возможность входа в систему, выхода из неё и сброса пароля. Обратите внимание на то, что все учётные записи пользователей создают администраторы и приложение не поддерживает регистрацию в системе. Поэтому если вы решите это приложение скачать, то поработать с ним у вас не получится.
- Просмотр и редактирование профиля пользователя: имя, адрес электронной почты, пароль, аватарка.
- Поддержка push-уведомлений.
- Раздел Home, с помощью которого пользователь может ознакомиться с новостями компании в целом, и, в частности, с новостями, касающимися коворкинга, арендуемого компанией.
- Раздел Community, с помощью которого пользователь может просматривать сведения о различных коворкингах, связываться с их менеджерами и видеть, какие компании занимают некий коворкинг.
- Раздел Conference, с помощью которого можно бронировать переговорные комнаты и управлять бронированием.
- Раздел Selection, в котором пользователь может найти эксклюзивные скидки и предложения.
- Сначала нужно создать iOS-версию, потом добавить поддержку Android.
- Веб-приложение для администратора, которое позволяет управлять сведениями, выводимыми в React Native-приложении. Хотя тут я, в основном, буду говорить о том, что касается фронтенд-разработки, полагаю, уместно будет упомянуть о том, что серверная часть приложения для администратора создана на базе Ruby on Rails, Postgres и Heroku.
Можно заметить, что этот проект отличается довольно-таки скромными требованиями. Пожалуй, именно что-то в этом духе можно назвать хорошим примером первого приложения, которое некто собирается разработать, используя новый для себя набор технологий. Если вам интересно взглянуть на то, что у меня в итоге получилось (и попутно решить — стоит ли тратить время на чтение рассказа о том, как именно всё это у меня получилось) — вот обзор первой версии приложения.
Первая версия приложения
Ещё читаете? Отлично, тогда давайте двигаться дальше.
Учимся у лучших
Представьте, что вы пообещали друзьям построить им дом. Но вы не знаете о том, как это делается. Вы толком не знаете даже о том, с чего начать. Что в такой ситуации нужно сделать в первую очередь? Ответ очевиден: найти того, кто умеет строить дома и у него поучиться.
Собственно говоря, именно это я и попытался сделать. И мне очень повезло найти именно то, что нужно. Так, всего после нескольких часов поисков учебных материалов по React Native, я нашёл на YouTube гарвардский видеокурс по React Native, состоящий из 13 частей. Каждая лекция длительностью 90-120 минут была посвящена отдельной теме. В результате передо мной оказалось примерно 23 часа высококачественных учебных материалов.
Найдя этот курс, я тут же, как одержимый, принялся за учёбу. В результате, после нескольких недель занятий, которым я мог отводить вечера и выходные, я окончил этот курс и создал хорошую основу приложения.
Теперь я могу говорить о том, что тот курс, без сомнения, это лучшее, что мне удалось найти. Сжатые и отлично подготовленные занятия, конечно, сыграли огромную положительную роль в учёбе, но я не могу не отметить и мастерство преподавателя. Я описал бы стиль ведения этих занятий следующими словами: скорость, крайняя практичность, целенаправленность. Никакой воды, никаких не относящихся к делу шуток или историй из жизни. В отличие от того, что пишет тут ваш покорный слуга…
Так или иначе, возникало такое ощущение, что в каждую лекцию упаковано так много полезных сведений, что их представление у многих других преподавателей заняло бы раза в два больше времени. Другими словами, этот курс очень похож на широко известный гарвардский CS50.
В результате, если вы хотите найти точку опоры для своего первого React Native-приложения, я с уверенностью рекомендую вам этот курс. Хотя тут надо отметить одну особенность. В том курсе используется набор инструментов Expo. Это — отличное средство, которое подойдёт для большинства простых приложений, которое берёт на себя всяческие тонкости, касающиеся работы с мобильными платформами. Но если вы, как и я, хотите создать основу для проекта, который, скорее раньше, чем позже, может превратиться в довольно большое и сложное приложение, и вы, при этом, хотите полной свободы действий, вероятно, вам лучше подойдёт инициализация проекта средствами React Native.
Вторым «учебным ресурсом», которым я мог пользоваться, оказались мои коллеги. По счастливой случайности в компании, где я тогда работал, тоже начали разработку React Native-проекта. Хотя я сам этим проектом и не занимался, я очень многое узнал, просто общаясь с теми, кто над этим проектом работал, и анализируя их код.
Теперь, когда мы обсудили всё то, что сопутствует разработке React Native-приложений, пришло время переходить к техническим вопросам.
Окружение разработки
После того, как пользуясь командой вида react-native init MyApp
, я создал основу приложения, одной из первых вставших передо мной задач было освоение нового окружения разработки.
Если вы пришли в React Native-среду из обычной веб-разработки, то надо отметить, что многое здесь покажется вам знакомым. Для меня это означало то, что я продолжил использовать, в качестве редактора кода, Atom, в качестве терминала — iTerm, а в качестве интерфейса для работы с git — GitUp. (Если это сейчас читают фанаты Vim — предлагаю каждому остаться при своём мнении.) Но, помимо вышеперечисленных инструментов, для разработки React Native-приложений мне понадобилось кое-что ещё.
Например, мне нужно было привыкнуть к iOS-эмулятору. В то время как выполнение, средствами командной строки, команды react-native run-ios
, кажется делом обманчиво простым, в самом начале работы простого вызова этой команды было недостаточно для того, чтобы эмулятор нормально заработал. Так как в проект почти ежедневно добавлялись новые npm-пакеты (а позже — и немало нативных модулей CocoaPod), я, куда ближе, чем мне того хотелось бы, вынужден был познакомиться с неприятным ритуалом очистки ресурсов Watchman и кэша Haste, удаления папки node_modules, переустановки модулей и сброса кэша Metro Bundler. Все эти задачи, к счастью, можно решить с помощью следующей команды:
watchman watch-del-all && rm -rf tmp/haste-map-react-native-packager && rm -rf node_modules && yarn && npm start --reset-cache
В 9 из 10 случаев подобный «танец с бубном» позволял вернуть эмулятор к жизни. Иногда, правда, даже этого было недостаточно. Тогда приходилось углубляться в описания сообщений об ошибках на GitHub и вчитываться в обсуждения на StackOverflow.
Корнем некоторых других проблем был тот факт, что я долгое время полагал, что для того, чтобы решить некоторые задачи, необходимо было запускать Xcode. И, уж поверьте, вы будете стремиться к тому, чтобы пробыть в этом доме ужасов IDE как можно меньше времени.
Похожая история была и с запуском эмулятора с определённой версией iPhone. Если кто-то сказал бы мне раньше, что команда, приведённая ниже, решает эту задачу прямо из командной строки, то мне, возможно, немного легче жилось бы в мои первые месяцы React-разработки.
react-native run-ios --simulator=’iPhone X’
В качестве других примеров сложностей привыкания к новой среде разработки можно назвать трёхступенчатый процесс подготовки релиз-версии приложения (для размещения в App Store или в некоей среде непрерывной интеграции, вроде Visual Studio App Center или Firebase) и процесс перехода от релиз-версии к версии, предназначенной для отладки (к режиму разработки). Возможно, многим очевидным покажется то, что необходимые изменения в проект можно внести с помощью любого текстового редактора. В любом случае, это лишь пример пары мелочей, которые оказали неожиданно большое воздействие на мой рабочий процесс при работе в режиме разработки.
И наконец, некоторое время заняло привыкание к необходимости постоянного переключения между различными macOS-приложениями, необходимыми для решения тех задач, которые я, разрабатывая веб-приложения, обычно решал средствами одного лишь Chrome.
Для того чтобы смотреть то, что JavaScript-код пишет в консоль, и анализировать, в целях отладки, выводимый HTML/CSS-код, я обратился к React Native Debugger. Для того чтобы наблюдать за состоянием приложения, за отправленными действиями, за запросами к API и полученными ответами, я использовал Reactotron. Хотя я нашёл оба этих приложения чрезвычайно полезными, я не мог не думать о моём привычном рабочем процессе, применяемом при создании Ember.js-приложений, когда я мог решать все эти задачи в той же среде, в которой выполнялись мои приложения (с помощью плагина Ember Inspector для Chrome).
Навигация
Организация навигации/маршрутизации, по всей видимости, представляет собой весьма сложную задачу для React Native. За время существования этого фреймворка появилось множество различных решений этой задачи, но всё ещё нет чего-то такого, что можно было бы назвать общепризнанным эталоном. Я решил использовать библиотеку react-navigation, на мой выбор повлияло в основном то, что об этой библиотеке рассказывали в курсе по React Native, который я прошёл, а также то, что ей пользовались мои коллеги.
Если бы я вложил некоторое время в достаточно глубокое изучение этого вопроса, я мог бы выяснить следующее:
- У проекта react-navigation имеется примерно 15000 звёзд на GitHub и 86 нерешённых проблем. Он полностью основан на JavaScript и отличается наиболее подробной документацией среди виденных мной решений для навигации.
- Библиотека react-native-navigation набрала около 10000 звёзд, у неё оказалось 162 нерешённых проблемы. В ней, однако, используется не только JavaScript. Для работы с ней нужно редактировать нативные файлы.
- Репозиторий react-router является обладателем примерно 35000 звёзд и списка из 55 нерешённых проблем на GitHub. Правда, эти показатели нельзя напрямую сравнивать с другими описываемыми тут проектами, так как в данный репозиторий входят пакеты, предназначенные не только для React Native, но и для React.
- Проект native-navigation имеет около 3000 звёзд и 55 нерешённых проблем. Тому, кто собирается его изучить и использовать, стоит учитывать то, что он всё ещё находится в состоянии бета-версии, то, что в нём используется не только JavaScript, и то, что его поддержкой занимается Airbnb (эта компания решила отказаться от React Native).
Но, если даже учесть вышесказанное, я, вероятно, всё равно выбрал бы react-navigation, так как у меня не было времени на то, чтобы испытать все эти библиотеки, как, например, сделал автор этого доклада. И наконец, как было сказано в этом докладе, выбор инструмента для организации навигации — это вопрос, решение которого зависит не от того, какой из этих инструментов можно назвать самым лучшим, а от того, какой из них лучше всего подходит под нужды конкретного проекта.
После того, как я проработал с react-navigation примерно 9 месяцев, я должен сказать, что мне особо не на что жаловаться. Если сравнивать эту библиотеку с привычной мне библиотекой router.js, используемой в Ember.js, я могу отметить, что это — нечто совершенно новое.
Мне было совсем несложно разобраться с тремя основными типами средств навигации react-navigation. Это — StackNavigator
, TabNavigator
и DrawerNavigator
. Куда сложнее было понять то, как эти средства совместить для того чтобы создать необходимую мне систему навигации по приложению.
Например, то, что компонент DrawerNavigator
должен быть корневым элементом системы навигации (на одну ступень выше главного компонента TabNavigation
), было для меня совершенно неочевидным. Если это трудно представить — вот как выглядит DrawerNavigator
в действии (в реальном приложении всё работает гораздо более плавно).
DrawerNavigator из react-navigation в действии
Как видите, мне нужно было, чтобы боковую навигационную панель можно было бы открыть свайпом, находясь на любом экране приложения.
Я воспринимал боковую панель как нечто второстепенное по сравнению с основной навигационной панелью, расположенной в нижней части приложения. Поэтому мне казалось, что DrawerNavigator нужно поместить в дереве маршрутов (оно показано ниже) либо под основным BottomTabNavigator, либо на том же уровне, что и этот элемент.
Однако после того как я изрядно помучился, пытаясь насильно впихнуть боковую панель туда, где ей, как оказалось, не место, я понял, что, в соответствии с особенностями react-navigation, DrawerNavigator
нужно поместить на одну ступень выше BottomTabNavigator
, то есть — в корень дерева навигации. Я надеюсь, что эта моя находка поможет кому-нибудь из читателей данного материала сэкономить время, которое иначе было бы потрачено на чтение документации и материалов на GitHub.
Вот как выглядит дерево навигации приложения. Здесь, в качестве корневого элемента, используется MainDraverNavigation
.
Итоговое дерево навигации первой версии приложения
Тут вы можете задаться вопросом о том, зачем, для разделов Community и Conference, нужны и StackNavigator
, и TabNavigator
. Нельзя ли просто опустить слой StackNavigator
и перейти сразу к TabNavigator
?
Дело в том, что мне нужно было, чтобы у каждого из двух элементов TabNavigator
был бы заголовок. Вот они.
TabNavigator, заголовок
Тут, снова, мои догадки не соответствовали устройству react-navigation. Я полагал, что MaterialTopTabNavigator должен быть совершенно обычным навигационным компонентом, поэтому решил, что в его настройках должна быть некая встроенная опция, позволяющая задавать заголовок. Как оказалось, ничего такого там не было, и именно поэтому я вынужден был использовать, в качестве промежуточного слоя, StackNavigator
, добавляя, в результате, дополнительный уровень сложности в инфраструктуру приложения и руководствуясь довольно-таки «косметическими» соображениями.
Этот недостаток react-navigation, кроме того, причинил мне гораздо более серьёзные неприятности. В частности, они заключались в необходимости организовать сворачивание и исчезновение заголовочного изображения в ходе прокрутки пользователем списка элементов, организованного средствами FlatList. Так как заголовки разделов Home и Selection выводились в пределах того же самого элемента StackNavigator
, что и списки их элементов, эту проблему было бы несложно решить, просто позволив заголовку прокручиваться вместе с остальными элементами списка.
Но вот в случае с разделами Community и Conference я не смог найти способа применить то же решение, так как заголовки выводятся средствами элементов StackNavigator
, а списки — с помощью элементов TabNavigator
, на одну ступень выше в дереве навигации. В результате я отказался от прокрутки заголовка, что внесло в приложение неприятную неоднородность.
Прокрутка в TabNavigator и StackNavigator
Хотя в эмуляторе, имитирующем Iphone X, описываемая проблема с заголовком серьёзной и не кажется, на экране маленького размера заголовок может занимать около 20% доступного экранного пространства. Если кто-нибудь знает о том, как эту проблему решить — дайте мне знать.
Та же самая проблема с TabNavigator
дала о себе знать и в разделе Destination. Как будет показано на следующем рисунке, справа, я хотел поместить другой элемент TabNavigator
на вкладку Coworking Spaces для того, чтобы вывести три верхних закладки Info, Members и Contact.
При работе с TabNavigator
сложно было поместить слайд-шоу в верхней части элемента без того, чтобы не усложнить решение и не вызвать множество проблем с навигацией (в основном связанных с параметрами навигации). Поэтому мне пришлось прибегнуть к JS-пакету, который называется react-native-swiper для того, чтобы работать с этими тремя вкладками. Надо отметить, что меня это решение полностью устроило бы в том случае, если бы его применение не приводило бы к скачкообразной анимации линий, подчёркивающих заголовки вкладок. В любом случае, я счёл этот недостаток справедливой платой за возможность избежать проблем с альтернативной системой навигации.
Сравнение TabNavigator из react-navigation с react-native-swiper (обратите внимание на разницу в анимации золотистых линий, находящихся ниже названий вкладок)
Вот какие выводы я сделал после реализации подсистемы навигации приложения:
- Есть множество хорошо документированных решений, предназначенных для организации навигации в React Native-приложениях, среди которых я выбрал react-navigation. Оно наилучшим образом удовлетворяет моим нуждам.
- Библиотека react-navigation значительно упрощает начало работы над проектом в том случае, если разработчик не особенно много знает о том, как работают системы навигации мобильных платформ.
- Библиотека react-navigation имеет некоторые особенности, которые не отличаются интуитивной понятностью (по крайней мере для веб-разработчика), но в ней нет ничего такого, чего нельзя бы обойти, пусть и не самым эффективным образом.
Экран-заставка
Запуская в эмуляторе новое приложение, созданное средствами команды react-native init
, постоянно перезагружая его по мере внесения в него изменений, очень быстро понимаешь то, что приложению нужен симпатичный экран-заставка (их ещё называют «сплэш-скринами»).
О том, как сделать экран-заставку, хорошо написано здесь, поэтому я не буду повторять слова автора этого материала. Я расскажу лишь о проблеме, с которой столкнулся, и о которой ничего нет в этом руководстве. Вот как выглядит эта проблема:
Экран-заставка, с которым возникла проблема
Эта проблема проявляется в редких случаях на iOS, но с ней, наверняка, столкнутся некоторые пользователи приложения. Впервые я обнаружил эту проблему тогда, когда работал в одном месте, где не мог пользоваться WiFi и подключил ноутбук к 4G-интернету через телефон. Пользователи iPhone знают, что когда телефон раздаёт интернет, его статус-бар окрашивается в синий цвет и становится выше. Это «поломало» изображение на моём экране-заставке когда я запускал приложение на телефоне. Та же проблема возникала и при вызове.
В результате я, порывшись некоторое время в репозитории react-native-splash-screen и не найдя там ничего полезного, решил обойти эту проблему, полностью скрыв строку состояния во время показа экрана-заставки.
Это очень просто — достаточно добавить в файл info.plist
ключ UIStatusBarHidden
, присвоив ему логическое значение true
, а затем, после вызова SplashScreen.hide()
, установить в значение false
свойство hidden
компонента React Native StatusBar
.
Управление состоянием
У меня такое ощущение, что последние два года я каждый день слышу о приоритете соглашения над конфигурацией, о принципе Convention over Configuration (CoC). Так было и на моей предыдущей работе. И это неудивительно, так как там мы, на сервере, использовали Ruby on Rails, а на клиенте — Ember.js — два фреймворка, суть которых как нельзя лучше соответствует принципу CoC. Я думал, что я знаю о том, что это значит, но процесс изучения управления состоянием в React Native дал мне совершенно новое понимание этого принципа.
Хотя я, в нескольких очень простых приложениях, и экспериментировал с библиотекой React, на которую повлиял этот принцип, я никогда не создавал на её основе чего-то достаточно масштабного, такого, что позволило бы мне оценить достоинства использования контейнеров состояния наподобие Redux или MobX. Большая часть моего опыта управления состоянием JS-приложений касается Ember Data (это — встроенная в Ember система для управления состоянием и организации постоянного хранения данных).
Так как библиотека Redux представлялась мне одним из лучших решений проблемы управления состоянием, о котором я слышал уже многие годы (и о котором говорилось в курсе по React Native, пройденным мной), я, в общем-то, и не смотрел в сторону её конкурентов. Мне просто хотелось оснастить своё приложение наилучшей из существующей систем управления состоянием и сделать это без лишних усилий.
В Ember 90% инфраструктуры для работы с данными оказываются в руках программиста уже готовыми к использованию. Я и не подозревал, что в моём текущем проекте всё будет с точностью до наоборот. Как оказалось, ничего полезного для поддержки глобального состояния не даёт не только React, но и Redux — самая популярная библиотека для управления состоянием. Эта библиотека настолько легковесна, что программисту приходится брать на себя 90% забот по работе с данными внутри приложения для того чтобы создать достойную систему управления состоянием.
После того, как я, куда менее опытный разработчик, чем сейчас, это выяснил, самым сложным для меня оказалось — привыкнуть к новому функционалу и к принципам иммутабельности. После того, как я смирился с тем удивительно большим объёмом работ, который нужно провести для того, чтобы просто загрузить данные с сервера или отправить их на сервер, всё это сложилось в виде 7 достаточно простых шагов:
- Добавить в файл с константами три константы:
SOME_ACTION_REQUEST
,SOME_ACTION_FAILED
,SOME_ACTION_SUCCEEDED
. - Добавить создатель действия в файл действий.
- Обработать три действия в подходящем редьюсере, и, если нужно, добавить в систему новый редьюсер и включить его в корневой редьюсер.
- Добавить воркеры в подходящую сагу, и, если нужно, добавить в систему новую сагу и включить её в корневую сагу (я, для обработки асинхронных действий, использую библиотеку redux-saga).
- Добавить функцию для обработки любых возможных запросов к API.
- Сделать маппинг необходимых данных из состояния в свойства в соответствующем React-компоненте.
- Отправить действие
SOME_ACTION_REQUEST
из соответствующего React-компонента.
Redux и redux-saga, конечно, обладают гораздо более обширными возможностями, но, учитывая то, что меня в настоящее время интересует, представленные выше 7 шагов — это то, чем для меня является Redux.
Сессии
Итак, мы настроили среду разработки для React Native-приложений, создали дерево навигации, подготовили инфраструктуру управления состоянием. Что хорошо будет сделать на следующем шаге работы над проектом? Для меня совершенно естественным ответом на этот вопрос стало создание системы аутентификации пользователей. Поэтому сейчас мы поговорим о сессиях.
Если вы пришли в сферу React Native из веб-разработки, то вы разберётесь с сессиями без особого труда. Если вы хотя бы немного знакомы с концепциями, на которых основано хранилище LocalStorage, то вам достаточно знать, что при работе с React Native обращение к подобному хранилищу нужно заменить на обращение к AsyncStorage. Это — уровень абстракции, который позволяет хранить данные в формате ключ-значение в перерывах между сессиями. Другими словами, тут можно хранить сгенерированный на сервере токен аутентификации, переданный на клиент после успешного входа пользователя в систему.
Списки
Если говорить о работе со списками, то у меня возникло такое ощущение, что в React Native эта задача решена достаточно хорошо. В целом можно отметить, что у разработчика есть три возможности. Если он работает со статическими списками, данные, представленные в которых, не меняются, то ему, скорее всего, хватит возможностей ScrollView. Если работать надо с более длинными списками, которые, к тому же, являются динамическими, тогда стоит присмотреться к FlatList. Если же речь идёт об ещё более крупных списках, которые, кроме того, могут быть разделены на разделы, тогда полезно будет прибегнуть к возможностям SectionList.
Я, для динамических списков, использую исключительно FlatList
. И хотя мне, на интуитивном уровне, нравятся эти списки и возможности по их конфигурированию, я, при работе с ними, столкнулся с несколькими неприятностями. Сейчас я о них расскажу.
▍Протяжка списка для обновления его содержимого
У компонента FlatList
имеется свойство, которое называется refreshControl
. С его помощью можно передать этому компоненту компонент, который нужно использовать для обновления содержимого списка, выполняемого в том случае, когда пользователь протягивает список, находясь в его верхней части. К счастью для нас, в React Native имеется компонент, предназначенный специально для решения этой задачи. Это — RefreshControl. Кажется, что всё здесь в этом плане устроено просто и понятно.
Компонент RefreshControl в действии
Однако тут я столкнулся с одной странностью, причиной возникновения которой, как казалось, было либо свойство refreshControl
, либо компонент RefreshControl
, а может и то, и другое. Итак, мне нужно, чтобы при работе со списком пользователь мог бы выполнять следующие действия:
- Он должен иметь возможность потянуть список вниз, находясь в его верхней части, для того чтобы обновить содержимое списка. Обновление планировалось выполнять с помощью функции, которую я назвал
handleRefresh()
. - Когда пользователь прокручивает список и доходит до его конца, автоматически должны были загружаться дополнительные элементы, что позволяло бы реализовать так называемую «бесконечную прокрутку» (об этом мы поговорим ниже).
Как видите — мои требования к спискам были совершенно обыкновенными.
Я приступил к работе, но через некоторое время стал сталкиваться с тем, что при обновлении списка индикатор загрузки данных крутился и крутился, а в списке не выводились новые элементы, загруженные с сервера. Убив немало времени на поиски решения этой проблемы, я наконец нашёл то, что искал, в этом комментарии на GitHub.
Проблема заключалась в том, что свойства refreshControl
и onEndReached
(это свойство используется для реализации бесконечной прокрутки) использовали одно и то же логическое свойство fetching
. И, по какой-то таинственной причине, когда это свойство менялось с false
на true
, а затем — снова менялось, во временном интервале, не превышающем 250 мс, компонент RefreshControl
переставал нормально работать, и индикатор загрузки оказывался бесполезным.
Для того чтобы испытать эту теорию, я попытался добавить в приложение setTimeout()
и задать минимальный временной интервал между изменениями значения fetching
равным 350 мс. Это позволило исправить проблему. Но так как, на мой взгляд, применение в подобной ситуации setTimeout
— это уже перебор, я в итоге вышел на решение, которое заключалось в использовании двух разных свойств для функций handleRefresh()
и handleLoadMore()
— это свойства refreshing
и loadingMore
. Не знаю, насколько распространена эта проблема, но, надеюсь, кому-то мой рассказ о ней окажется полезным.
Обратите внимание на то, что официальная документация рекомендует использовать onRefresh
и refreshing
вместо свойства refreshControl
. Причина, по которой я выбрал именно refreshControl
, заключалась в том, что я просто не нашёл ничего другого, что позволяло бы мне настроить стиль индикатора загрузки так, как мне хочется.
▍Бесконечная прокрутка
Как уже было сказано, мне хотелось дать пользователям возможность как можно более удобной работы со списком. То есть — чтобы им, дойдя до конца списка, не приходилось бы нажимать на кнопку вроде Load More для загрузки дополнительных элементов, и не приходилось бы ждать завершения операций загрузки новых элементов, блокирующих работу со списком, или смотреть на пустые элементы, в которые загружаются данные.
Организация бесконечной прокрутки средствами FlatList. Обратите внимание на то, как, благодаря установке порогового значения 2, onEndReached вызывается в тот момент, когда мы находимся в 2-х экранах от конца списка
Казалось, что для решения поставленных передо мной задач мне должно было хватить возможностей onEndReached
. Однако в ходе работы я столкнулся с парой проблем.
Первая проблема заключалась в свойстве onEndReachedThreshold
, которое сообщает элементу FlatList
о том, когда нужно вызывать функцию, переданную onEndReached
. После череды проб и ошибок я могу сказать об этой проблеме следующее.
Если есть 100 элементов, загруженных в список, и при этом на одном экране помещается 10 элементов, значение 1, записанное в onEndReachedThreshold
, означает, что функция onEndReached
будет вызвана тогда, когда при прокрутке списка пользователь дойдёт до элементов, следующих за 90-м. Если значение равняется 2, то функция будет вызвана в тот момент, когда пользователь будет в 2-х экранах от конца списка, то есть — когда будет просматриваться его 80-й элемент, и так далее.
Причина второй проблемы, которая возникла у меня с бесконечной прокруткой, заключается, видимо, в ошибке компонента FlatList
. В частности, дело было в том, что, при прокрутке списка после прохождения заданного порогового значения, функция handleLoadMore()
, переданная в свойство onEndReached
, вызывалась постоянно, часто — более 10 раз подряд.
По случайности, снова оказалось, что решение проблемы заключается в использовании свойства loadingMore
, а именно, в добавлении в функцию handleLoadMore()
проверки, направленной на обеспечение отправки действия по загрузке данных лишь в том случае, если соблюдается условие, выглядящее как !loadingMore
. И, что совершенно естественно, в подобном условии стоит проверить и достижение последней страницы списка в соответствии с имеющимися на сервере материалами.
▍Загрузка местозаполнителей
Наличие свойства ListLoadingComponent
во FlatList
, так же, как и ListHeaderComponent
, ListEmptyComponent
и ListFooterComponent
, это то, что необязательно как-то повлияет на удобство использования приложения, но, определённо, придётся по душе разработчику.
Так как ничего такого здесь нет, мне пришлось полагаться на довольно неуклюжую условную конструкцию для поддержки вывода местозаполнителей во множестве функций render()
.
▍Прокрутка списка в самый верх
Последнее, о чём я хочу сказать, говоря о списках, заключается в прокрутке списка в самый верх по нажатию на кнопку. В моём приложении подобные кнопки имеются в заголовках, но часто их располагают на панелях, расположенных в нижней части экрана.
Для того чтобы реализовать такую прокрутку, я использовал метод scrollToOffset
компонента FlatList
, с которым несложно разобраться, почитав документацию. Однако тут есть одна очень важная деталь, которую я не смог найти в документации. Заключается она в необходимости использования свойства ref
в компоненте FlatList
:
<FlatList
ref={(ref) => { this.newsListRef = ref; }}
...
/>
Благодаря этой конструкции экземпляр компонента получает идентификатор, который можно использовать в любой функции. В моём случае это позволило мне вызвать функцию ScrollToOffset
из моей функции handleScrollToTop()
, и, например, передать функцию в качестве параметра навигационному объекту react-navigation, что позволяет вызывать её из любого маршрута, которому был передан параметр:
componentDidMount() {
this.props.navigation.setParams({
scrollToTop: this.handleScrollToTop,
});
}
handleScrollToTop = () => {
this.newsListRef.ScrollToOffset({
x: 0, y: 0, animated: true,
});
};
Обратите внимание на то, что после выхода react-navigation версии 3 в ref
больше нет необходимости, так как кнопки BottomTabNavigator
теперь обрабатывают прокрутку списка до самого верха по умолчанию.
Изображения
Неправильная работа с изображениями, в чём я смог убедиться, несёт в себе большой риск ухудшения качества мобильного приложения. Конечно, эффективная обработка изображений важна и для веба, но так как телефоны подключаются к интернету средствами 4G-сетей (или даже, боже упаси, 3G), мобильное приложение может столкнуться со сравнительно низкими скоростями передачи данных по сети, что, в свою очередь, может привести к тому, что оно будет казаться пользователям медленным.
Изображения, кроме того, будут, весьма вероятно, занимать большую долю экрана на мобильном устройстве, чем, в похожих условиях, они занимали бы на экране компьютера. Поэтому им нужно уделять особое внимание и исходя из требований к внешнему виду приложений. И хотя работа с изображениями, скорее всего, не является самой интересной частью мобильной разработки, ей стоит уделить некоторое время, и это время не пропадёт даром.
В моём приложении, как оказалось, используется очень много изображений. В нём имеется 7 списков, у элементов которых есть свойство image
, причём изображения выводятся не только в элементах списков, но и на экране просмотра подробных сведений об этих элементах (на них пользователи попадают, касаясь элементов списков).
▍Загрузка изображений
Приложение позволяет пользователям, в ходе редактирования сведений об их профилях, загружать в систему аватарки. Для реализации этого функционала я воспользовался библиотекой react-native-image-picker, а так же Cloudinary и Carrierwawe на Rails-бэкенде.
Сначала я поместил всю логику выгрузки изображений пользователей на клиенте, используя Node-API Cloudinary и модуль react-native-fetch-blob. Но через некоторое время, так как мне хотелось, чтобы логика выгрузки изображений была бы немного более гибкой, и не хотелось, чтобы на стороне клиента присутствовала бы слишком сложная логика, я переместил это всё на сервер.
Надо отметить, что я, пытаясь отправлять изображения на сервер с использованием react-native-fetch-blob, столкнулся с некоторыми проблемами. В результате же, усложнение системы и неясные перспективы поддержки react-native-fetch-blob привели к тому, что я решил использовать встроенный API JS FormData. Однако обратите внимание на то, что неподдерживаемая библиотека react-native-fetch-blob с тех пор была перенесена в репозиторий rn-fetch-blob, в котором можно наблюдать активную работу над ней.
▍Вывод изображений
Надо сказать, что стандартного элемента React Native Image с его свойствами style
, source
и resizeMode
будет достаточно для решения множества задач вывода изображений. Если вам не нужны такие возможности, как кэширование, вывод нескольких изображений или ещё что-то достаточно продвинутое, то вам, вероятно, не понадобятся дополнительные библиотеки для работы с изображениями.
Я, однако, столкнулся с двумя ситуациями, в которых оправданным выглядел поиск дополнительных библиотек. Первая из этих ситуаций возникла тогда, когда мне нужно было вывести изображение, оформленное в виде круга, как обычно оформляют аватарки. Подобные изображения используются при просмотре некоторых элементов списков и на экранах профилей пользователей. Тут мне пригодился компонент Avatar из пакета react-native-elements.
Однако этот компонент не делает ничего такого, чего нельзя было бы добиться путём соответствующей стилизации стандартного компонента Image
. В результате, если только эта библиотека не используется для решения ещё каких-то задач, я не рекомендовал бы добавлять её в проект только ради форматирования аватарок.
Ещё одна ситуация, в которой мне понадобилась внешняя зависимость, возникла в ходе работы над галереей изображений. Ниже показана одна из таких галерей.
Форк react-native-slideshow в действии
Здесь я воспользовался библиотекой react-native-slideshow, которая позволила создать именно то, что мне было нужно. Надо отметить, что эта библиотека плохо поддерживается, поэтому, если вы решите ей воспользоваться, я рекомендую вам создать её форк и немного почистить её код, а не использовать её в неизменном виде, просто установив как зависимость проекта.
▍Загрузка местозаполнителей
В приложении имеется 7 списков с бесконечной прокруткой, выводящих изображения, поэтому пользователю приходится немного подождать загрузки всех этих изображений с сервера. Как все мы знаем, ожидание — это то, что чрезвычайно не нравится тем, кто пользуется современными технологиями. В результате разработчику приложений нужно стремиться к тому, чтобы его пользователям не приходилось бы слишком долго чего-либо ждать. Тут нам на помощь приходят местозаполнители.
Не знаю точно почему, но всегда, когда мне приходится ждать загрузки чего-либо, я чувствую себя гораздо хуже в том случае, если мне, во время ожидания, приходится наблюдать за чем-то вроде вращающегося индикатора загрузки (а ещё хуже — если на экран вообще ничего не выводится), чем в случае, если я вижу приятно выглядящие динамические местозаполнители, напоминающие то, что выводится в списке новостей Facebook во время их загрузки. В результате я постарался, чтобы элементы списков в моём приложении загружались именно так.
К счастью, я не единственный, кому хотелось бы реализовать подобный функционал в React Native-приложении. У меня ушло сравнительно немного времени на то, чтобы выйти на две хороших библиотеки для работы с местозаполнителями. Первая — это react-native-loading-placeholder, которая применяется, собственно, для загрузки местозаполнителей. Вторая — это react-native-linear-gradient, которая используется для анимации. То, чего удалось добиться с их использованием, мне очень понравилось, даже учитывая то, что анимация элементов, показанная на примере, расположенном в правой части следующего рисунка, возможно, может быть сделана лучше.
Загрузка местозаполнителей с использованием react-native-loading-placeholder и их анимация с помощью react-native-linear-gradient
▍Кэширование
Кэширование изображений в мире мобильных приложений — это очень важно. Как ни странно, но в стандартном элементе React Native Image
всё ещё нет встроенной поддержки кэширования. Для того чтобы кэшировать изображения, придётся воспользоваться чем-то вроде элемента CachedImage
из отличной библиотеки react-native-cached-image.
Для кэширования изображений нужно установить соответствующий npm-пакет и поменять все стандартные теги Image
, используемые для вывода изображений, которые нужно кэшировать, на теги CachedImage
. Потом можно проанализировать работу приложения с помощью Reactotron для того, чтобы проверить, действительно ли изображения кэшируются.
Огромные плюсы, которые даёт кэширование, с лихвой перекрывают те небольшие усилия, которые нужно приложить для переработки приложения. В частности, в моём случае оказалось, что после введения кэширования объём трафика на Cloudinary, ранее составлявший 95% от объёма, предоставляемого ежемесячно бесплатно, теперь составляет лишь 4%. Это меня более чем устраивает.
Кстати, вот вам совет. При работе с CachedImage
используйте свойство activityIndicatorProps={{ animating: false }}
и примените собственный местозаполнитель вместо стандартного индикатора загрузки, выводимого при загрузке изображений.
Дата и время
▍Инструменты для выбора времени
В React Native имеется кросс-платформенный компонент Picker. Однако из-за того, что он нуждается в серьёзной настройке (и из-за моей нетерпеливости), я решил поискать JS-библиотеку, которая облегчает задачу создания элементов управления для работы со временем. К счастью, я нашёл библиотеку react-native-picker-select, которая эмулирует нативный интерфейс <select>
для iOS и Android и практически идеально мне подходит.
Так как эта библиотека представляет собой JS-файл, использующий стандартные React Native-компоненты (а также lodash, но эта библиотека уже была у меня в зависимостях), я решил просто взять код из этой библиотеки, немного его подправить, и включить в состав моего собственного компонента, который я использую для работы со временем. С тех пор я использую этот компонент не только для выбора времени, но и во всех случаях, когда нужно предоставить пользователю возможность выбирать что-либо из списка. Единственным исключением являются ситуации, в которых нужно выбирать дату.
▍Инструменты для выбора даты
Для работы с календарём я решил использовать библиотеку react-native-calendars от Wix. Сделал я это по следующим причинам:
- Мне не нравится нативный элемент управления iOS для выбора даты, так как в нём неудобно устроен выбор месяца и года. Возможно, тут сказывается искажение моего восприятия, вызванное тем, что я много занимался веб-разработкой, но это — моё мнение.
- React Native в настоящий момент требует две разных реализации средства для выбора даты для двух платформ —
DatePickerIOS
иDatePickerAndroid
, использование которых потребовало бы наличия в проекте немалого объёма дублирующего кода, решающего одну и ту же задачу. - Мне хотелось, чтобы средство для выбора даты лучше соответствовало бы внешнему виду приложения, отражало бы особенности стиля компании клиента, а не стиля Apple или Google.
Вот то, что у меня получилось. Возможно, вам это понравится, возможно — нет.
Компоненты из react-native-calendars и react-native-picker-select в действии
▍Часовые пояса
В теории поддержка разных часовых поясов — это очень просто, но в реальности это не так.
Ближе к завершению проекта я интегрировал бэкенд приложения со сторонней SaaS-системой, которую мой клиент использует для организации бронирования комнат. Я только что с удовольствием разобрался со старым добрым протоколом SOAP, настроив необходимые запросы к соответствующему API в разделе приложения Conference. И когда я вышел на работающее решение, я начал замечать, на стороне клиента, странные явления, связанные со временем.
Компания клиента чётко обозначила то, что ей не хотелось бы, по определённым причинам, чтобы пользователи могли бы бронировать помещения на текущую дату после 5 часов вечера. Но из-за того, что по умолчанию объекты JavaScript Date используют UTC, непросто было указать максимальное значение для элементов, предназначенных для выбора времени. На самом деле, это оказалось так сложно, что для реализации данной логики мне пришлось буквально набить компонент кодом, что, на мой взгляд, выглядело не очень-то хорошо. «Хоть бы удалось найти подходящую библиотеку», — думал я тогда.
Ответом на вопрос, касающийся работы с часовыми поясами, стала библиотека moment-js, которая не только полностью совместима с React Native, но ещё и оснащена специальным модулем timezone, который позволил мне делать такие вот удобные сравнения:
const timeSthlmAfterFive = moment().isAfter(moment.tz('17:00:00', 'HH:mm:ss', 'Europe/Stockholm'), 'second');
Собственные шрифты и значки
Использование собственных шрифтов и значков, как кажется, мелочь, но эта мелочь оказывает огромнейшее влияние на пользовательский интерфейс и на брендирование приложения. Так как я пришёл в мир React Native из веб-разработки, я ожидал, что шрифты и значки доставят мне немало неприятностей, ожидал, что мне, как уже приходилось раньше, и теперь придётся морочить себе голову преобразованиями графических файлов и экспериментами с font-face
в CSS.
Однако существующие в этой области наработки значительно облегчили мою задачу. Всё оказалось куда легче, чем я ожидал. Воспользовавшись этим руководством, я примерно за 10 минут импортировал в проект шрифты компании-клиента. Со значками мне удалось разобраться благодаря огромной библиотеке react-native-vector-icons и нескольким командам импорта собственных ресурсов.
Непрерывная интеграция, развёртывание, мониторинг
Теперь перехожу к рассказу о CI/CD, о хлебе DevOps-специалистов и о главном кошмаре программистов-одиночек, которые хотят подзаработать.
Так как я был (и всё ещё являюсь) единственным разработчиком рассматриваемого приложения, кому-то применение в проекте системы непрерывной интеграции может показаться ненужным излишеством. В проекте не применяются средства для совместной разработки, развёртывание всегда выполняется с одного и того же компьютера. Я могу просто собрать и протестировать приложение локально перед отправкой кода в GitHub-репозиторий и перед публикацией нового релиза в магазинах приложений. Однако я считаю, что проекту необходима система непрерывной интеграции по следующим причинам:
- Компания клиента собирается организовать внутреннюю команду разработчиков. Когда это будет сделано, понадобится инфраструктура, максимально упрощающая ввод в проект новых программистов.
- Хотя локальное выполнение тестов означает необходимость выполнения всего одной команды в терминале, если что-то можно автоматизировать, то всегда надо стремиться к тому, чтобы так и сделать.
В результате я был настроен на внедрение в проект системы непрерывной интеграции. Но до определённого момента я полагал, что эта система будет отвечать исключительно за сборку и тестирование приложения, и считал, что мне понадобится найти отдельные системы для решения таких задач, как формирование отчётов об ошибках, анализ данных о работе приложения, отправка push-уведомлений. О том, чтобы пользоваться возможностями непрерывного развёртывания, я и не думал, так как полагал, что в мире нативных приложений подобных систем не существует.
Потом я обнаружил Visual Studio App Center (VSAC). Это выступление с Chain React 2017 совершенно перевернуло мои взгляды. То, о чём там шла речь, по всей видимости, включало в себя все те DevOps-решения, которые я намеревался собирать из разных проектов. А именно, речь шла о сборке и тестировании, о работе с ошибками, об аналитике, о push-уведомлениях, и даже о непрерывном развёртывании с помощью Codepush. Не говоря уже о публикации приложения в магазинах и об отправке их бета-тестерам. И, что самое приятное, рассматриваемое решение давало возможность управлять, из одного места, и приложением для iOS, и приложением для Android. А ещё, меня прямо-таки восхитило то, что всё это ничего не стоит, по крайней мере, до тех пор, пока приложение не станет достаточно большим, то есть платить за все эти возможности придётся где-то через год или позже.
Visual Studio App Center (взято отсюда)
«Это слишком хорошо чтобы быть правдой», — подумал я тогда, тронутый до слёз. Всё это казалось невероятно простым и красивым, было нацелено на нужды разработчиков (API — это самое главное). Кроме того, система имела настолько приятный и простой интерфейс, что даже люди, далёкие от программирования, некоторые сотрудники компании-клиента, могли бы в нём разобраться (ну, по крайней мере, в некоторых его частях).
Как это возможно? Компания Microsoft, как оказалось, перед запуском VSAC хорошо «прошлась по магазинам». Для того чтобы сделать этот сервис тем, что он есть, компания купила множество существующих независимых решений вроде Codepush (непрерывное развёртывание React Native-приложений) и HockeyApp (тестирование и сообщения об ошибках), а так же расширила некоторые собственные продукты и создала кое-что новое. Возникает такое ощущение, что тут мы видим воплощение в жизнь знаменитого «developers, developers, developers, developers» Стива Балмера.
Итак, достаточно ли я узнал о VSAC для того, чтобы принять взвешенное решение по поводу использования этой сравнительно новой платформы и отказаться от сервисов-конкурентов наподобие Fastlane, BuddyBuild и Firebase? На самом деле, если всё и на самом деле было бы так хорошо, как казалось мне после просмотра вышеупомянутого выступления, то выбор VSAC позволил бы мне сэкономить недели, избавившись от необходимости разбираться с разными сервисами, настраивать их, поддерживать в работоспособном состоянии. Зачем это всё, если нужных мне результатов можно достичь, воспользовавшись единственным сервисом? В результате я решил, что VSAC, в любом случае, стоит попробовать.
В результате примерно через неделю моё приложение уже пользовалось всеми возможностями VSAC. Были тут и некоторые проблемы, вероятно, вызванные новизной сервиса, но чтение документации и обращение в техподдержку помогли мне найти ответы на все интересующие меня вопросы.
Одной из проблем, с которой я тогда столкнулся, было то, что VSAC ещё не поддерживал интеграцию с учётными записями Apple Developers с использованием двухфакторной аутентификации (Apple, очень вовремя, начала настоятельно рекомендовать такую схему аутентификации как раз тогда, когда я настраивал учётную запись). Тогда меня это расстроило, но всего через несколько недель после того, как я сообщил о проблеме, была введена официальная поддержка этой возможности.
Если вы полагаете, что я тут до невозможности захвалил VSAC, и хотите узнать о том, как этот сервис выглядит с точки зрения разработчика более масштабного приложения, я рекомендую взглянуть на этот критический обзор CI/CD-сервисов для мобильных разработчиков.
Поддержка платформы Android
После того, как iOS-версия приложения была готова, добавить в него поддержку Android оказалось на удивление просто. После установки Android Studio и запуска приложения в эмуляторе Android большинство проблем можно было решить средствами модуля React Native Platform. Стилизацией элементов для конкретной платформы можно заниматься, пользуясь методом Platform.select()
. Для выполнения каких-либо действий, зависящих от платформы, на которой выполняется приложение, можно воспользоваться Platform.OS
.
Кроме того, надо отметить, что отправка приложения в магазин Google Play и его допуск к публикации оказались несравненно проще, чем при работе с App Store. Почему? Всё дело в том, что публикуясь в App Store мы имеем дело с Apple.
Особенности взаимоотношений с Apple
Очевидно то, что веб-разработчик, приступающий к созданию мобильного приложения на React Native, встретится с определёнными сложностями. В моём случае главным источником головной боли стал процесс работы над проектом, предписываемый Apple. Я, честно говоря, не могу вспомнить о том, чтобы так много сложностей и переносов сроков были бы вызваны, по техническим и иным причинам, некоей отдельно взятой сущностью.
Apple и разработчик
Я так думаю, что всему виной — необъяснимо высокий уровень «канцелярской работы». Если и есть что-то, в чём можно быть уверенным, разрабатывая своё первое iOS-приложение, так это то, что вы, в процессе работы над ним, приобретёте множество новых друзей.
Например — это сотрудники из техподдержки Apple. Вам может понадобиться убедить их в том, что компания, для которой разработано приложение, реально существует. Несколько друзей можно приобрести и в компании Dun & Bradstreet, которая является партнёром Apple, выполняющим проверку информации о других компаниях. Занимаясь публикацией приложения, вы ещё можете подружиться и с представителями местных властей, приводя адрес компании к тому виду, который принимает Apple (компании, зарегистрированные с использованием абонементного ящика, Apple не устраивают, а в скандинавских странах это — распространённая практика).
Потом вам может понадобиться потратить ещё немало времени на общение с техподдержкой, так как Apple не сможет одобрить вашу учётную запись разработчика, видя, что вы — лишь консультант, а не работник вашей компании-клиента. Всё это может занять более месяца. Но какая разница, ведь главное — найти побольше друзей?
На самом деле, на этом интересности не заканчиваются.
Вы, наконец, получили учётную запись Apple Developer для компании и всё настроили. Завершена работа над первой версией приложения и вам не терпится его опубликовать.
Но спешить вам ни к чему. Сначала надо подготовить кучу электронных документов. Нужно сгенерировать provisioning-профиль, iOS-сертификат, идентификатор, сертификат Apple .p12 для работы с push-уведомлениями, и старый добрый dSym-файл. После того, как вы сгенерируете и загрузите всё это туда, куда нужно, может начаться процесс проверки приложения.
По сведениям Apple, решение по поводу допуска к публикации для 50% приложений принимается в течение 24 часов, для 90% приложений — в течение 48 часов. Ожидая результатов рассмотрения, будьте готовы к худшему, так как отказ в публикации — это, похоже, совершенно обычное дело для Apple-разработчика.
К счастью, моё приложение не пропустили лишь один раз. В качестве причины отказа было указано «Metadata Rejected». Я бы по этому поводу не беспокоился, если бы просто забыл заполнить какое-нибудь поле. Но видя, что добавление в приложение недостающих метаданных означает нахождение ответов на 5 весьма специфических вопросов (об этом ничего не говорится в App Store Review Guidelines), я приуныл.
Невесело жить в мире, в котором всего две компании решают, каким мобильным приложениям можно выйти в свет, а каким — нельзя. Грустно то, что как минимум одна из этих компаний так мало заботится о своих клиентах, что, по своему усмотрению, позволяет себе отнимать у людей время, что приводит к дорогостоящим многомесячным задержкам выхода приложений. Пожалуй, в современном мире разработки приложений есть лишь одна радость — веб, к которому (пока) подобное не относится.
Разрабатывая моё первое нативное приложение для iOS, я столкнулся с множеством бюрократических барьеров. Если в мире мобильной разработки было бы нечто, подобное дементорам, то можно сказать, что только что мы говорили об одном из них. Это существо вытягивает из разработчика душу, и то, что делает его счастливым. Но не будем о грустном. Ведь, на самом деле, всё не так уж и плохо.
Итоги
Как я уже сказал, заниматься React Native- проектом я начал ранним летом. Невысокая летняя нагрузка на моей основной работе позволила совмещать её с разработкой React Native-приложения. Дедлайн был назначен на октябрь-ноябрь. Тогда я понял, что мне нужно сделать выбор: остаться на работе или в срок доделать приложение. После нескольких недель размышлений я выбрал последнее.
Хотя решение это было и непростое, сейчас, оглядываясь назад, я понимаю, что оно было правильным. Я получил именно то, что искал, и даже больше — свободу, возможность решать сложные и интересные задачи и заниматься саморазвитием.
Если говорить о свободе, то особенности взаимоотношений с компанией-клиентом позволили мне работать откуда угодно, самому строить свой график. Это оказало позитивное влияние на многие аспекты моей жизни. Например, позволило, более или менее регулярно, спать по 8 часов в сутки. Свобода помогла мне лучше организовать работу. Она позволила мне проводить больше времени с любимыми людьми и работать во время путешествий.
Минус подобной свободы в том, что она означает работу в одиночестве. Даже если работаешь в кафе или в коворкинге, очень остро ощущаешь нехватку общения с коллегами, невозможность что-то с ними обсудить.
В том, что касается решения сложных задач и саморазвития, я обнаружил, что работа над проектом в течение 6-7 месяцев дала мне знания, которые я мог бы получить на любой обычной работе лишь за несколько лет. В результате я, как разработчик, профессионально вырос во всех сферах, включая следующие:
- Я открыл себе выход даже не на одну, а на две мобильные платформы (iOS и Android), доведя число платформ, для которых я могу разрабатывать приложения, до трёх.
- Я более глубоко изучил JavaScript. Мне понадобилось пересмотреть многие концепции, которые я воспринимал как данность, разрабатывая проекты в ограниченном разного рода соглашениями пространстве Ember.js.
- До этого у меня был весьма скромный опыт работы с обычной библиотекой React.js, а теперь я чувствую, что у меня есть всё необходимое для создания достаточно масштабных React-приложений, рассчитанных на работу в браузере. Как результат, теперь я владею двумя JS-библиотеками/фреймворками.
- Я ознакомился с функциональным программированием и философией иммутабельности, связанной с управлением состоянием в Redux.
- Я многое узнал о DevOps и об управлении проектами.
- Я научился лучше анализировать чужой код, исследуя репозитории плохо документированных библиотек.
- Я вырос в сфере UI/UX.
- И, что самое главное, я теперь гораздо сильнее, чем раньше, уверен в том, что смогу самостоятельно изучить любую технологию, которую мне захочется освоить, и смогу обойти любые препятствия, которые встретятся мне на пути к цели.
Так как у меня совершенно нет опыта в работе с другими JS-инструментами для разработки нативных мобильных приложений, вроде Flutter, или NativeScript, равно как и в работе с Objective-C, Swift, Java или Kotlin, я не берусь сравнивать React Native с конкурирующими технологиями.
Но, как веб-разработчик, я могу говорить о том, что применение React Native позволило мне, без особенных сложностей, перейти к мобильной разработке, узнав в процессе перехода много полезного. Нельзя не признавать того, что React Native — молодая технология, далёкая от совершенства. Но лично я без сомнения воспользуюсь React Native снова.
Уважаемые читатели! Какие технологии вы используете для разработки мобильных приложений?
Автор: ru_vds