Flutter Navigator2.0 Полное руководство и принципиальный анализ

Flutter
Flutter Navigator2.0 Полное руководство и принципиальный анализ

Эта статья была впервые опубликована в публичном аккаунте WeChat."Меанд Ни",нажмите, чтобы прочитать.

Flutter 1.22Впоследствии вы можете обнаружить, что официальные изменения в API-интерфейсах, связанных с маршрутизацией, очень велики.Это не дало разработчикам гибкий способ управлять прямым стеком маршрутизации, даже кажется устаревшим, совсем не Flutter.

As mentioned by a participant in one of Flutter's user studies, the API also feels outdated and not very Flutter-y.

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

juejin.im_post_6880388303745449991

В этой статье я познакомлю читателей с основными принципами Navigator 2.0 и помогу вам найти лучший способ его использования.

Зачем нужны новые API

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

Параметр initialRoute в исходном API, то есть начальная страница системы по умолчанию, не может быть изменен после запуска приложения.. В этом случае, если пользователь получит системное уведомление и нажмет кнопку[Main -> Profile -> Settings]переключиться на новый[Main -> List -> Detail[id=24], в Navigator1.0 нет элегантного способа добиться этого эффекта.

Первоначальный императивный API-интерфейс Navigator предоставляет разработчикам только некоторые очень целевые интерфейсы, такие как push, pop и т. д., но не предоставляет нам более гибкого способа прямого управления стеком маршрутизации.. Это также то, о чем я упоминал в своей прошлой статье.Этот подход на самом деле противоречит концепции Flutter.Представьте, если мы хотим изменить все подкомпоненты виджета, нам нужно только перестроить все подкомпоненты и создать серию новых виджетов. и применить эту концепцию к маршрутизации... en? Когда в приложении есть ряд страниц маршрутизации, которые нужно изменить, мы можем только вызывать push-, pop- и другие интерфейсы туда и обратно,Флаттер не имеет вкуса.

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

Итак, Navigator2.0 появился как американские горки~

Navigator2.0

Недавно добавленный декларативный API Navigator2.0 в основном состоит из двух частей: Page API и Router API. Их соответствующие мощные функции обеспечивают прочный краеугольный камень для Navigator2.0. В этом разделе я познакомлю читателей с соответствующими деталями реализации.

Page

Page — один из самых распространенных классов в Navigator 2.0. Из названия можно понять, что его значение — «страница», как и виджет.компонентыТо же самое, но отношения между страницей и виджетом также тонкие.

а такжеТри дерева во Флаттереконцепция остается прежней. Виджет сохраняет только информацию о конфигурации компонента, а слой фреймворка имеет встроенныйcreateElement()Можно создать соответствующий экземпляр элемента. Страница также сохраняет только информацию, связанную с маршрутизацией страницы, а такжеcreateRoute()для создания соответствующего экземпляра Route.

Виджет и Страница также имеют одинcanUpdate()метод, помогающий Flutter определить, был ли он обновлен или изменен:

// Page
bool canUpdate(Page<dynamic> other) {
  return other.runtimeType == runtimeType &&
         other.key == key;
}

// Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

Даже условия для сравненияТип среды выполнения и ключ.

На уровне кода Page наследуется от RouteSettings, который мы использовали ранее:

abstract class Page<T> extends RouteSettings

Среди них сохраняется информация, включающая имя маршрута (имя, например, «/settings»), параметры (аргументы) и т. д.

pages

Далее давайте рассмотрим сценарии использования Page. В новом компоненте Navigator примитеpagesПараметр, который принимает список объектов Page, выглядит следующим образом:

class _MyAppState extends State<MyApp> {
  final pages = [
    MyPage(
      key: Key('/'),
      name: '/',
      builder: (context) => HomeScreen(),
    ),
    MyPage(
      key: Key('/category/5'),
      name: '/category/5',
      builder: (context) => CategoryScreen(id: 5),
    ),
    MyPage(
      key: Key('/item/15'),
      name: '/item/15',
      builder: (context) => ItemScreen(id: 15),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return //...
      Navigator(
          key: _navigatorKey,
          pages: List.of(pages),
        ),
  }
}

В этот момент запустите приложение,Flutter сгенерирует соответствующий экземпляр Route из всех объектов Page на страницах в базовом стеке маршрутизации., то есть три страницы, соответствующие страницам .

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

void addPage(MyPage page) {
  setState(() => pages.add(page));
}

Компонент Navigator также добавляет новыйonPopPageПараметр принимает функцию обратного вызова для ответа на всплывающее событие страницы, например _onPopPage в следующем коде:

class _MyAppState extends State<MyApp> {

  bool _onPopPage(Route<dynamic> route, dynamic result) {
    setState(() => pages.remove(route.settings));
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    print('build: $pages');
    return // ...
      Navigator(
        key: _navigatorKey,
        onPopPage: _onPopPage,
        pages: List.of(pages),
      )
  }
}

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

существует_onPopPage, если договоримся закрыть страницу, звонитеroute.didPop(result), функция по умолчанию возвращает true.

Итак, возникает проблема: что, если мы получим уведомление, но не обновим страницы, чтобы удалить соответствующий объект Page, следующий код:

bool _onPopPage(Route<dynamic> route, dynamic result) {
  // setState(() => pages.remove(route.settings));
  return route.didPop(result);
}

В настоящее время,route.didPop(result)Когда функция срабатывает, Flutter сравнитНижний слой закрыл стек маршрутизации страницысодержание истраницы, хранящиеся в данный момент в навигаторе, если обнаружено несоответствие, дополнительная страница будет рассматриваться как новая страница в соответствии с существующими страницами, и будет создан объект маршрута, чтобы содержимое в базовом стеке маршрутизации могло быть согласовано с данными страниц верхнего уровня. в любое время.

То есть,Способна ли страница полностью перевернуться нами, а не просто передать системеNavigator.pop(), здесь, если мы не хотим закрывать страницу, просто верните false напрямую:

bool _onPopPage(Route<dynamic> route, dynamic result) {
  if (...) {
    return false;
  }
  setState(() => pages.remove(route.settings));
  return route.didPop(result);
}

Следует отметить, что onPopPage реагирует только на запуск верхней страницы стека маршрутизации, а удаление средней страницы не вызовет эту callback-функцию.

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

Чтобы запустить полный вариант выше, см. полный код:GitHub.com/mean NI/Приложение…

Платформа Flutter имеет две предварительно созданные страницы, MaterialPage и CupertinoPage, которые представляют страницы в стилях Material и Cupertino соответственно, которые повторяют MaterialPageRoute и CupertinoPageRoute в Navigator 1.0, обе из которых принимают дочерний компонент для представления содержимого, которое будет представлено на странице. . Например, в следующем примере мы можем использовать MaterialPage для создания страниц непосредственно на страницах:

List<Page> pages = <Page>[
  MaterialPage(
    key: ValueKey('VeggiesListPage'),
    child: VeggiesListScreen(
      veggies: veggies,
      onTapped: _handleVeggieTapped,
    ),
  ),
  if (show404)
    MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
  else
    if (_selectedVeggie != null)
      VeggieDetailsPage(veggie: _selectedVeggie)
];

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

class MyPage extends Page {
  final Veggie veggie;

  MyPage({
    this.veggie,
  }) : super(key: ValueKey(veggie));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        return VeggieDetailsScreen(veggie: veggie);
      },
    );
  }
}

