Когда мы начинаем разрабатывать приложение на 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 из нашего презентера и таким образом получить больше свободы и легкости, чтобы в один прекрасный день изменить библиотеки навигации без серьезных проблем, кроме того, оставив наш презентер менее связанным.
Спасибо, ребята, увидимся в следующий раз.