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

Настройка маршрутизации во Flutter, особенно в Navigator 2.0, может быть очень утомительной и отнимающей много времени. Именно здесь на помощь приходит AutoRoute с его интуитивно понятным API и удобной генерацией кода, которая сэкономит вам много времени и усилий.

В этом уроке вы узнаете, как использовать простоту пакетов AutoRoute и Salomon Bottom Bar для создания элегантной нижней навигационной панели с вложенной маршрутизацией.

Готовое приложение

В этом уроке мы создадим простое приложение, которое будет состоять из трех основных разделов.

  • Посты

  • Пользователи

  • Настройки

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

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

В этом руководстве мы рассмотрим самый простой способ настройки подобной системы.

Смотрите анимацию в оригинале

Начало работы

Обновление Flutter 2.5

8 сентября 2021 года команда Google Flutter объявила о выпуске Flutter 2.5 и Dart 2.14. На этом занятии мы выполним разработку с использованием обновленных версий. Предоставленные файлы стартового и готового проектов построены с использованием новых версий Dart и Flutter. Поэтому, если вы еще не обновились, обязательно выполните команду flutter upgrade в своем терминале, прежде чем продолжить изучение этого руководства. 

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

  • В новой версии Flutter каждый раз, когда вы создаете новый проект, в него будет включена зависимость для разработки flutter_lints. Это поможет вам писать более чистый код прямо из коробки. Если вы еще не обновились, то не увидите существенной разницы в этом уроке, кроме зависимости для разработки (dev-зависимости) flutter_lints в файле pubspec.yaml.

  • Некоторые зависимости, которые мы будем использовать в этом проекте, зависят от meta 1.7.0, и если вы еще не обновились, то столкнетесь с проблемой, поскольку предыдущая версия Flutter привязана к более ранней версии meta. Вы можете легко решить это, переопределив версию meta. Просто добавьте следующий код в ваш файл pubspec.yaml:

dependency_overrides:
  meta: ^1.7.0
  • Если вы хотите продолжить обучение, используя файлы начального проекта, или просмотреть готовый проект на своем устройстве, но при этом еще не обновились, необходимо сделать следующее:

  1. Скачайте файлы проекта с GitHub (ссылки приведены ниже).

  2. Создайте новый проект Flutter на своем компьютере. 

  3. Скопируйте папку lib из загруженного проекта и вставьте ее на место папки lib во вновь созданном проекте. 

  4. Исправьте импорты.

  5. Убедитесь, что вы переопределили мета-зависимость, как указано выше. 

Зависимости

В этом руководстве мы будем использовать некоторые зависимости и dev-зависимости.

Для маршрутизации будут использоваться зависимость auto_route и dev-зависимость auto_route_generator. Обе они будут версии 2.3.2. Генератор автоматических маршрутов поможет нам генерировать код, который в противном случае нам пришлось бы писать самим. Именно этим и хорош AutoRoute, он позволяет нам обойтись без написания большого количества шаблонного кода. Для генерации кода нам также необходимо добавить build_runner версии 2.1.2 в качестве dev-зависимости.

Для создания стильной нижней навигационной панели мы воспользуемся пакетом Salomon Bottom Bar. Этот пакет был вдохновлен дизайном, созданным Aurélien Salomon. Я выбрал именно его, потому что дизайн очень аккуратный и привлекательный, а реализация этой навигационной панели невероятно проста. Если вы когда-либо создавали виджет BottomNavigationBar во Flutter, то уже знаете, как настроить навигационную панель из пакета Salomon Bottom Bar. Их синтаксис практически идентичен. Для этого проекта мы будем использовать версию 3.1.0 пакета.

Перейдите к добавлению всех этих зависимостей. После этого ваш файл pubspec.yaml должен выглядеть примерно так:

dependencies:
  flutter:
    sdk: flutter
  auto_route: ^2.3.2
  salomon_bottom_bar: ^3.1.0
  cupertino_icons: ^1.0.2

dev_dependencies:
  auto_route_generator: ^2.3.2
  build_runner: ^2.1.2

Если вы используете Visual Studio Code, то можете добавлять зависимости с помощью палитры команд. Благодаря обновленному плагину VS Code Flutter достаточно вызвать палитру команд и использовать команды "Dart: Add Dependency" и "Dart: Add Dev Dependency" для добавления пакетов в ваши проекты.