Здесь мы переписываемcreateRoute()Просто верните объект MaterialPageRoute.

Router

Маршрутизатор — еще один очень важный компонент, добавленный в Navigator2.0, он наследуется от StatefulWidget для управления своим состоянием (естьofфункция в сочетании с InheritedWidget для достижения единого управления состоянием,Реклама 😁, глава 9 грядущей книги "Путешествие разработки Flutter с юга на север").

Состояние, которым он управляет, — это приложениесостояние маршрутизации, В сочетании с концепцией страницы, упомянутой в предыдущем разделе, мы можем рассматривать страницы как состояние маршрутизации здесь.Когда мы изменяем содержимое/состояние страниц, маршрутизатор распределяет состояние дочерним компонентам, а изменение состояния приведет к тому, что дочерний компонент перестроения применяет последнее состояние. (Глава 9 требует, чтобы читатели внимательно читали, охватывая точки знаний об управлении состоянием на протяжении всего проектирования уровня инфраструктуры Flutter).

Поэтому, когда Navigator используется как подкомпонент Router, он, естественно, будет иметь возможность воспринимать изменения в статусе маршрутизации, как показано на следующем рисунке:

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

void _pushPage() {
  MyRouteDelegate.of(context).push('Route$_counter');
}

Суть декларативного API, подчеркнутого в Navigator2.0, заключается в том, что мы работаем с маршрутизацией не в режиме push или pop, а в изменении состояния приложения! Нам нужно концептуально понять, чем декларативные API отличаются от прошлых.

