В предыдущей статье я подробно рассказал о нашем опыте создания веб-сервиса/мобильного приложения для ведения личного дневника. Актуальная версия приложения (минимальная работоспособная версия уже выложена в Google Play) разрабатывается на React Native, и вот на нем мы и остановимся подробно сегодня.
Рассказываем о собственном опыте работы с фреймворком, способах расширения функционала, «подводных камнях» (куда ж без них!) и как мы их обошли.
О фреймворке в целом
Немного о виновнике торжества — React Native. Он все-таки хорош!
Для тех, кто в достаточной степени знает JavaScript и тем более NodeJS — он очень хорош. Если же есть опыт с React, ну или хотя бы есть понимание ее идеи, механизма — он просто великолепен!
Главное, что на выходе получается действительно нативное приложение. Расширения и плагины покрывают практически 99% типовых задач. Оставшийся процент при острой необходимости можно дописать на родных языках (java, object-c) и подключить к React Native приложению.
Но хватит про плюсы, от них толку ноль, хоть список и будет внушительным. Все плюшки и вкусности бессмысленны, если приложение не запускается, а это первое чем нас «порадовал» React Native.
Сначала ему не понравилась версия NodeJS. Потом версия npm. Потом версия Android SDK, потом версия Android tools, потом… Писать про то, как все проблемы решились, смысла нет, ибо с того момента все вышеперечисленное ПО обновило свои версии и инструкции будут неактуальны.
Просто знайте: узкое место React Native — среда сборки. Будьте готовы к штудированию google, чтению форумов и stackoverflow. На развертывание в итоге потратили: Ubuntu 12.6 — 3 дня, Win10 — 2 дня. Как ни странно, на «винде» все оказалось проще, ну, или просто на ubuntu «шишек набили» и уже понимали, что и куда подсовывать.
На заметку, вдруг кому пригодится: код, представленный ниже, решил все проблемы с совместимостью версий sdk у дополнений при компиляции проекта.
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion 26
buildToolsVersion '26.0.3'
}
}
}
}
Прописывается в файле /android/build.gradle в самом конце. Без этой «директивы», судя по всему, каждый из плагинов/расширений пытался компилироваться по своим собственным версиям Android SDK, что приводило сборку проекта в хаотичное ассорти из лютых ошибок и богомерзких предупреждений. Никто не знает, насколько актуально будет рекомендация в будущем. Но на сегодняшний день, особенно после того как Google принудительно запретил для компиляции использовать SDK ниже 26-й ревизии, это очень даже помогает.
Второе «узкое» место — боль не столько React Native, сколько, видимо, всего Open Source в целом — ограниченная поддержка. В репозиториях куча нерешенных issues. Лютые «умные» боты закрывают баги при отсутствии активности иногда аж через 7 дней… И вроде все это нормально. Никто никому ничем не обязан. Все привыкли.
Терпение лопнуло, когда обнаружился «косяк» при банальном вводе текста в обычный TextInput. Просто текстовое поле. Просто ввод текста с экранной клавиатуры. Через пару минут печатания начинается жутчайшие лаги и тормоза системы. Бросились искать проблему — да, есть такое, началось с версии RN 5x.xx Проблему решают? Нет. Два или три issues по теме просто закрыто. Еще несколько слиты в один большой.
Пришлось копать внутрь, выяснять, пробовать различные варианты, менять версии RN, в надежде что там косяка этого внезапно нет. В итоге опытным путем удалось минимизировать провалы в производительности – убрали полностью все форматирование и переписали обработчик ввода. Но неприятный осадок остался. Проблема у разработчиков фреймворка, кстати, не решена до сих пор, прошло полгода…
База данных
Realm — шустрая база данных, с внушительным функционалом и работающая на Android, IOS, Windows.
Поначалу было двоякое ощущение, мол, никакой тебе ORM, реально нет sql, запись ведется только внутри callback. Непривычно и странно, особенно для веб-разработчика родом из PHP, выросшего на ActiveRecord и Doctrine.
Поначалу было двоякое ощущение, мол, никакой тебе ORM, реально нет sql, запись ведется только внутри callback. Непривычно и странно, особенно для веб-разработчика родом из PHP, выросшего на ActiveRecord и Doctrine. Но по факту набросать свой минимальный набор функций для CRUD оказалось совсем просто и быстро. А все вопросы вкусовщины и привычек разрешились чтением официальной справки, краткой, лаконичной и понятной.
А потом и вовсе началась карусель подарков:
- Шифрование данных, из коробки
- Ленивая загрузка данных (тянет из базы только то, что нужно прямо сейчас)
- Реальные связи между сущностями (привет, mongo!
- Версионирование структуры БД, с миграциями — из коробки
- Шифрование данных, из коробки!
- И еще куча маленьких, но приятных мелочей.
Казалось, вопрос с БД закрыт совсем. Работаем! Дело спорилось, пока не дошли до поиска. Вернее, до полнотекстового поиска. Еще точнее, до полнотекстового поиска на русском языке без учета регистра. Он не работал. Совсем. На английском — работал. С учетом регистра тоже работал. А вот без регистра, да на русском — хоть плачь. Покопав справку, багтрекер и интернет, выяснилось что разработчику в силу определенных технических причин было очень неудобно «думать» о поддержке мультибайтовых кодировок и всего, что выходит за рамки латиницы. Ну вот он и не стал. А почему бы и нет?
Делать нечего, пришлось искать обходное решение. В результате непродолжительного штурма, было принято «волевое» решение — делаем отдельное поле «fulltext_index». В него дублируем весь текст в верхнем регистре, попутно «выпиливая» ненужные знаки препинания, лишние проблемы и разного рода мусор. После этого, логично предположить, делаем поиск с принудительным верхним регистром.
Победа! Поиск теперь работает как часы хоть на русском, хоть на английском!
Итого: несмотря на проблемы с регистром, база данных работает реально быстро, удобство на уровне, куча готовых фишек из коробки — в общем, рекомендую.
Навигация по экранам
wix/react-native-navigation — простой и стабильно работающий навигатор (роутер, как сказал бы веб-программист).
Был выбран только потому, что прошел все нужные внутренние тесты (открытие экрана, стек вызовов, возврат, сайдбар). В общем, минимальный нужный минимум.
В отличие от широко любимого всеми react-navigation у wix заявлена 100% нативность. Так оно и есть — все переходы между экранами транслируются в java код приложения и отрабатывают на уровне системы.
В процессе разработки столкнулись с жутким багом «белого экрана», возникающего только в некоторых случаях и на отдельных устройствах. Случается, при выходе из «спящего» режима, процесс загрузки просто замирает. Дебаггер и отладка молчат. На github по данной проблеме нашлись лишь странные намеки на «...try to play» с очередностью загрузки экранов и прочая колдовская благодать. Толком даже не понятно, на каком уровне проблема зарыта: java-код андроида или уже в машине JavaScript. После того, как мы потанцевали с бубном, ошибка стала проявляться реже, но совсем не ушла, зависнув в списке нерешенных задач. Увы.
За вычетом данного «косяка» — все более-менее сносно и гладко. А, главное, нативно!
Файловая система
От файловой системы нам нужно было хранение пользовательских фото, а также работа с парой файлов, связанными с резервным копированием. В результате выбора из двух возможных вариантов выбор пал на react-native-fs
«Доступ к нативной файловой системе» — написано на входе в репозиторий. Что ж, наверное, так и есть, но с некоторыми поправками и ограничениями.
1. Доступ только асинхронный. В результате иногда приходится вспоминать работу с Promise / async / await. Хотя в React об этом начинаешь забывать.
Синхронное выполнение асинхронной функции (await), требует чтобы текущая функция была Асинхронной (async). Для этого достаточно просто добавить async перед именем функции. И да, для метода класса React.Component это работает тоже. (в справке React, ReactNative об этом умалчивают, хотя это само собой подразумевается).
export default class CloudIndex extends BasePage {
async setupBackupFolders(init = false) {
// some stuff there...
await RunSomeAsyncFuncInSyncMode(foo, bar)
RunFuncAfter(bar)
}
}
Важно помнить, что после этого функция тоже становится асинхронной! Если она была уже где-то использована ранее, перепроверьте ее вызов.
2. Полноценный кроссплатформенный доступ есть лишь к части файловой системы. По сути только к одной директории: DocumentDirectoryPath. И это, собственно, директория в которой лежит приложение. Забудьте о сканировании корневой директории, поиске картинок в галерее, аудио и т. д. Ничего из этого не доступно.
А в целом, свои задачи решает на 100%. В копилку маст хев.
Доступ к облаку
Задача одновременно простая и сложная. Простая, потому что у всех есть API — бери и пользуйся. Сложная — лезть в глубины не хочется, да и формат времени не позволял сидеть и ковыряться в «возможно работающих» способах. Решили найти то, что работает 100% и реализовано в уже готовом расширении для React Native.
Таких нашлось ровно одно: Google Drive. Работа с диском понятна и рулится банальными запросами на API. А вот получение доступа приложения к диску — совсем другая история.
React-native-google-signin — система управления авторизацией в сервисах гугла.
Вот здесь-то мы и «повеселились». Хотели, что попроще и понадежней, а получили…
Все началось с получения ключа разработчика. Раньше всем этим занимался сам Google. Но после поглощения FireBase было решено перенести эту функцию в ее чудесную консоль.
Итак, чтобы получить ключ, нужно:
- Зарегистрировать приложение на google developer console чтобы там «включить» доступ к Drive службе.
- Зарегистрировать приложение на firebase console.
- Сформировать в firebase console файл google-services.json — в котором зашиты ключи сервиса.
- Подсунуть этот файлик в проект с установленным расширением react-native-google-signin.
И тогда, да. Что-то начинает работать. Вернее, коды ошибок в ответах сервиса начинают быть осмысленными.
Особенно важно отметить, что API key, получаемый приложением непосредственно при подключении к сервису, совсем не вечный. Иногда он меняется раз в день, иногда раз в минуту. Поэтому, перед обращением к сервису всегда сначала лучше проверять, не просрочен ли текущий ключ. А если он просрочен — получать заново.
Процесс получения API ключа у Google выглядит следующим образом:
await GoogleSignin.hasPlayServices()
const userInfo = await GoogleSignin.signIn()
this.setState({
userInfo: userInfo,
})
settings.set('google.drive.key', userInfo.accessToken)
trace('>> Key obtained:', userInfo.accessToken)
this.apiKey = userInfo.accessToken
Так, например, в нашем приложении, при открытии экрана бекапа мы пытаемся получить у Google id папки с бекапами. Если все успешно — мы получаем id.
backupRootID = await Storage.safeCreateFolder({
name: backupFolder,
parents: ["root"],
}).catch((e)=>{
if(e.status == 401) {
trace(' >> Google signin unauthorized', e)
signGoogle()
return false
} else {
trace(' >> Google signin failed', e)
}
})
// Yeahh. The api key is valid, and rootID found on GoogleDrive!
SomeStorage.setRootId(backupRootID)
Если нет (пришла 401 ошибка) — пытаемся получить новый API ключ и повторяем запрос на получение id папки с бэкапом заново.
И еще несколько приятных мелочей
Работа с датами и временем
Честь и хвала moment.js
Знакомство с этим чудом началась уже давным-давно и было чертовски приятно, что он так же хорошо работает и в среде React Native.
Куча форматов, магические + — день / месяц / год. Поддержка многоязычности и национальных форматов. Красота!
Можно закидать нас помидорами, указав, что все это легко «рулится» руками с обычными Date, но в условиях быстрой разработки НЕ думать о таких вещах очень и очень полезно!
Графики и диаграммы
React-native-charts-wrapper — обертка на JavaScript для родного андроидного MPAndroidCharts.
Понравилось наличие обилия различных типов графиков (хотя на данным этапе мы использовали только два из них — линейный и «пирог»).
Подпортил впечатление скудный почти отсутствующий справочник API. Автор рекомендует смотреть документацию по оригинальному MPAndroidCharts. По факту, совет оказался трудновыполнимым, так как разработка последнего ведется непрерывно и на несколько версий обгоняет реализацию враппера. Кроме того, MPAndroidCharts написан на Java. Враппер – на JavaScript. Быстро сообразить что к чему бывает сложно, приходится задумываться.
Мультиязычность и переводы
React-native-i18n -work like a charm, guys!
Хоть данный компонент и висит на github с пометкой Deprecated, но работает он без сбоев и косяков. Все переводы аккуратно раскиданы по файлам с языками.
Использование параметров транслятора работает тоже на ура:
// en.js
sync: {
success: 'All items are up to date!',
progress: 'Sync Notes %{idx} of %{total}'
}
//app.js
import I18n from 'react-native-i18n'
import en from './en.js'
I18n.translations = { en }
I18n.locale = "en"
const _t = (msg, data) => { return I18n.t(msg, data) }
console.log(_t('sync.progress', {idx: 3, total: 10}))
В сухом остатке
React Native оправдал практически все свои ожидания. С его помощью можно относительно быстро собрать прототип приложения, отработать структуру и юзабилити. Все необходимые инструменты для «базы» есть.
С другой стороны, всегда есть риск, оказаться в «вакууме» когда готовых решений просто нет. Так, например, у нас получилось при загрузке фото в приложение — компонент который может нормально резать и пережимать изображения — всего один. И он не запустился в нашей текущей сборке. Если необходимость в нем будет очень «острой» — придется обновлять почти полсистемы, что наверняка приведет к очередной охоте за ошибками.
Как покажет себя наш продукт, собранный на React Native на рынке, мы узнаем в течение ближайших месяцев. Но это уже совсем другая история.
Автор: Vladimirov_Vladimir