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

Это руководство будет продолжено с кода, созданного в конце части 2. Вы можете скачать стартовый код здесь.

"Реализация"

Вот как мы разобьем интерфейс нашего веб-сайта. Для веб-сайта, который я создаю, я хочу, чтобы синий раздел (панель навигации) оставался одинаковым для всех представлений. Когда мы переходим к представлению, меняет местами только красный раздел (Контент).

Шаблон макета

Мы представим новый виджет под названием LayoutTemplate. Он будет предоставлен как корневой макет MaterialApp через свойство home. Он будет содержать весь код NavigationBar и NavigationDrawer, который у нас есть в настоящее время в HomeView, с одним дополнением. Дочерним элементом расширенного виджета будет навигатор, который позволит нам заменять содержимое расширенного виджета только с помощью вызовов навигации. Создайте новую папку в представлениях с именем layout_template и внутри нового файла с именем layout_template.dart.

class LayoutTemplate extends StatelessWidget {
  const LayoutTemplate({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, sizingInformation) => Scaffold(
        drawer: sizingInformation.isMobile ? NavigationDrawer() : null,
        backgroundColor: Colors.white,
        body: CenteredView(
          child: Column(
            children: <Widget>[
              NavigationBar(),
              Expanded(
                  child: Navigator(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Пользовательская навигация

Основываясь на дизайне, мы знаем, что когда пользователь куда-то перемещается, мы хотим, чтобы UI NavigationBar и NavigationDrawer оставались на месте и заменялся только раздел содержимого. Отмечено красным на изображении выше. Для этого нам нужно встроить виджет Navigator как дочерний элемент расширенного дочернего элемента. Мне нравится настраивать NavigationService, чтобы я мог перемещаться из моих ViewModels (которые мы добавим в следующем выпуске). Создайте новую папку с именем services в lib и внутри нового файла с именем navigation_service.dart.

class NavigationService {
  final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();
  Future<dynamic> navigateTo(String routeName) {
    return navigatorKey.currentState.pushNamed(routeName);
  }
  bool goBack() {
    return navigatorKey.currentState.pop();
  }
}

Быстрое заявление об отказе от ответственности Имейте в виду, что при использовании моей настройки архитектуры Mvvm служба навигации будет использоваться в viewModel, а не в пользовательском интерфейсе. Только viewModel должен выполнять функции или действия, которые изменяют состояние службы навигации или приложения. Мы по-прежнему МОЖЕМ использовать его для передачи значения, такого как клавиша, в навигаторе, но он не должен выполнять функции, которые изменяют состояние. В этом руководстве мы сделаем это, но это потому, что у нас еще нет правильной настройки моделей просмотра и управления состоянием.

Настроить GetIt

Теперь, когда это не проблема, мы можем настроить get_it для местоположения нашей службы. Добавьте пакет в свой pubspec

get_it:

Создайте новый файл в папке lib с именем locator.dart.

import 'package:get_it/get_it.dart';
import 'package:the_basics/services/navigation_service.dart';
GetIt locator = GetIt.instance;
void setupLocator() {
  locator.registerLazySingleton(() => NavigationService());
}

Затем не забудьте вызвать setupLocator перед запуском приложения. Откройте main.dart и обновите свою основную функцию.

void main() {
  setupLocator();
  runApp(MyApp());
}

Если материал get_it сбивает с толку, вы можете прочитать короткий пост здесь, в котором рассказывается, как и зачем его использовать.

Именованная маршрутизация

Я подробно рассмотрел навигацию во Flutter, и сегодня мы будем использовать именованную маршрутизацию во Flutter. Навигатор ожидает функции, которая генерирует маршрут по имени, ЕСЛИ вы используете именованную навигацию. Мы создадим функцию, которая будет предоставлять наши различные маршруты на основе имен. Первым делом мы создадим новую папку с именем routing в папке lib. Внутри этой папки мы создадим новый файл с именем route_names.dart.

const String HomeRoute = "home";
const String AboutRoute = "about";
const String EpisodesRoute = "episodes";

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

Route<dynamic> generateRoute(RouteSettings settings) {
  print('generateRoute: ${settings.name}');
  switch (settings.name) {
    case HomeRoute:
      return _getPageRoute(HomeView());
    case EpisodesRoute:
      return _getPageRoute(EpisodesView());
    case AboutRoute:
      return _getPageRoute(AboutView());
    default:
      return _getPageRoute(HomeView());
  }
}
PageRoute _getPageRoute(Widget child) {
  return MaterialPageRoute(
    builder: (context) => child,
  );
}

Что мы здесь делаем, так это настраиваем оператор switch для проверки того, какое представление мы запрашиваем через именованную навигацию, а затем создаем и возвращаем этот виджет в MaterialPageRoute. Теперь, когда все настроено, мы можем предоставить код нашему Navigator в LayoutTemplate.

class LayoutTemplate extends StatelessWidget {
  const LayoutTemplate({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, sizingInformation) => Scaffold(
              ...
              Expanded(
                  child: Navigator(
                key: locator<NavigationService>().navigatorKey,
                onGenerateRoute: generateRoute,
                initialRoute: HomeRoute,
              ))
            ],
          ),
        ),
      ),
    );
  }
}

Мы устанавливаем клавишу навигатора на ту, которой мы можем управлять из службы. Мы предоставляем вызов generateRoute и устанавливаем для initialRoute значение HomeRoute. Затем мы можем обновить файл HomeView, чтобы удалить весь код, который сейчас находится в LayoutTemplate, а затем также создать еще два представления. Эпизоды и про.

class HomeView extends StatelessWidget {
  const HomeView({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ScreenTypeLayout(
      mobile: HomeContentMobile(),
      desktop: HomeContentDesktop(),
    );
  }
}

Создайте новую папку в представлениях, называемых эпизодами, и внутри нового файла с именем epsodes_view.dart.

class EpisodesView extends StatelessWidget {
  const EpisodesView({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Episodes View'),
    );
  }
}

Создайте еще одну папку в представлениях с именем about и внутри нового файла с именем about_view.dart.

class AboutView extends StatelessWidget {
  const AboutView({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('About View'),
    );
  }
}

Теперь нам нужно выполнить собственно навигацию. Это будет сделано в NavBarItem, где мы получим доступ к NavigationService и navigateTo. Вы не должны делать это непосредственно из пользовательского интерфейса, но мы не будем рассматривать настройку ViewModel в этой статье, поэтому мы должны сделать это плохим способом.

Мы обновим navbar_item. Мы заключим Text в GestureDetector, а затем вызовем navigateTo onTap. Мы также передадим дополнительное значение, которым будет navigationPath.

class NavBarItem extends StatelessWidget {
  final String title;
  final String navigationPath;
  const NavBarItem(this.title, this.navigationPath);
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        locator<NavigationService>().navigateTo(navigationPath);
      },
      child: Text(
        title,
        style: TextStyle(fontSize: 18),
      ),
    );
  }
}

Затем мы должны пройти через код и обновить все, где используется NavBarItem. Перейдите к navigation_bar_tablet_desktop.dart и пройдите правильный маршрут для элементов.

...
NavBarItem('Episodes', EpisodesRoute),
SizedBox(
  width: 60,
),
NavBarItem('About', AboutRoute),
...

Перейдите к DrawerItem и обновите конструктор, чтобы он принял новую строку, navigationPath и передал ее NavBarItem. Вероятно, это должен быть один виджет, который меняет свою компоновку в зависимости от типа экрана. Вы можете сделать это как упражнение. Объедините NavBarItem и DrawerItem в один адаптивный виджет.

class DrawerItem extends StatelessWidget {
  final String title;
  final IconData icon;
  final String navigationPath;
  const DrawerItem(this.title, this.icon, this.navigationPath);
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 30, top: 60),
      child: Row(
        children: <Widget>[
          Icon(icon),
          SizedBox(
            width: 30,
          ),
          NavBarItem(title, navigationPath)
        ],
      ),
    );
  }
}