Прокси-сервер маршрутизатора

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

После Navigator 2.0 Flutter также предоставляет новый конструктор MaterialApp, router, который помогает нам неявно создавать глобальный компонент Router, который используется следующим образом:

MaterialApp.router(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
    visualDensity: VisualDensity.adaptivePlatformDensity,
  ),
  routeInformationParser: MyRouteParser(),
  routerDelegate: delegate,
)

Конструктор принимаетrouterDelegateПараметры здесь мы можем передать в объекте MyRouteDelegate, который мы создали сами, конкретный код выглядит следующим образом:

class MyRouteDelegate extends RouterDelegate<String>
    with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
  final _stack = <String>[];

  static MyRouteDelegate of(BuildContext context) {
    final delegate = Router.of(context).routerDelegate;
    assert(delegate is MyRouteDelegate, 'Delegate type must match');
    return delegate as MyRouteDelegate;
  }

  MyRouteDelegate({
    @required this.onGenerateRoute,
  });

  // ...
  @override
  Widget build(BuildContext context) {
    print('${describeIdentity(this)}.stack: $_stack');
    return Navigator(
      key: navigatorKey,
      onPopPage: _onPopPage,
      pages: [
        for (final name in _stack)
            MyPage(
              key: ValueKey(name),
              name: name,
              routeFactory: onGenerateRoute,
            ),
      ],
    );
  }
}

Вышеупомянутый MyRouteDelegate наследуется от RouterDelegate. Он может реализовать четыре внутренних метода setInitialRoutePath, setNewRoutePath, build и currentConfiguration getter, а также смешивается с классом PopNavigatorRouterDelegateMixin. Его основная функция — реагировать на кнопку «Назад» устройства Android, а функция ChangeNotifier должен сделать уведомление о событии, ниже "Реализация RouterDelegate.Разберем роль каждого из этих методов.

Здесь давайте сначала рассмотрим метод MyRouteDelegate.build.Как и в предыдущем разделе, мы можем создать компонент Navigator для возврата, передав параметры pages и onPopPage, чтобы при передаче объекта MyRouteDelegate вMaterialApp.router()После конструктора Навигатор здесь успешно стал подкомпонентом Маршрутизатора.

В большинстве случаев это может сделать настраиваемый прокси-сервер маршрутизации.

События маршрутизатора

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

  • Начальный маршрут, запрошенный системой при первом запуске приложения.

  • Прослушивайте новые намерения от системы, т.е. открывайте новую страницу маршрутизации.

  • Прослушивающее устройство отступает и закрывает верхний маршрут в стеке маршрутизации.

Чтобы полностью реагировать на эти события, маршрутизатор должен быть настроен с делегатом RouteNameProvider и делегатом BackButtonDispatcher.

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

Аналогичным образом, когда пользователь нажимает кнопку «Назад» на устройстве, событие передается делегату BackButtonDispatcher.

Наконец, данные объекта, проанализированные RouteNameParser, и резервное событие BackButtonDispatcher Delegate будут перенаправлены в RouteDelegate, указанный выше.После получения этих событий RouteDelegate ответит и выполнит изменение состояния ответа, что приведет к реконструкции компонента Navigator. содержащий страницы. Представляет последнее состояние маршрутизации.

Весь процесс можно представить следующей схемой:

Что вам нужно знать, так это то, что и RouteNameProvider Delegate, и BackButtonDispatcher Delegate имеют встроенные реализации по умолчанию во Flutter. Поэтому в большинстве случаев нам не нужно рассматривать детали. В настоящее время тип T по умолчанию равен RouteSetting (в соответствии с Navogator1.0, включая информацию о маршрутизации).

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

Реализовать RouterDelegate

Как мы сказали выше, Flutter предоставляет реализации по умолчанию как для делегата RouteNameProvider, так и для делегата BackButtonDispatcher, в то время как RouterDelegate должен быть реализован вручную и передан вMaterialApp.router()Конструктор для работы.