Обзор стартового проекта

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

В файле main.dart у нас есть AppWidget, который возвращает MaterialApp. Сейчас MaterialApp имеет виджет PostsPage в качестве аргумента home, но это изменится, когда мы реализуем маршрутизацию.

Затем в папке lib у нас также есть файл widgets.dart, в котором находятся виджеты PostTile и UserAvatar. Это необходимое разделение для оптимизации страниц, где используются эти виджеты. 

Также у нас имеется папка data, внутри которой находится файл app_data.dart. Этот файл содержит классы Post и User. Эти классы используются при создании макетов данных для постов и пользователей в приложении.

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

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

Папка Posts:

В папке posts находятся файлы posts_page.dart и single_post_page.dart.

  • В файле posts_page.dart у нас есть StatelessWidget, который будет отображать Column с тремя виджетами PostTile. Вы можете заметить, что на этой странице нет Scaffold. Когда мы перейдем к настройке нижней навигационной панели, вы поймете почему. 

  • В файле single_post_page.dart содержится StatelessWidget, который будет динамически отображать страницу с названием и цветом поста, соответствующим PostTile, который был нажат на странице постов.

Папка Users:

В папке users находятся файлы user_page.dart и user_profile_page.dart.

  • Users_page.dart содержит StatelessWidget, который отображает Column с тремя виджетами UserAvatar

  • В файле user_profile_page.dart находится StatelessWidget, который отображает страницу с динамически устанавливаемым цветом фона и именем пользователя, соответствующим UserAvatar, который был выбран на странице пользователей.

Папка настроек:

В папке настроек (settings) находится только один файл — settings_page.dart. Это самый простой из всех файлов функций, которые мы рассмотрели. Расположенный здесь виджет SettingsPage без сохранения состояния, который просто отображает текстовый заголовок и некоторые фейковые данные учетной записи пользователя.

Теперь, когда у вас есть ясное представление о стартовом проекте, давайте приступим к реализации маршрутизации. 

Конфигурация вложенной маршрутизации

В этом разделе мы настроим файл, который будет служить образцом для сгенерированного кода маршрутизации. Если вы работали с AutoRoute не так давно, то синтаксис здесь должен выглядеть довольно знакомым. Имейте в виду, что эта конфигурация будет отличаться от стандартной настройки маршрутизации AutoRouter. Это связано с тем, что мы будем следовать особым рекомендациям по созданию нижней панели навигации с вложенной маршрутизацией.

Исходная маршрутизация - HomePage

Прежде чем мы настроим маршрутизацию, давайте сначала убедимся, что у нас есть все необходимые для этого файлы. Создайте новый файл в папке lib и назовите его home_page.dart. Это будет файл, в котором мы определим нижнюю навигационную панель. Пока что просто создайте здесь StatelessWidget. Неважно, что он будет возвращать, потому что мы изменим это в ближайшее время. Пока можно просто вернуть Container.

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Конфигурация AutoRoute - router.dart

Теперь создайте новую папку в папке lib и назовите ее "routes". В ней создайте новый файл и назовите его router.dart. Это файл, в котором мы создадим образец для генератора кода.

Начнем настройку маршрутизатора с начального маршрута HomePage. Не забудьте импортировать пакет auto route и файл home_page.dart.

@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(
      path: '/',
      page: HomePage,
    )
  ],
)
class $AppRouter {}

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

Здесь мы указываем аргумент replaceInRouteName, чтобы сделать наши имена маршрутов менее избыточными. Когда вы будете переходить от одной страницы к другой, придется использовать сгенерированные имена маршрутов. Если не указать replaceInRouteName так, как мы сделали здесь, имя маршрута для нашей страницы SinglePostPage будет SinglePostPageRoute. С настроенным replaceInRouteName сгенерированное имя маршрута в этом примере будет SinglePostRoute.

Затем мы предоставляем List объектов AutoRoute в качестве аргумента routes. Здесь у нас есть один объект AutoRoute, который устанавливает нашу HomePage в качестве начального маршрута, предоставляя "/" в качестве аргумента path.

