Тонкости вложения навигаторов в React Native с использованием реакции-навигации

Это последний пост в серии из 4 частей о создании приложения для iOS и Android для uncovercity с использованием React Native. Вы можете найти другие здесь:

  1. Ускорение создания приложения с ужином-сюрпризом в React Native с помощью Expo
  2. Боевое тестирование API райдшеринга и MapView от React Native в Expo
  3. Поддержка нескольких языков в React Native с локализацией Expo
  4. Тонкости вложения навигаторов в React Native с использованием реакции-навигации

В одном из моих последних проектов я создал собственное приложение для испанского стартапа uncovercity с помощью React Native.

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

Все начинается с дерева

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

- Main (StackNavigator)
  - Home
  - Login
  - Recover password
  - Sidebar (DrawerNavigator)
    - Router (StackNavigator)
      - Experience
      - Experience Map
      - Experience Active
      - Experience Active Detail
      - Map
      - Evaluation
    - PastExperiences
    - FAQ
    - Contact

Позвольте мне провести вас по этому дереву шаг за шагом.

Уровень 1: Stack Navigator - экран приветствия и входа в систему

Как видите, корневой элемент - это StackNavigator. Поскольку первые три экрана («Домой», «Вход в систему», «Восстановить пароль») просто отображаются друг над другом при переходе от одного к другому, мы можем использовать простой StackNavigator, который имеет приятный переход по умолчанию (слайд-налево на iOS, слайд-вверх на Android).

Уровень 2: Навигатор ящика - боковая панель

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

Из боковой панели пользователь может получить быстрый доступ к некоторым статическим страницам, таким как FAQ и Контакты. Но по умолчанию он отображает другой элемент контейнера, который содержит пару других суб-маршрутов, Маршрутизатор.

Уровень 3: Stack Navigator - маршрутизатор

Маршрутизатор является сердцем приложения и содержит большую часть контента. Здесь пользователь может увидеть текущий статус своего (ужина) опыта, экран с подробной информацией о месте, карту с точкой посадки / возврата и экран оценки, который позволяет ей оценить опыт, когда он закончится.

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

Проблемы, с которыми я столкнулся при использовании реакции-навигации

React Navigation упрощает многие вещи, которые сложно достичь в родных iOS и Android. Но он также имеет свой собственный набор проблем, решение или обходное решение которых не всегда бывает простым.

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

Проблема # 1: включение панелей заголовков внутри DrawerNavigator

StackNavigator имеет панель заголовка, которая включена по умолчанию. Его можно отключить с помощью headerMode: none в визуальных параметрах StackNavigator. Это желательно, если вы хотите, чтобы текущий экран покрыл всю область просмотра, включая пространство, которое обычно занимает заголовок.

Что не так интуитивно понятно, так это когда вы находитесь внутри DrawerNavigator и хотите включить панель заголовка для всех его дочерних элементов. DrawerNavigator не предоставляет наследуемый headerMode параметр, такой как StackNavigator, поскольку его цель - не предоставить вам стек экранов с возможностью навигации, а предоставить плоскую иерархию, которая по умолчанию содержит экраны без заголовка.

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

const One = StackNavigator(
  {
    main: { screen: Main } // only has one route
  },
  {
    navigationOptions: {
      headerMode: 'screen' // enabling header mode for main screen
    }
  }
);
const AppNavigator = DrawerNavigator({
  one: { screen: One },
  two: { screen: Two }
});

Задача # 2: переключение боковой панели из вложенных маршрутов

Затем мне было интересно, как переключить боковую панель DrawerNavigator с компонента внутри вложенного маршрута.

Начиная с обновления React Navigation 2.0 вы можете использовать функцию openDrawer в свойстве navigation. Но это свойство не доступно в каждом компоненте, если вы не передадите его как prop, что иногда означает передачу его по длинной цепочке компонентов.

Более изящное решение (указанное в документации по response-navigation) - сохранить ссылку DrawerNavigator в сервисе и импортировать ее везде, где вам нужен доступ к функции openDrawer. Вот что делают методы службы:

function setTopLevelNavigator(navigatorRef) {
  _navigator = navigatorRef;
}
function openDrawer(routeName, params){
  _navigator.dispatch(DrawerActions.openDrawer());
}

Компонент, который отображает DrawerNavigator, затем должен передать ссылку навигатора функции setTopLevelNavigator.

<TopLevelNavigator
  ref={navigatorRef => {
    NavigationService.setTopLevelNavigator(navigatorRef);
  }}
/>

