Как разделить навигацию в React Native

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

Этот вопрос возник из личного раздражения и сомнения, есть ли другие библиотеки, которые выполняют навигацию в React Native, и да, есть такие, как React Native Navigation от Wix, React Router Native и т.д..

Итак, что если однажды я захочу использовать другую библиотеку навигации, отличную от React Navigation, сколько усилий мне придется приложить для этого, учитывая, что действия навигации пристыковываются прямо в Presenter?

Поэтому я решил провести этот эксперимент в личном проекте, используя такие концепции, как TDD и Clean Architecture, с которым вы можете ознакомиться здесь. И я воспользовался этой возможностью, чтобы написать эту статью.

Эта статья разделена на следующие шаги:

1 🧐. Имеет ли смысл отделять навигацию в React Native?

2 🤓. Настройка начального каркаса навигации с помощью React Navigation

3 😎. Использование слоев для отделения React Navigation от моего приложения

4 🤌🏻. Вызов действия Navigate в нашем презентаторе

5 🧑🏻💻. Написание некоторых сценариев модульного тестирования

🧐 Имеет ли смысл отделять навигацию в React Native?

Если мы подумаем о дополнительной работе, которую это может принести в нашу разработку, возможно, не имеет смысла отделять действия навигации от презентатора. В конце концов, есть ли сценарий, при котором я откажусь от React Navigation ради использования другой библиотеки?

В данном случае, я думаю, это будет зависеть от решения разработчика или команды в целом.

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

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

type Props = {
  navigate: Navigate;
};

const WelcomePresenter: React.FC<Props> = ({ navigate }) => {
  const buttonAction = () => {
    navigate.navigateToMyPlans();
  };
}
Войдите в полноэкранный режим Выход из полноэкранного режима
const WelcomePresenter: React.FC<Props> = ({ navigation }) => {
  const buttonAction = () => {
    navigation.navigate(Routes.ACTIVITY);
  };
}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

🤓 Настройка начального навигационного фреймворка с помощью React Navigation

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

Сначала давайте настроим наш навигационный контейнер:

type Props = {
  setNavigationTop: (navigatorRef: NavigationContainerRef<any>) => void;
  initialRouteName: keyof StackParams;
};

const Navigation: React.FC<Props> = ({
  setNavigationTop,
  initialRouteName,
}) => {
  return (
    <NavigationContainer ref={setNavigationTop} theme={DefaultThemes}>
      <StackNavigation initialRouteName={initialRouteName} />
    </NavigationContainer>
  );
};

export default Navigation;
Войдите в полноэкранный режим Выход из полноэкранного режима

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

const Stack = createNativeStackNavigator<StackParams>();

type StackNavigationParams = {
  initialRouteName: keyof StackParams;
};

const StackNavigation: React.FC<StackNavigationParams> = ({
  initialRouteName,
}) => {
  return (
    <Stack.Navigator
      initialRouteName={initialRouteName}
      screenOptions={{
        headerTransparent: true,
        headerBackTitleVisible: false,
        headerTintColor: colors.white,
        title: '',
      }}
    >
      <Stack.Screen name={Routes.WELCOME}>
        {(props) => <WelcomeFactory {...props} />}
      </Stack.Screen>
    </Stack.Navigator>
  );
};

export default StackNavigation;
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь осталось только добавить нашу навигацию в корень приложения, чтобы она выглядела следующим образом

type Props = {
  initialRouteName: keyof StackParams;
};

const Main: React.FC<Props> = ({ initialRouteName }) => {
  return (
    <WrapperScreen>
      <StatusBar barStyle="dark-content" />
      <Navigation
        setNavigationTop={(navigationRef: NavigationContainerRef<any>) =>
          setTopLevelNavigator(navigationRef)
        }
        initialRouteName={initialRouteName}
      />
    </WrapperScreen>
  );
};
Войдите в полноэкранный режим Выход из полноэкранного режима

Круто, это будет необходимая конфигурация для навигации между экранами в нашем приложении. Разумеется, я не стал подробно описывать используемые импорты и типы, поскольку это не является целью данной статьи. Но если вы хотите узнать подробности, вы можете найти их в этом репозитории.

😎 Использование слоев для отделения React Navigation от моего приложения

И вот наконец мы добрались до вишенки 🍒 торта, здесь мы немного нарушим ритуал, и будем использовать некоторые концепции, такие как слои, интерфейсы, адаптеры и т.д.