Далее нам нужно создать маршрутизаторы для разделов "Сообщения", "Пользователи" и "Настройки" приложения. Для этого добавьте List объектов AutoRoute в качестве аргумента  children существующего объекта AutoRoute, как показано ниже. Здесь также необходимо импортировать все используемые файлы виджетов страниц.

...
AutoRoute(
      path: '/',
      page: HomePage,
      children: [
        AutoRoute(
          path: 'posts',
          name: 'PostsRouter',
          page: EmptyRouterPage,
          children: [
            AutoRoute(path: '', page: PostsPage),
            AutoRoute(path: ':postId', page: SinglePostPage),
          ],
        ),
        AutoRoute(
          path: 'users',
          name: 'UsersRouter',
          page: EmptyRouterPage,
          children: [
            AutoRoute(path: '', page: UsersPage),
            AutoRoute(path: ':userId', page: UserProfilePage),
          ],
        ),
        AutoRoute(
          path: 'settings',
          name: 'SettingsRouter',
          page: SettingsPage,
        )
      ],
    )
...

Теперь давайте разберемся, что именно мы здесь делаем.

Маршрутизаторы постов и пользователей

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

  • path: Для аргумента path мы указываем имя пути, который хотелось бы использовать в маршрутизаторе.

  • name: String, указанная в качестве аргумента name, будет использоваться для создания имени маршрутизатора. Это имя можно использовать во время доступа к маршрутизатору при настройке нижней панели навигации или для навигации между страницами, расположенными в разных маршрутизаторах/вкладках навигации.

  • page: Здесь в качестве аргумента page мы указываем EmptyRouterPage (предоставляется пакетом AutoRoute). Это следует делать всякий раз, когда у вас есть вложенные маршруты для определенной нижней навигационной вкладки.

  • children: Аргумент children принимает List объектов AutoRoute. Это будет List вложенных маршрутов, которые будут находиться внутри данного маршрутизатора. Первый маршрут имеет пустую String для аргумента path. Это означает, что это будет первая страница, которая будет отображаться при выборе соответствующей навигационной вкладки. Путь второго объекта AutoRoute выглядит немного иначе. Синтаксис ':postId' и ':userId' используется для создания динамических сегментов. При такой настройке, если вы запустите свое приложение в браузере и введете что-то вроде "/posts/1", вы попадете на страницу поста с полем postId, равным 1. Чтобы это работало правильно, вам также необходимо аннотировать параметры конструктора postId и userId в файлах страницы. Мы сделаем это в ближайшее время.

Настройки

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

Прежде чем создать сгенерированный файл кода, давайте сделаем еще одну вещь. Как упоминалось ранее, для того чтобы динамические сегменты, определенные как ':postId' и ':userId', работали, нам нужно перейти к файлам single_post_page.dart и user_profile_page.dart и аннотировать соответствующие параметры конструктора с помощью @PathParam('optional-alias'). Если вы задаете псевдоним, он должен совпадать с именем сегмента, которое вы определили в файле router.dart. Если название вашего поля совпадает с именем сегмента, то вам не нужно задавать псевдоним. Сначала сделайте это для SinglePostPage.

const SinglePostPage({
  Key? key,
  @PathParam() required this.postId,
}) : super(key: key);

Поскольку название нашего поля postId совпадает с именем сегмента, определенным в файле router.dart, мы не стали включать псевдоним в аннотацию. Теперь вы можете сделать то же самое для страницы UserProfilePage.

const UserProfilePage({
  Key? key,
  @PathParam() required this.userId,
}) : super(key: key);

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

flutter pub run build_runner build --delete-conflicting-outputs

Мы используем флаг build, который заставит генератор запуститься только один раз. Если вместо этого вы планируете внести несколько изменений в файл router.dart, то флаг build можно заменить на watch. При использовании флага watch генератор будет запускаться каждый раз, когда вы вносите изменения.

Теперь вы должны обнаружить файл router.gr.dart в папке routes. Если его открыть, то можно увидеть, сколько строк кода нам помог написать этот полезный инструмент генерации.

Подключение маршрутизатора к приложению

Теперь, когда мы настроили маршрутизатор, его можно подключить к нашему приложению. Перейдите к файлу 

