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

    В предыдущей статье я подробно рассказал о нашем опыте создания веб-сервиса/мобильного приложения для ведения личного дневника. Актуальная версия приложения (минимальная работоспособная версия уже выложена в 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 — 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 дней… И вроде все это нормально. Никто никому ничем не обязан. Все привыкли.

    about.me text input

    Терпение лопнуло, когда обнаружился «косяк» при банальном вводе текста в обычный TextInput. Просто текстовое поле. Просто ввод текста с экранной клавиатуры. Через пару минут печатания начинается жутчайшие лаги и тормоза системы. Бросились искать проблему — да, есть такое, началось с версии RN 5x.xx Проблему решают? Нет. Два или три issues по теме просто закрыто. Еще несколько слиты в один большой.

    Пришлось копать внутрь, выяснять, пробовать различные варианты, менять версии RN, в надежде что там косяка этого внезапно нет. В итоге опытным путем удалось минимизировать провалы в производительности – убрали полностью все форматирование и переписали обработчик ввода. Но неприятный осадок остался. Проблема у разработчиков фреймворка, кстати, не решена до сих пор, прошло полгода…

    База данных


    Realm — шустрая база данных, с внушительным функционалом и работающая на Android, IOS, Windows.

    Поначалу было двоякое ощущение, мол, никакой тебе ORM, реально нет sql, запись ведется только внутри callback. Непривычно и странно, особенно для веб-разработчика родом из PHP, выросшего на ActiveRecord и Doctrine. Но по факту набросать свой минимальный набор функций для CRUD оказалось совсем просто и быстро. А все вопросы вкусовщины и привычек разрешились чтением официальной справки, краткой, лаконичной и понятной.

    А потом и вовсе началась карусель подарков:

    • Шифрование данных, из коробки
    • Ленивая загрузка данных (тянет из базы только то, что нужно прямо сейчас)
    • Реальные связи между сущностями (привет, mongo!
    • Версионирование структуры БД, с миграциями — из коробки
    • И еще куча маленьких, но приятных мелочей.

    about.me index search
    Казалось, вопрос с БД закрыт совсем. Работаем! Дело спорилось, пока не дошли до поиска. Вернее, до полнотекстового поиска. Еще точнее, до полнотекстового поиска на русском языке без учета регистра. Он не работал. Совсем. На английском — работал. С учетом регистра тоже работал. А вот без регистра, да на русском — хоть плачь. Покопав справку, багтрекер и интернет, выяснилось что разработчику в силу определенных технических причин было очень неудобно «думать» о поддержке мультибайтовых кодировок и всего, что выходит за рамки латиницы. Ну вот он и не стал. А почему бы и нет?

    Делать нечего, пришлось искать обходное решение. В результате непродолжительного штурма, было принято «волевое» решение — делаем отдельное поле «fulltext_index». В него дублируем весь текст в верхнем регистре, попутно «выпиливая» ненужные знаки препинания, лишние проблемы и разного рода мусор. После этого, логично предположить, делаем поиск с принудительным верхним регистром.

    Победа! Поиск теперь работает как часы хоть на русском, хоть на английском!

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

    Навигация по экранам


    wix/react-native-navigation — простой и стабильно работающий навигатор (роутер, как сказал бы веб-программист).

    Был выбран только потому, что прошел все нужные внутренние тесты (открытие экрана, стек вызовов, возврат, сайдбар). В общем, минимальный нужный минимум.

    about.me wix slider
    В отличие от широко любимого всеми react-navigation у wix заявлена 100% нативность. Так оно и есть — все переходы между экранами транслируются в java код приложения и отрабатывают на уровне системы.

    В процессе разработки столкнулись с жутким багом «белого экрана», возникающего только в некоторых случаях и на отдельных устройствах. Случается, при выходе из «спящего» режима, процесс загрузки просто замирает. Дебаггер и отладка молчат. На github по данной проблеме нашлись лишь странные намеки на «...try to play» с очередностью загрузки экранов и прочая колдовская благодать. Толком даже не понятно, на каком уровне проблема зарыта: java-код андроида или уже в машине JavaScript. После того, как мы потанцевали с бубном, ошибка стала проявляться реже, но совсем не ушла, зависнув в списке нерешенных задач. Увы.

    За вычетом данного «косяка» — все более-менее сносно и гладко. А, главное, нативно!

    Файловая система


    От файловой системы нам нужно было хранение пользовательских фото, а также работа с парой файлов, связанными с резервным копированием. В результате выбора из двух возможных вариантов выбор пал на react-native-fs
    about.me wix slider
    «Доступ к нативной файловой системе» — написано на входе в репозиторий. Что ж, наверное, так и есть, но с некоторыми поправками и ограничениями.

    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 — система управления авторизацией в сервисах гугла.

    about.me wix slider
    Вот здесь-то мы и «повеселились». Хотели, что попроще и понадежней, а получили…

    Все началось с получения ключа разработчика. Раньше всем этим занимался сам Google. Но после поглощения FireBase было решено перенести эту функцию в ее чудесную консоль.

    Итак, чтобы получить ключ, нужно:

    1. Зарегистрировать приложение на google developer console чтобы там «включить» доступ к Drive службе.
    2. Зарегистрировать приложение на firebase console.
    3. Сформировать в firebase console файл google-services.json — в котором зашиты ключи сервиса.
    4. Подсунуть этот файлик в проект с установленным расширением 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, но в условиях быстрой разработки НЕ думать о таких вещах очень и очень полезно!

    Графики и диаграммы


    about.me wix slider
    React-native-charts-wrapper — обертка на JavaScript для родного андроидного MPAndroidCharts.

    Понравилось наличие обилия различных типов графиков (хотя на данным этапе мы использовали только два из них — линейный и «пирог»).

    Подпортил впечатление скудный почти отсутствующий справочник API. Автор рекомендует смотреть документацию по оригинальному MPAndroidCharts. По факту, совет оказался трудновыполнимым, так как разработка последнего ведется непрерывно и на несколько версий обгоняет реализацию враппера. Кроме того, MPAndroidCharts написан на Java. Враппер – на JavaScript. Быстро сообразить что к чему бывает сложно, приходится задумываться.

    Мультиязычность и переводы


    about.me wix slider
    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 на рынке, мы узнаем в течение ближайших месяцев. Но это уже совсем другая история.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      –5
      Хоть убейте не понимаю зачем?
      Что мешает тупо написать натив?
      Надежностью и не пахнет, собрано как-то из непонятно чего, как-то работает, ну и ладно + миллион узких мест.

        +4
        Что мешает тупо написать натив?

        Отсутствие знаний Java/Objective-c и наличие таковых с Js/React?
          –5
          Да бросьте Вы, элементарная лень и нежелание выходить за пределы своего динамического «у меня и так работает» мирка. Сегодня приложения для Android пишут на Kotlin и ничего, я повторяю, ничего сложного в нем нет. Я вот только драйвера разрабатывал или еще какие низкоуровневые службы Windows и я написал нативное приложение для личного использования за два месяца (только на выходных им занимался). На iOS все используют Swift и он мало чего общего имеет с Objective-C. Я в iOS разработку глубоко не нырял, но девушка иногда с ней играется и все у нее легко и просто. React и JS нужны для web-разработки. Зачем подметать ломом?
            +4
            Затем что можно получить приемлемый результат на обоих платформах за намного меньшее время, которое деньги. Плюс опыт с JS фронтом уменьшает время старта новых людей, что тоже деньги. Проекты без особых нативных требований отлично делаются на RN, проверено на практике.
          +1
          С другой стороны, всегда есть риск, оказаться в «вакууме» когда готовых решений просто нет.

          Вот это и мешает :)
          Я ни в коем случае не против использования библиотек или чужих наработок, но у этой формулировки совсем другой посыл.
            0
            Codepush. Чет про него все молчат всегда, а это самая главная фича RN, после которой я окончательно зауважал RN.

            Codepush работает под RN и Cordova. Почему Cordova не фонтан надеюсь не надо объяснять, так что RN.

            Long live RN.
            –1
            У меня вообще версия 0.57 не работает, все сделал по канону но выскакивает ошибка, гугл молчит.
            Главный недостаток RN это то что он обновляется в раз пол года, такими темпами версия 1.0 будет в 2038 году.
              +3
              Понимаю боль автора. Просто несколько заметок которые я писал для себя около года назад, чтобы не забыть (дают прочувствовать атмосферу):
              =======

              иногда если не билдится, можно попробовать почистить содержимое \android\.gradle
              и после этого перезапустить react-native run-android пару раз.
              ========
              Если не билдится release, прежде чем что-то менять, попробовать раз пять перезапустить процесс
              =======
              если cannot unzip… то надо сделать:
              android\gradlew.bat clean
              =========
              Если при попытке задеплоить приложение на смартфон там вылезает Error: Requiring module «NativeModules» (а на эмуляторе всё ок) — причина неизвестна, но возможно как-то связана со служебным адресом в строке wsClient = new SubscriptionClient(`ws://10.0.2.2:3002/subscriptions`,…
              Временное решение — забить на отладку и задеплоить release. С ним всё ок.
              Кто-то советовал удалить node_modules и заново сделать npm install. Не пробовал.
              =========
              решение проблемы с «ERROR EPERM: operation not permitted, lstat… » после react-native run-android:
              react-native run-android, as soon it opens the packager, kill it, wait for the BUILD SUCCESSFUL message to appear, then node node_modules/react-native/local-cli/cli.js start
              =======
              (это всё под Win7).

              Ну и далее в таком духе (включая всякие тонкости, где можно и где нельзя вставлять в каких-то конфигах пробелы, можно или нельзя кавычки использовать и т.п.). Сильно напрягает, что многие проблемы плохо воспроизводятся и решаются кучей совершенно разных способов (судя по советам столкнувшихся с ними). При этом у кого-то срабатывают одни советы, у кого-то другие, а у кого-то никакие не срабатывают :)
              Несмотря на всё это (можно постепенно привыкнуть) вещь действительно хорошая и, при определённых условиях, приложение с точки зрения пользователя ощущается как нативное.
                0
                Спасибо за статью. У Вас продублировалось «Поначалу было двоякое ощущение» и «Шифрование из коробки»
                  0
                  Спасибо за наблюдательность. Исправил.
                  0
                  За статью — спасибо.
                  P.S.
                  Поначалу было двоякое ощущение, мол, никакой тебе ORM, реально нет sql, запись ведется только внутри callback. Непривычно и странно, особенно для веб-разработчика родом из PHP, выросшего на ActiveRecord и Doctrine.

                  Абзац продублировался.
                    0
                    Ubuntu 12.6


                    Это всё было в каком году?

                    С его помощью можно относительно быстро собрать прототип приложения, отработать структуру и юзабилити.


                    То есть потом придётся переписывать?
                      +3

                      Решили запилить RN компонент (дающий 2/3 функционала) в xamarin проект в надежде сократить время разработки второй платформы. Сделали iOS, прострадали лишнюю неделю минимум на отлов багов и придумывания взаимодействия реакта и xamarin. Планировали отыграться на android, но в итоге потратили еще недели две на интеграцию и отлов багов в android, связанных со взаимодействием RN компонента и натива.
                      Вобщем, в целях саморазвития и вообще — интересно. С точки зрения бизнеса — крайне сомнительно. Как минимум, если компонент будет использоваться только в одном проекте.

                        0
                        Хорошо бы еще услышать лучшие практики/советы/инструменты по продуктивности в RN.

                        Например до меня не сразу дошло, что можно в live режиме менять UI(CSS) в React Native Debugger без ребилда приложения.

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

                        Сам работаю с RN уже как 2 месяца. В целом доволен.
                          0
                          В первой версии все бегало по сокетам на Meteor.JS Файлы закачивались средствами метеора.

                          Сейчас, когда приложение стало самостоятельным (без серверной части), есть только обертка на Google.Drive (react-native-google-drive-api-wrapper )

                          Но там все стандартно — обычный fetch

                          код внутри враппера:

                          fetch(
                             `${uploadUrl}?uploadType=multipart`, {
                                 method: "POST",
                                 headers: GDrive._createHeaders(
                                     `multipart/related; boundary=${this.params.boundary}`,
                                     body.length
                                 ),
                                 body
                             }
                          );


                          Косяк был только в одном месте — из файловой системы (react-native-fs) бинарный файл приходил только в base64. Как выяснилось, Google.Drive спокойно принимает этот формат. Но для его отправки на сервера просто нужно было указать `Content-Transfer-Encoding: base64\n\n`; в body запроса. Насколько помню, в компоненте react-native-google-drive-api-wrapper это учли и код поправили.
                            0
                            Учту.Спасибо!

                            Будем на связи.
                          0
                          Присоединяюсь к флешмобу.

                          Делаю кросс-платформенное приложение-плеер на React Native. Сначала сделал работу со звуком на open-source компонентах. Всё работало. Даже информация о треке выводилась в систему через компонент, оборачивающий MPNowPlayingInfo и MediaSession Metadata.

                          Но плеер подразумевает работу в фоне большую часть времени. В iOS это решается через соответствующий флаг в Capabilities. В Android же единственный верный путь, насколько я понял, – это отдельная служба. Портирую сейчас логику на Java, используя код из тех самых open-source компонентов. Хорошо хоть UI уцелел.
                            0
                            Тоже делал плеер. Обошелся возможностями react-native-track-player.
                            +1
                            Синхронное выполнение асинхронной функции

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

                             const setupBackupFolders = async (init = false) => {
                               // some stuff there...
                               await RunSomeAsyncFuncInSyncMode(foo, bar)
                               RunFuncAfter(bar)
                             };
                            setupBackupFolders();
                            doBaz();
                            


                            В этом случае doBaz может выполниться раньше, чем RunFuncAfter. И еще, если setupBackupFolders или RunSomeAsyncFuncInSyncMode бросит исключение (в виде reject), оно «потеряется», будет UnhandledPromiseRejection, код продолжит выполняться. Если это сделает синхронная функция doBaz, приложение упадет (если считать что нет обработчика в вызывающем коде).

                            И да, для метода класса React.Component это работает тоже. (в справке React, ReactNative об этом умалчивают, хотя это само собой подразумевается).


                            Само собой это подразумевается именно потому, что async/await следует воспринимать ни в коем случае не как «sync mode» для асинхронных функций, а лишь как синтаксический сахар над промисами, которые следует воспринимать как синтаксический сахар над коллбэками. Строго говоря это не совсем так, но лучше думать об async/await именно в таком ключе, ни в коем случае это не «синхронный способ выполнить асинхронный код».
                              0
                              монолитный и тяжелый moment, отлично заменяется на модульный date-fns.

                              Only users with full accounts can post comments. Log in, please.