Чтобы представить эту развязку навигационного действия, давайте воспользуемся этой диаграммой, которая демонстрирует слои, которые мы будем использовать.

Но прежде чем мы начнем, я быстро объясню диаграмму выше, предложение заключается в том, чтобы отвязать наше приложение от внешних зависимостей, таких как React Navigation, например, мы будем использовать Adapter. Другим интересным моментом является слой Domain, где наш слой Presentation ничего не знает о слое Data и Infra, связь между этими слоями осуществляется через слой Domain, именно его мы будем использовать в нашем Presenter для осуществления навигации.

Начиная с домена:

export interface Navigate {
  navigateToMyPlans(params?: RouteParams): void;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Движение к данным:

export class NavigateScreenMyPlans implements Navigate {
  constructor(readonly navigateScreen: NavigateScreen) {}

  navigateToMyPlans(params?: RouteParams | undefined): void {
    this.navigateScreen.navigate(Routes.ACTIVITY, params);
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима
export interface NavigateScreen {
  navigate(routeName: string, params?: GenericObject | undefined): void;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

И только теперь мы будем использовать действия React Navigation в слое Infra:

export class ReactNavigationAdapter implements NavigateScreen {
  constructor(readonly navigation: NavigationContainerRef<any>) {}
  navigate(routeName: string, params: GenericObject | undefined): void {
    this.navigation.dispatch(
      CommonActions.navigate({ name: routeName, params: params }),
    );
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

🤌🏻 Вызов действия Navigate в нашем Presenter

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

type Props = {
  navigate: Navigate;
};

const WelcomePresenter: React.FC<Props> = ({ navigate }) => {
  const [toggleEnabled, componentsToggle] = useState(false);

  const buttonAction = () => {
    navigate.navigateToMyPlans();
  };

  return (
    <Welcome
      buttonAction={buttonAction}
      componentsToggle={componentsToggle}
      toggleEnabled={toggleEnabled}
    />
  );
};

export default WelcomePresenter;
Войдите в полноэкранный режим Выход из полноэкранного режима

Таким образом, если мы остановимся и понаблюдаем, то ни в одной точке нашего презентатора мы не узнаем, что используем React Navigation для осуществления навигации. То, что мы используем, — это просто интерфейс, который передается через props.

Но в какой момент мы передаем его через реквизит? Для этого мы будем использовать Фабрику, в которой мы соберем необходимые зависимости.

type Props = {
  route: RouteProp<StackParams, Routes>;
  navigation: any;
};

const WelcomeFactory: React.FC<Props> = () => {
  const navigate = useNavigate();
  const navigateScreen = new NavigateScreenMyPlans(navigate);
  return <Welcome navigate={navigateScreen} />;
};

export default WelcomeFactory;
Войдите в полноэкранный режим Выход из полноэкранного режима

🧑🏻💻 Написание некоторых сценариев модульного тестирования

И последнее, но не менее важное. Давайте посмотрим, как становятся проще и понятнее модульные тесты, включающие навигацию.

Поскольку использовался TDD, все слои, включающие навигацию, имеют модульные тесты, но поскольку основное внимание здесь уделяется отделению React Navigation от нашего презентатора, я покажу только тесты, выполненные в нашем презентаторе.

describe('Presentation: Welcome', () => {
  test('should navigate with success when button press', () => {
    const { sut, navigateToMyPlansSpy } = makeSut();
    const button = sut.getByTestId('button_id');

    fireEvent.press(button);
    expect(navigateToMyPlansSpy).toHaveBeenCalledTimes(1);
  });
});

const makeSut = () => {
  let navigation = {} as NavigationContainerRef<any>;

  render(
    <Navigation
      setNavigationTop={(navigationRef) => (navigation = navigationRef)}
      initialRouteName={Routes.WELCOME}
    />,
  );

  const navigate = new NavigateScreenMyPlans(navigation);

  const navigateToMyPlansSpy = jest.spyOn(navigate, 'navigateToMyPlans');

  const sut = render(<Welcome navigate={navigate} />);
  return { sut, navigateToMyPlansSpy };
};
Войдите в полноэкранный режим Выход из полноэкранного режима

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

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

Спасибо, ребята, увидимся в следующий раз.

Оцените статью
devanswers.ru
Добавить комментарий