main.dart, и там нам нужно изменить несколько вещей. Сейчас AppWidget возвращает MaterialApp. Нам нужно поменять его на MaterialApp.router. Вы можете пойти дальше, удалить весь код AppWidget внутри метода build  и настроить свой файл main.dart следующим образом.

void main() => runApp(AppWidget());

class AppWidget extends StatelessWidget {
  AppWidget({Key? key}) : super(key: key);
  final _appRouter = AppRouter();
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      title: 'Bottom Nav Bar with Nested Routing',
      routerDelegate: _appRouter.delegate(),
      routeInformationParser: _appRouter.defaultRouteParser(),
    );
  }
}

Теперь давайте обсудим, что у нас получилось. Во-первых, мы инициализировали AppRouter и сохранили его в переменной _appRouter. Из-за этого нам также пришлось убрать const рядом с AppWidget в runApp и в конструкторе. AppRouter генерируется для нас с помощью AutoRoute, поэтому обязательно импортируйте сюда файл router.gr.dart. Инициализируя AutoRoute внутри корневого виджета, мы делаем этот маршрутизатор доступным для всего приложения на протяжении его жизненного цикла.

Затем мы предоставили MaterialApp.router два обязательных аргумента, специфичных для маршрутизации. Значения, которые мы предоставили для аргументов routerDelegate и routeInformaitonParser, берутся из сгенерированного объекта AppRouter.

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

Реализация нижней навигации

AutoTabsScaffold

Наконец-то мы можем приступить к реализации нижней навигационной панели приложения. К счастью, в пакете AutoRoute есть полезный виджет, который позволяет невероятно просто настроить ее. Откройте файл home_page.dart, который мы создали ранее, и замените Container в методе build на виджет AutoTabsScaffold. Этот виджет поставляется из пакета AutoRoute, поэтому не забудьте его сюда импортировать.

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AutoTabsScaffold();
  }
}

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

Если присмотреться, то можно заметить, что в отличие от PostsPage, SinglePostPage имеет свой собственный Scaffold. Это потому, что мы хотим, чтобы у PostsPage был Scaffold, определенный с помощью виджета AutoTabsScaffold. Однако в SinglePostPage и UserProfilePage мы определили отдельный Scaffold, чтобы иметь возможность указать пользовательский цвет фона.

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

...
return AutoTabsScaffold(
  appBarBuilder: (_, tabsRouter) => AppBar(
    backgroundColor: Colors.indigo,
    title: const Text('FlutterBottomNav'),
    centerTitle: true,
    leading: const AutoBackButton(),
  ),
);
...

Обратный вызов appBarBuilder дает нам доступ к context и объекту TabsRouter. Однако мы не будем использовать их для этой панели приложения. Наша панель приложения имеет пользовательский цвет фона, центрированный заголовок и кнопку AutoBackButton в качестве аргумента leading. AutoBackButton - это виджет, предоставляемый пакетом AutoRoute для удобной работы с вложенными маршрутизаторами. Вскоре мы увидим его в действии.

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

...
backgroundColor: Colors.indigo,
routes: const [
  PostsRouter(),
  UsersRouter(),
  SettingsRouter(),
],
...

Теперь можно настроить саму нижнюю навигационную панель. Для этого будем использовать аргумент bottomNavigationBuilder.

...
bottomNavigationBuilder: (_, tabsRouter) {},
...

Этот обратный вызов дает нам доступ к context и объекту TabsRouter. В данном случае нам понадобится объект TabsRouter. Вы можете использовать этот обратный вызов для возврата виджета BottomNavigationBar, который входит в состав Flutter, но также можно вернуть и пользовательскую панель навигации. Чтобы продемонстрировать это, воспользуемся пакетом Salomon Bottom Bar для создания нижней навигационной панели.

Salomon Bottom Bar