Здесь мы можем выполнять различные операции, связанные с бизнесом.Сам RouteDelegate реализован из Listenable, который может отслеживать объекты или называться наблюдателями.Всякий раз, когда состояние изменяется, наблюдатели могут уведомить его, чтобы отреагировать на событие, тем самым инициируя перестроение компонента Navigator, обновление состояние маршрутизации.

События маршрутизации в RouterDelegate в основном принимаются следующими функциями:

  • Когда backButtonDispatcher генерирует событие кнопки «Назад», RouterDelegate'spopRouteМетод, реализованный миксином PopNavigatorRouterDelegateMixin.
  • RouterDelegate'ssetInitialRoutePathметод, который принимает имя маршрута. По умолчанию этот метод будет напрямую вызывать функцию setNewRoutePath класса RouterDelegate.
  • Когда система routeNameProvider начинает открывать уведомление о новой странице маршрута, она вызывается напрямуюsetNewRoutePathметод, параметр является результатом синтаксического анализа с помощью routeNameParser.

Поэтому мы наконец можем реализовать RouterDelegate следующим образом:

class MyRouteDelegate extends RouterDelegate<String>
    with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
  final _stack = <String>[];

  static MyRouteDelegate of(BuildContext context) {
    final delegate = Router.of(context).routerDelegate;
    assert(delegate is MyRouteDelegate, 'Delegate type must match');
    return delegate as MyRouteDelegate;
  }

  MyRouteDelegate({
    @required this.onGenerateRoute,
  });

  final RouteFactory onGenerateRoute;

  @override
  GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  String get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;

  List<String> get stack => List.unmodifiable(_stack);

  void push(String newRoute) {
    _stack.add(newRoute);
    notifyListeners();
  }

  void pop() {
    if (_stack.isNotEmpty) {
      _stack.remove(_stack.last);
    }
    notifyListeners();
  }

  @override
  Future<void> setInitialRoutePath(String configuration) {
    return setNewRoutePath(configuration);
  }

  @override
  Future<void> setNewRoutePath(String configuration) {
    print('setNewRoutePath $configuration');
    _stack
      ..clear()
      ..add(configuration);
    return SynchronousFuture<void>(null);
  }

  bool _onPopPage(Route<dynamic> route, dynamic result) {
    if (_stack.isNotEmpty) {
      if (_stack.last == route.settings.name) {
        _stack.remove(route.settings.name);
        notifyListeners();
      }
    }
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    print('${describeIdentity(this)}.stack: $_stack');
    return Navigator(
      key: navigatorKey,
      onPopPage: _onPopPage,
      pages: [
        for (final name in _stack)
            MyPage(
              key: ValueKey(name),
              name: name,
            ),
      ],
    );
  }
}

Здесь _stack представляет собой набор данных, каждые данные будут создавать MyPage в функции сборки, которая по умолчанию пуста. Когда приложение запустится, оно сначала позвонит сюдаsetInitialRoutePath(String configuration)метод, параметр '/', то в стеке маршрутизации будет домашняя страница.

Полный код см.GitHub.com/mean NI/Приложение…

В дочерних компонентах мы также можем использовать MyRouteDelegate для открытия или закрытия страницы следующим образом:

MyRouteDelegate.of(context).push('Route$_counter');

MyRouteDelegate.of(context).pop();

Это то же самое, что и наследуемый компонент.В MyRouteDelegate наши пользовательские методы push и pop работают с объявленным стеком маршрутизации и, наконец, уведомляют об обновлении статуса маршрутизации.

Реализовать RouteInformationParser

MaterialApp.routerПомимо принятия необходимого параметра routerDelegate прокси-сервера маршрутизации, вам также необходимо указать параметр routeInformationParser следующим образом:

MaterialApp.router(
  title: 'Flutter Demo',
  routeInformationParser: MyRouteParser(), 	// 传入 MyRouteParser
  routerDelegate: delegate,
)

Этот параметр получает объект RouteInformationParser, и определение этого класса обычно имеет самую простую и прямую реализацию, как показано ниже:

class MyRouteParser extends RouteInformationParser<String> {
  @override
  Future<String> parseRouteInformation(RouteInformation routeInformation) {
    return SynchronousFuture(routeInformation.location);
  }