Затем перейдите к навигационному ящику и обновите DrawerItem, чтобы выбрать правильные пути.

...
child: Column(
  children: <Widget>[
    NavigationDrawerHeader(),
    DrawerItem('Episodes', Icons.videocam, EpisodesRoute),
    DrawerItem('About', Icons.help, AboutRoute),
  ],
),
...

Запустите код прямо сейчас, и у вас будет веб-сайт, на котором вы сможете перемещаться, сохраняя постоянную навигационную панель и просто заменяя раздел содержимого. Довольно законно, этот код будет вовремя свернут в пакет, поэтому он будет работать намного быстрее. Если вы запустите код сейчас и нажмете «Эпизоды» или «О программе», вы увидите, что контент заменяется переходом по умолчанию MaterialPage. И последнее, что я хотел бы сделать, это заставить его исчезнуть. Откройте файл router.dart, добавьте класс _FadeRoute внизу и обновите функцию _getPageRoute.

PageRoute _getPageRoute(Widget child) {
  return _FadeRoute(
    child: child,
  );
}
class _FadeRoute extends PageRouteBuilder {
  final Widget child;
  _FadeRoute({this.child})
      : super(
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              child,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              FadeTransition(
            opacity: animation,
            child: child,
          ),
        );
}

Теперь, если вы запустите код, страницы, по которым вы переходите, будут постепенно появляться и исчезать. Я не знаю, как сопоставить путь URL-адреса в браузере с настраиваемым вложенным навигатором. По мере того, как я буду исследовать, я, вероятно, это выясню. А пока это все по навигации. Используя это, вы сможете создать базовое веб-приложение (без прямой навигации по URL-адресам).

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