Если вы когда-либо использовали виджет BottomNavigationBar, поставляемый с Flutter, то SalomonBottomBar для вас окажется абсолютно интуитивно понятным в настройке. Сначала нам нужно импортировать пакет Salomon Bottom Bar в файл home_page.dart. Затем нужно вернуть виджет SolomonBottomBar из обратного вызова bottomNavigationBuilder. После этого необходимо указать следующие аргументы для виджета SalomonBottomBar:

  • margin: создает некоторое пространство вокруг навигационных вкладок. Это, конечно, необязательно.

  • currentIndex: индекс текущей навигационной вкладки 

  • onTap: функция, возвращающая индекс вкладки, по которой было произведено касание

  • items: List виджетов SolomonBottomBarItem, по одному виджету для каждой навигационной вкладки в нижней навигационной панели.

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

  • selectedColor

  • icon

  • title

После того как все это будет сделано, виджет SolomonBottomBar должен выглядеть так, как показано в приведенном ниже фрагменте кода.

...
return SalomonBottomBar(
  margin: const EdgeInsets.symmetric(
    horizontal: 20,
    vertical: 40,
  ),
  currentIndex: tabsRouter.activeIndex,
  onTap: tabsRouter.setActiveIndex,
  items: [
    SalomonBottomBarItem(
      selectedColor: Colors.amberAccent,
      icon: const Icon(
        Icons.post_add,
        size: 30,
      ),
      title: const Text('Posts'),
    ),
    SalomonBottomBarItem(
      selectedColor: Colors.blue[200],
      icon: const Icon(
        Icons.person,
        size: 30,
      ),
      title: const Text('Users'),
    ),
    SalomonBottomBarItem(
      selectedColor: Colors.pinkAccent[100],
      icon: const Icon(
        Icons.settings,
        size: 30,
      ),
      title: const Text('Settings'),
    ),
  ],
);
...

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

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

Переход на SinglePostPage и UserProfilePage

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

AutoRoute предоставляет множество различных методов для навигации по вашему приложению. В этом примере мы будем придерживаться простого метода push. Чтобы вызвать push или любой другой метод навигации, сначала необходимо получить маршрутизатор с заданной областью действия, вызвав AutoRouter.of(context) или context.router. Затем можно вызвать метод по вашему выбору на скопированном маршрутизаторе и передать ему нужный маршрут(ы). Перейдите к файлу posts_page.dart и настройте маршрутизацию на SinglePostRoute, вызвав метод push в аргументе onTileTap.

...
child: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    for (int i = 0; i < posts.length; i++)
      PostTile(
        tileColor: posts[i].color,
        postTitle: posts[i].title,
        onTileTap: () => context.router.push(
          SinglePostRoute(
            postId: posts[i].id,
          ),
        ),
      ),
  ],
),
...

Теперь перейдем к файлу users_page.dart и проделаем то же самое для UserProfileRoute в аргументе onAvatarTap.

...
child: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    for (int i = 0; i < users.length; i++)
      UserAvatar(
        avatarColor: users[i].color,
        username: 'user${users[i].id}',
        onAvatarTap: () => context.router.push(
          UserProfileRoute(
            userId: users[i].id,
          ),
        ),
      ),
  ],
),
...

В этом приложении мы осуществляем навигацию по маршрутам, расположенным в одном маршрутизаторе. Если необходимо перейти со страницы в одной навигационной вкладке/маршрутизаторе на страницу в другой навигационной вкладке/маршрутизаторе, то и это тоже возможно. Предположим, нужно перейти от UserProfileRoute к SinglePostRoute, для этого надо сделать следующее:

context.navigateTo(PostsRouter(children: SinglePostRoute(postId: id)).

Теперь вы можете перезапустить приложение и опробовать всю навигацию, которую мы в результате настроили. Обратите внимание, что при нажатии на плитки постов и аватары пользователей вы должны увидеть кнопку "Назад", появившуюся в Scaffold. Это потому, что ранее мы добавили виджет AutoBackButton в качестве ведущего аргумента AutoTabsScaffold.

Заключение

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


Материал подготовлен в рамках курса «Flutter Mobile Developer».

Всех желающих приглашаем на demo-занятие «Explicit анимации и 3D-графика в Flutter». На уроке рассмотрим технические детали анимации во Flutter, научимся создавать сложные составные параллельные и последовательные анимации, посмотрим основы использования двухмерных игровых движков (Flare, SpriteWidget) и создания трехмерной графики (Cube, адаптер для Unity, библиотека собственной разработки для использования WebGL в Flutter for Web-приложениях).
>> РЕГИСТРАЦИЯ

Комментарии (0)