Навигация

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

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

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

В завершение соберём адаптивный пример с использованием всех навигационных компонентов.

Экран — отдельное состояние интерфейса, отображающееся в один момент времени.

Сценарий — последовательность экранов, объединённых одной задачей пользователя. Например, экран “Настройки”, откуда можно перейти на экраны “Уведомления”, “Конфиденциональность и безопасность” и другие связанные экраны.

Раздел — крупная часть приложения со своими сценариями. Например, например “Профиль” или “Сообщения”.

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

  • SplitLayout с передачей в свойство header заглушки в виде PanelHeader, чтобы компенсировать боковые отступы для видимых PanelHeader (при использовании платформы vkcom заглушку можно не создавать).
  • SplitCol с передачей свойств stretchedOnMobile и autoSpaced. В зависимости от ширины экрана:
    • stretchedOnMobile растягивает колонку на всю ширину на мобильных устройствах;
    • autoSpaced включает автоматические боковые отступы на широких экранах, в частности, это нужно при применении компонента Group.

Так как эта обёртка должна быть одна на всё приложение, её достаточно разместить ближе к корню — в точке входа приложения.

src/App.tsx
import { SplitLayout, SplitCol } from '@vkontakte/vkui';
 
export default function App() {
  return (
    <SplitLayout header={<PanelHeader delimiter="none" />}>
      <SplitCol stretchedOnMobile autoSpaced>
        {/* ... */}
      </SplitCol>
    </Panel>
  );
};

Описывается с помощью компонента Panel. Также, чтобы пользователь знал на каком экране он находится, рекомендуется добавлять заголовок с помощью компонента PanelHeader.

Panel
  └─ PanelHeader
  └─ <content>

Ниже пример с описанием начального экрана приложения в отдельном файле.

src/panels/home.tsx
import { type PanelProps, Panel, PanelHeader } from '@vkontakte/vkui';
 
/**
 * Как пример, наследуем весь `PanelProps`, но достаточно будет
 * передавать только идентификатор (`Pick<PanelProps, 'id'>`),
 * а остальные свойства определять тут при необходимости.
 */
export const Home = (props: PanelProps) => {
  return (
    <Panel {...props}>
      <PanelHeader>Главная</PanelHeader>
      Привет, Мир!
    </Panel>
  );
};

За переключение экранов отвечает компонент View. Принимает необходимое количество Panel с уникальным id. Далее id с нужным экраном передаётся в свойство activePanel.

View
  └─ Panel N
    └─ PanelHeader
    └─ <content>

Представим простой пример в отдельном файле.

src/router.tsx
import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
 
export const Router = () => {
  const [activePanel, setActivePanel] = useState('panel-1');
  return (
    <View activePanel={activePanel}>
      <Panel id="panel-1">
        <PanelHeader>Панель 1</PanelHeader>
        <Button onClick={() => setActivePanel('panel-2')}>Перейти к панели 2</Button>
      </Panel>
      <Panel id="panel-2">
        <PanelHeader>Панель 2</PanelHeader>
        <Button onClick={() => setActivePanel('panel-1')}>Перейти к панели 1</Button>
      </Panel>
    </View>
  );
};

Для создания разделов есть два компонента: Root и Epic. Использование того или другого зависит от требований к приложению и к дизайну.

Root – универсальный вариант для организации разделов. Принимает необходимое количество View с уникальным id. Далее id с нужным сценарием передаётся в свойство activeView.

Root
  └─ View N
    └─ Panel N
      └─ PanelHeader
      └─ <content>
src/scenarios.tsx
import { useState } from 'react';
import { View, Panel, PanelHeader, PanelHeaderBack, Button } from '@vkontakte/vkui';
 
export const FirstScenario = ({ onBack }) => {
  const [activePanel, setActivePanel] = useState('first-panel-1');
  return (
    <View activePanel={activePanel}>
      <Panel id="first-panel-1">
        <PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
        <Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
      </Panel>
      <Panel id="first-panel-2">
        <PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
        <Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
      </Panel>
    </View>
  );
};
 
export const SecondScenario = ({ onBack }) => {
  const [activePanel, setActivePanel] = useState('second-panel-1');
  return (
    <View activePanel={activePanel}>
      <Panel id="second-panel-1">
        <PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
        <Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
      </Panel>
      <Panel id="second-panel-2">
        <PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
        <Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
      </Panel>
    </View>
  );
};
src/router.tsx
import { useState } from 'react';
import { Root } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
 