Теперь вы можете просто вызвать функцию navigate из любого места, чтобы получить доступ к методу openDrawer DrawerNavigator:

NavigationService.openDrawer();

Вероятно, вы могли бы сделать то же самое, используя React Context API (представленный в React 16.3) или используя Redux или Mobx, но на момент написания этого кода это казалось самым простым решением без каких-либо накладных расходов.

Задача # 3: ввод аутентифицированных пользователей в пользовательскую зону

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

Возвращаясь к дереву навигации, описанному выше, это означало бы перенести ее из корневого навигатора (Main) на один из экранов, вложенных в стек маршрутизатора. Совершенно прыжок!

Main -> Sidebar -> Router -> Experience
     -> Login

В Основном мы получаем текущий статус пользователей из локального хранилища телефона (AsyncStorage). Если пользователь находится в журнале, он будет содержать объект с данными пользователя (идентификатор, адрес электронной почты, имя), и мы переведем его на следующий уровень, Боковая панель. не вошла в систему, вместо этого она перейдет на экран Вход.

const user = await AsyncStorage.getItem('user');

Поскольку это async функция, перед рендерингом внешнего навигатора нам нужно дождаться ее завершения, поэтому мы возвращаем экран загрузки в нашей функции рендеринга и устанавливаем для loading значение true только после завершения AsyncStorage операции.

if (this.state.loading) {
  return <LoadingScreen />;
}

В navigationOptions навигатора второго уровня (Боковая панель) мы устанавливаем initialRouteName на следующий экран в нашей иерархии навигации в зависимости от статуса входа пользователя.

const Navigator = createStackNavigator({
   ...routesObject...
  }, 
    {
      initialRouteName: (this.props.loggedIn) ? 'Sidebar' : 'Login',
    },
});

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

const Drawer = createDrawerNavigator({
  Router: { screen: Router }, // another StackNavigator in here
  PastExperiences: { screen: PastExperiencesScreen },
  FAQ: { screen: FaqScreen },
  Contact: { ContactScreen }
})

Внутри маршрутизатора экран, который мы показываем пользователю, зависит от текущего состояния его опыта. Детали взаимодействия являются частью последующего запроса API, который разрешается только после входа пользователя в систему.

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

Задача №4: отображение вложенного экрана на основе ответа API без ущерба для взаимодействия с пользователем

И последнее, но не менее важное: мне пришлось решить проблему, связанную с тем, как приложение взаимодействует с API. Внутри маршрутизатора экран, который видит пользователь, основан на ответе API от службы совместного использования.

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

Main -> Sidebar -> Router -> Pickup screen
                          -> Map
                          -> Return screen
                          -> Evaluation

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

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

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

Для проблемы номер два мне пришлось запросить небольшое изменение API, которое возвращало бы статус пользователя в начальном ответе сервера (тот, который изначально не был разработан для этой цели). Но я также должен был убедиться, что экран не появится, прежде чем оценивать ответ. Поэтому мне пришлось использовать технику, которую я уже использовал в Задаче 2, возвращая LoadingScreen, пока другая логика выполняется в жизненном цикле компонента.

Тем, кто живет на передовых технологиях, React Suspense поможет в будущем с подобным асинхронным рендерингом.

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

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

Примерно так выглядит функциональный компонент без сохранения состояния. Он создает новый StackNavigator при двух условиях:

  • initialRouteName изменился
  • ранее не было определено RouterStack.
let RouterStack;
let lastRouteName
const RouterStackWithInitialRoute = ({ initialRouteName }) => {
  if (RouterStack === null || lastRouteName !== initialRouteName) {
    lastRouteName = initialRouteName;
    RouterStack = createStackNavigator(
      require('../../../constants/routeConfig').default,
      {
        initialRouteName,
      });
     }
  return <RouterStack />;
};

Передача имени исходного маршрута в этот компонент выглядит следующим образом:

render() {
  return this.state.loading ?
    <LoadingScreen /> :
    <RouterStackWithInitialRoute 
      initialRouteName={this.state.initialRouteName} />;
}

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

Помните, что все примеры кода - это просто упрощение реального кода, чтобы проиллюстрировать то, что я хочу высказать. Так что нет гарантии, что они будут работать, если вы попытаетесь скопировать / вставить любой из них :)

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

Это моя серия статей о создании мобильного приложения для Uncovercity.

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

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

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

Если вы оказались в Испании, вы можете найти приложение в магазинах приложений iOS и Google. Просто не забудьте забронировать столик, чтобы получить все возможности приложения :)