  @override
  RouteInformation restoreRouteInformation(String configuration) {
    return RouteInformation(location: configuration);
  }
}

Здесь MyRouteParser наследуется от RouteInformationParser и переопределяетparseRouteInformation()а такжеrestoreRouteInformation()два метода.

Как указано выше,parseRouteInformation()Функция метода принимает маршрутную информацию routeInformation, переданную нам системой после парсинга, возвращает и перенаправляет ее в routerDelegate, который мы определили ранее, а разбираемый тип — это универсальный тип RouteInformationParser, то есть String здесь. То есть в routerDelegate нижеsetNewRoutePath()Конфигурация параметров метода пересылается отсюда:

@override
Future<void> setNewRoutePath(String configuration) {
  print('setNewRoutePath $configuration');
  _stack
    ..clear()
    ..add(configuration);
  return SynchronousFuture<void>(null);
}

restoreRouteInformation()Метод возвращает объект RouteInformation, представляющий восстановление информации о маршрутизации из переданной конфигурации. Отражает parseRouteInformation.

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

Вышеупомянутая реализация MyRouteParser является самой простой реализацией, и функция состоит в том, чтобыparseRouteInformation()принимает базовую RouteInformation,restoreRouteInformation()Восстановить верхнюю КОНФИГУРАЦИЮ.

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

import 'package:flutter/material.dart';
import 'package:flutter_navigator_v2/navigator_v2/model.dart';

class VeggieRouteInformationParser extends RouteInformationParser<VeggieRoutePath> {
  @override
  Future<VeggieRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    print("parseRouteInformation");
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return VeggieRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'veggie') return VeggieRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return VeggieRoutePath.unknown();
      return VeggieRoutePath.details(id);
    }

    // Handle unknown routes
    return VeggieRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(VeggieRoutePath path) {
    print("restoreRouteInformation");
    if (path.isUnknown) {
      return RouteInformation(location: '/404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/veggie/${path.id}');
    }
    return null;
  }
}

Универсальный тип RouteInformationParser, унаследованный VeggieRouteInformationParser, здесь обозначен как наш пользовательский VeggieRoutePath. В Navigator2.0 мы называем эту проанализированную форму какмодель маршрутизации.

В этот момент выделена роль VeggieRouteInformationParser, она находится вparseRouteInformation()После принятия системной информации RouteInformation в методе ее можно преобразовать в объект модели VeggieRoutePath, знакомый нашему верхнему уровню. Содержимое класса VeggieRoutePath выглядит следующим образом:

class VeggieRoutePath {
  final int id;
  final bool isUnknown;

  VeggieRoutePath.home()
      : id = null,
        isUnknown = false;

  VeggieRoutePath.details(this.id) : isUnknown = false;

  VeggieRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool get isDetailsPage => id != null;
}

В настоящее время,RouterDelegate<VeggieRoutePath>, мы можем обновить состояние маршрутизации в соответствии с объектом.

Лучшие практики

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

Весь приведенный выше код содержит три случая, а именно:

  • pages_example.dart, Navigator + Page реализуют управление состоянием маршрутизации.
  • router_example.dart, Router + Navigator + Page для достижения унифицированного управления статусом маршрутизации
  • Рекомендации по списку фруктов, относительно полный случай, включая пользовательскую модель RouteInformationParser и операции управления состоянием маршрутизации.

Адрес источника:GitHub.com/mean NI/Приложение…

Предпродажа новой книги 🔥

image-20201108214042932

Наконец-то я могу объявить о своей новой книге«Путешествие развития Fluter с юга на север»В асинхронном сообществе наконец стартовала предпродажа! Он охватывает различные дополнительные знания о Flutter, в том числеПринцип трех деревьев, ограничения компоновки, самоотрисовывающиеся компоненты, управление состояниемПодождите, вся книга поставляется с открытым исходным кодом:GitHub.com/mean NI/Приложение…, после официального релиза будет специальное введение в статью, студенты, которые в ней нуждаются, могут сначала обратить внимание 😊 Надеюсь внести свой вклад в сообщество Flutter.

Адрес предпродажи:item.JD.com/10024203424…, он будет выпущен одновременно в крупных книжных магазинах позже, все желающие могут обратить внимание на паблик-аккаунт»MeandNi», обратите внимание на следующеезаказать доставкуИ последние качественные технические статьи по Flutter.