export const Router = () => {
  const [activeView, setActiveView] = useState('main');
  const onBack = () => setActiveView('main');
  return (
    <Root activeView={activeView}>
      <View id="main" activePanel="main-panel">
        <Panel id="main-panel">
          <PanelHeader>Главный экран</PanelHeader>
          <Button onClick={() => setActiveView('first')}>Перейти к сценарию 1</Button>
          <Button onClick={() => setActiveView('second')}>Перейти к сценарию 2</Button>
        </Panel>
      </View>
      <FirstScenario id="first" onBack={onBack} />
      <SecondScenario id="second" onBack={onBack} />
    </Root>
  );
};

Epic используется для мобильных приложений, которые по дизайну требуют наличия классической нижней панели с основными разделами. Применение можно увидеть на m.vk.com ↗ или в нативном приложении VK.

Отличия от Root следующие:

  • принимает Tabbar через одноимённое свойство tabbar;
  • может содержать в качестве потомков не только View, но и Root для создания более глубокой иерархии сценариев;
  • переключает разделы без анимаций.

Принимает необходимое количество View и/или Root с уникальным id. Далее id с нужным сценарием передаётся в свойство activeStory.

Epic
  └─ View N
    └─ Panel N
      └─ PanelHeader
      └─ <content>
  └─ Root N
    └─ View N
      └─ Panel N
        └─ PanelHeader
        └─ <content>
src/scenarios.tsx
import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
 
export const FirstScenario = () => {
  const [activePanel, setActivePanel] = useState('first-panel-1');
  return (
    <View activePanel={activePanel}>
      <Panel id="first-panel-1">
        <PanelHeader>Панель 1</PanelHeader>
        <Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
      </Panel>
      <Panel id="first-panel-2">
        <PanelHeader>Панель 2</PanelHeader>
        <Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
      </Panel>
    </View>
  );
};
 
export const SecondScenario = () => {
  const [activePanel, setActivePanel] = useState('second-panel-1');
  return (
    <View activePanel={activePanel}>
      <Panel id="second-panel-1">
        <PanelHeader>Панель 1</PanelHeader>
        <Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
      </Panel>
      <Panel id="second-panel-2">
        <PanelHeader>Панель 2</PanelHeader>
        <Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
      </Panel>
    </View>
  );
};
src/router.tsx
import { useState } from 'react';
import { Epic } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
 
export const Router = () => {
  const [activeStory, setActiveStory] = useState('main');
  return (
    <Epic
      activeStory={activeStory}
      tabbar={
        <Tabbar>
          <TabbarItem
            label="Главный экран"
            selected={activeStory === id}
            onClick={() => setActiveStory('main')}
          >
            🏠
          </TabbarItem>
          <TabbarItem
            label="Перейти к сценарию 1"
            selected={activeStory === id}
            onClick={() => setActiveStory('first')}
          >
            1️⃣
          </TabbarItem>
          <TabbarItem
            label="Перейти к сценарию 2"
            selected={activeStory === id}
            onClick={() => setActiveStory('second')}
          >
            2️⃣
          </TabbarItem>
        </Tabbar>
      }
    >
      <View id="main" activePanel="main-panel">
        <Panel id="main-panel">
          <PanelHeader>Главный экран</PanelHeader>
        </Panel>
      </View>
      <FirstScenario id="first" />
      <SecondScenario id="second" />
    </Epic>
  );
};

Создадим приложение вот с такой структурой:

app (Epic)
  └─ profile (View)
    └─ profile-panel (Panel)
  └─ feed (View)
    └─ feed-panel (Panel)
  └─ messenger (View)
    └─ messenger-panel (Panel)
  └─ more (Root)
    └─ main-scenario (View)
      └─ main-menu-panel (Panel)
    └─ notification-scenario (View)
      └─ notification-main-panel (Panel)
      └─ notification-messenger-panel (Panel)
    └─ about-scenario (View)
      └─ about-version-panel (Panel)

Для навигации по основным разделам на мобильных экранах используется (Tabbar), а на настольных — (SplitCol) c Cell в роли бокового меню.

Загружается...