Flutter Navigator2.0的原理和Web端实践

news2024/12/16 13:47:01

01

背景与动机

Navigator 2.0推出之前,Flutter主要通过Navigator 1.0和其提供的 API(如push()pop()pushNamed()等)来管理页面路由。然而,Navigator 1.0存在一些局限性,如难以实现复杂的页面操作(如移除栈内中间页面、交换页面等)、不支持嵌套路由以及无法满足全平台(尤其是Web平台)的新需求。因此,Flutter官方团队决定对路由系统进行改造,推出了Navigator 2.0。 

02

主要特性

  • 声明式API Navigator 2.0提供的声明式API使得路由管理更加直观和易于理解。开发者只需声明页面的配置信息,而无需编写复杂的导航逻辑代码。这种方式不仅减少了代码量,还提高了代码的可读性和可维护性。

  • 嵌套路由 Navigator 2.0满足了嵌套路由的需求场景,允许开发者在应用中创建嵌套的路由结构。这使得应用的结构更加清晰,同时也提高了页面导航的灵活性。

  • 全平台支持 Navigator 2.0提供的API能够满足不同平台(如iOSAndroidWeb等)的导航需求,使得开发者能够更加方便地构建跨平台的应用。

  • 强大的页面操作能力 Navigator 2.0提供了更加丰富的页面操作能力,如移除栈内中间页面、交换页面等。这些操作在Navigator 1.0中很难实现或需要编写复杂的代码,而在Navigator 2.0中则变得简单直接。

03

核心组件

  • Router 在Navigator 2.0中,Router组件是路由管理的核心。它负责根据当前的路由信息(RouteInformation)和路由信息解析器(RouteInformationParser)来构建和更新UIRouter组件接收三个主要参数:

    1.routeInformationProvider:提供当前的路由信息;

    2.routeInformationParser:将路由信息解析为路由配置;

    3.routerDelegate:根据路由配置构建和更新UI

  • RouteInformationProvider RouteInformationProvider是一个提供当前路由信息的组件。它通常与平台相关的路由信息源(如浏览器的URLAndroidIntent等)集成,以获取当前的路由信息。

  • RouteInformationParser RouteInformationParser负责将RouteInformation解析为RouteConfiguration。这个过程允许开发者根据路由信息的格式(如URL)来定义如何将其映射到应用内的路由配置。

  • RouterDelegate RouterDelegate是与UI构建紧密相关的组件。它必须实现RouterDelegate接口,并提供两个主要方法: 

    1.build(BuildContext context):根据当前的路由配置构建UI

    2.setNewRoutePath(List configuration):设置新的路由路径,并更新UI

    3.Future popRoute() :实现后退逻辑。

04

简单实例

首先通过MaterialApp.router()来创建MaterialApp:

class MyApp extends StatelessWidget {  
  @override  
  Widget build(BuildContext context) {  
    final routerDelegate = MyRouterDelegate();  
    final routeInformationParser = MyRouteInformationParser();  
  
    return MaterialApp.router(  
      title: 'Flutter Navigator 2.0 Demo',  
      theme: ThemeData(  
        primarySwatch: Colors.blue,  
      ),  
      routerDelegate: routerDelegate,  
      routeInformationParser: routeInformationParser,  
    );  
  }  
}

需要定义一个RouterDelegate对象和一个RouteInformationParser对象。其中根据路由配置构建和更新UIRouteInformationParser负责将RouteInformation解析为RouteConfiguration。 RouterDelegate可以传个泛型,定义其currentConfiguration对象的类型。

class MyRouterDelegate extends RouterDelegate<String>  
  with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {  
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();  
  private List<String> _pages = ['/home'];  
  
  @override  
  Widget build(BuildContext context) {  
    return Navigator(  
      key: navigatorKey,  
      pages: _pages.map((route) => MaterialPage(  
        key: Key(route),  
        child: generatePage(route),  
      )).toList(),  
      onPopPage: (route, result) {  
        if (!route.didPop(result)) {  
          return false;  
        }  
  
        _pages.removeLast();  
        notifyListeners();  
        return true;  
      },  
    );  
  }  
  
  @override  
  Future<void> setNewRoutePath(String path) async {  
    if (!_pages.contains(path)) {  
      _pages.add(path);  
      notifyListeners();  
    }  
  }  
  
  Widget generatePage(String route) {  
    switch (route) {  
      case '/home':  
        return HomePage();  
      case '/details':  
        // 这里可以传递参数,例如 DetailsPage(arguments: someData)  
        return DetailsPage();  
      default:  
        return NotFoundPage();  
    }  
  }  
  
  @override  
  String get currentConfiguration => _pages.last;  
}

其中build()一般返回的是一个Navigator对象,popRoute()实现后退逻辑,setNewRoutePath()实现新页面的逻辑。定义了一个_pages数组对象,记录每个路由的path,可以理解为是一个路由栈,这个路由栈对我们来说非常友好,在有复杂的业务逻辑时,我们可以自行定义相应的栈管理逻辑。currentConfiguration返回的是栈顶的page信息。创建一个类继承RouteInformationParser,主要的作用是包装解析路由信息,这里有一个最简单的方式,如下:

class MyRouteInformationParser extends RouteInformationParser<String> {  
  @override  
  Future<String> parseRouteInformation(RouteInformation routeInformation) {  
    final uri = Uri.parse(routeInformation.location);  
    return SynchronousFuture(uri.path);  
  }  
  
  @override  
  RouteInformation restoreRouteInformation(String configuration) {  
    return RouteInformation(location: configuration);  
  }  
}

好的,接下来我们看一下调用:

class HomePage extends StatelessWidget {  
  @override  
  Widget build(BuildContext context) {  
    return Scaffold(  
      appBar: AppBar(title: Text('Home')),  
      body: Center(  
        child: ElevatedButton(  
          onPressed: () {  
            Router.of(context).routerDelegate.setNewRoutePath("/details");
          },  
          child: Text('Go to Details'),  
        ),  
      ),  
    );  
  }  
}  
  
class DetailsPage extends StatelessWidget {  
  @override  
  Widget build(BuildContext context) {  
    return Scaffold(  
      appBar: AppBar(title: Text('Details')),  
      body: Center(  
        child: Text('This is Details Page'),  
      ),  
    );  
  }  
} 

class NotFoundPage extends StatelessWidget {  
  @override  
  Widget build(BuildContext context) {  
    return Scaffold(  
      appBar: AppBar(title: Text('Not Found')),  
      body: Center(  
        child: Text('Page not found'),  
      ),  
    );  
  }  
}

非常简单,直接调用Router.of(context).routerDelegate.setNewRoutePath()即可。

到此为止,一个使用Navigator2.0的最简单的路由实例就完成了。和Navigator1.0相比,看上去繁杂了不少。但是可以根据业务需求自定义路由栈进行管理,大大的提升了灵活性。接来看我们看一下Navigator2.0是如何对路由进行实现的。

05

源码简析

我们在使用Navigator2.0时,是通过MaterialApp.router()创建的MaterialApp对象,之前章节提到过,传了RouteInformationParserRouterDelegate这两个对象。当传递了RouterDelegate对象时,_MaterialAppState中的_usesRouter会被设置为true

bool get _usesRouter => widget.routerDelegate != null || widget.routerConfig != null;

build()时,通过WidgetsApp.router()方法创建了一个WidgetsApp对象:

if (_usesRouter) {
      return WidgetsApp.router(
        key: GlobalObjectKey(this),
        routeInformationProvider: widget.routeInformationProvider,
        routeInformationParser: widget.routeInformationParser,
        routerDelegate: widget.routerDelegate,
        routerConfig: widget.routerConfig,
        backButtonDispatcher: widget.backButtonDispatcher,
        builder: _materialBuilder,
        title: widget.title,
        onGenerateTitle: widget.onGenerateTitle,
        textStyle: _errorTextStyle,
        color: materialColor,
        locale: widget.locale,
        localizationsDelegates: _localizationsDelegates,
        localeResolutionCallback: widget.localeResolutionCallback,
        localeListResolutionCallback: widget.localeListResolutionCallback,
        supportedLocales: widget.supportedLocales,
        showPerformanceOverlay: widget.showPerformanceOverlay,
        checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
        checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
        showSemanticsDebugger: widget.showSemanticsDebugger,
        debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
        inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
        shortcuts: widget.shortcuts,
        actions: widget.actions,
        restorationScopeId: widget.restorationScopeId,
      );
    }

_WidgetsAppState中根据routerDelegate设置了成员变量_usesRouterWithDelegates的值:

bool get _usesRouterWithDelegates => widget.routerDelegate != null;

build()时会创建一个Router对象,其中Router继承了StatefulWidget

@override
  Widget build(BuildContext context) {
    Widget? routing;
    if (_usesRouterWithDelegates) {
      routing = Router<Object>(
        restorationScopeId: 'router',
        routeInformationProvider: _effectiveRouteInformationProvider,
        routeInformationParser: widget.routeInformationParser,
        routerDelegate: widget.routerDelegate!,
        backButtonDispatcher: _effectiveBackButtonDispatcher,
      );
    } 
......
  }

在上一章节的实例中我们可得知,页面的切换都是依靠RouterDelegate对象进行的。每当切换到新的页面时,都会调用setNewRoutePath()方法,因此我们来看一下setNewRoutePath()是什么时候被调用的,有两处。第一处:

void _handleRouteInformationProviderNotification() {
    _routeParsePending = true;
    _processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
  }
_RouteSetter<T> _processParsedRouteInformation(Object? transaction, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
    return (T data) async {
      if (_currentRouterTransaction != transaction) {
        return;
      }
      await delegateRouteSetter()(data);
      if (_currentRouterTransaction == transaction) {
        _rebuild();
      }
    };
  }

我们看看_handleRouteInformationProviderNotification的调用时机:

@override
  void initState() {
    super.initState();
    widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
    widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
    widget.routerDelegate.addListener(_handleRouterDelegateNotification);
  }

我们可以看到在initState()时,也就是在Router被初始化的时候由widget.routeInformationProvider来监听一些状态实现新页面的切换。我们来看一下routeInformationProviderRouteInformationProvider在我们自己没有创建的情况下,系统会默认为我们创建一个PlatformRouteInformationProvider对象。它实际上是个ChangeNotifier。系统会监听每一帧的信号发送,调用其父类routerReportsNewRouteInformation()方法,我们看看它的实现:

@override
  void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) {
    final bool replace =
      type == RouteInformationReportingType.neglect ||
      (type == RouteInformationReportingType.none &&
      _equals(_valueInEngine.uri, routeInformation.uri));
    SystemNavigator.selectMultiEntryHistory();
    SystemNavigator.routeInformationUpdated(
      uri: routeInformation.uri,
      state: routeInformation.state,
      replace: replace,
    );
    _value = routeInformation;
    _valueInEngine = routeInformation;
  }

其中SystemNavigator.selectMultiEntryHistory()的实现如下:

/// Selects the multiple-entry history mode.
  ///
  /// On web, this switches the browser history model to one that tracks all
  /// updates to [routeInformationUpdated] to form a history stack. This is the
  /// default.
  ///
  /// Currently, this is ignored on other platforms.
  ///
  /// See also:
  ///
  ///  * [selectSingleEntryHistory], which forces the history to only have one
  ///    entry.
  static Future<void> selectMultiEntryHistory() {
    return SystemChannels.navigation.invokeMethod<void>('selectMultiEntryHistory');
  }

这个方法是由各个平台自行实现的。从注释中我们可得知如果是在Web平台下,它会切换成history模式,并从history stack中追踪所有的变化。在history发生变化时,会发送信号给Flutter层等待处理。SystemNavigator.routeInformationUpdated()方法是用来更新路由的,我们先不做分析。接着我们回到PlatformRouteInformationProvider,看看它什么时候会执行notifyListeners()方法:

@override
  Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
    assert(hasListeners);
    _platformReportsNewRouteInformation(routeInformation);
    return true;
  }
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
    if (_value == routeInformation) {
      return;
    }
    _value = routeInformation;
    _valueInEngine = routeInformation;
    notifyListeners();
  }

在监听到有push路由的情况下时,会调用notifyListeners(),从而实现页面的切换。我们再来看第二处调用setNewRoutePath()的地方:

@override
  void didChangeDependencies() {
    _routeParsePending = true;
    super.didChangeDependencies();
    // The super.didChangeDependencies may have parsed the route information.
    // This can happen if the didChangeDependencies is triggered by state
    // restoration or first build.
    if (widget.routeInformationProvider != null && _routeParsePending) {
      _processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
    }
    _routeParsePending = false;
    _maybeNeedToReportRouteInformation();
  }
void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
    assert(_routeParsePending);
    _routeParsePending = false;
    _currentRouterTransaction = Object();
    widget.routeInformationParser!
      .parseRouteInformationWithDependencies(information, context)
      .then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
  }

parseRouteInformationWithDependencies()方法中调用的parseRouteInformation()其实就是我们自定义RouteInformationParser来进行的实现。

Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
    return parseRouteInformation(routeInformation);
  }

看到当其与父的依赖关系被改变的时候会调用setNewRoutePath()。大概率就是App初始化的时候被调用一次。

06

根据狐友业务的Web端实践 

我们的Flutter团队会承担一些运营活动的H5需求。在实现时我们对路由有如下需求:

1.可以根据业务自由的管理路由栈;

2.分享链接只能分享出去默认入口链接,不希望中间的路由链接被分享出去;

3.不管有多少个路由页面,history始终不变,在响应浏览器返回键时不响应路由栈的pop操作。

在之前使用Navigator1.0时体验并不太好,一个是不够灵活,另外还需对分享出去的链接做处理。因此我们利用Navigator2.0设计了一套新的路由:

MyRouterDelegate delegate = MyRouterDelegate();

  @override
  Widget build(BuildContext context) {
     return MaterialApp.router(
        debugShowCheckedModeBanner: false,
        routeInformationParser: MyRouteParser(),
        routerDelegate: delegate,
      );
  }

Parser实现非常简单:

class MyRouteParser extends RouteInformationParser<RouteSettings> {
  @override
  ///parseRouteInformation() 方法的作用就是接受系统传递给我们的路由信息 routeInformation
  Future<RouteSettings> parseRouteInformation(
      RouteInformation routeInformation) {
    // Uri uri = Uri.parse(routeInformation.location??"/");
    return SynchronousFuture(RouteSettings(name: routeInformation.location));
  }

  @override
  ///恢复路由信息
  RouteInformation restoreRouteInformation(RouteSettings configuration) {
    return RouteInformation(location: configuration.name);
  }
}

Delegate的实现如下:

import 'package:ai_chatchallenge/router/exit_util.dart';
import 'package:ai_chatchallenge/router/navigator_util.dart';
import 'package:ai_chatchallenge/router/my_router_arg.dart';
import 'package:flutter/material.dart';

import 'route_page_config.dart';

class MyRouterDelegate extends RouterDelegate<RouteSettings>
    with PopNavigatorRouterDelegateMixin<RouteSettings>, ChangeNotifier {
  ///页面栈
  List<Page> _stack = [];

  //当前的界面信息
  RouteSettings _setting = RouteSettings(
      name: RouterName.rootPage,
      arguments: BaseArgument()..name = RouterName.rootPage);

  //重写navigatorKey
  @override
  GlobalKey<NavigatorState> navigatorKey;

  MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
    //初始化两个方法 一个是push页面 另一个是替换页面

    NavigatorUtil()
        .registerRouteJump(RouteJumpFunction(onJumpTo: (RouteSettings setting) {
      // _setting = setting;
      // changePage();
      addPage(name: setting.name, arguments: setting.arguments);
    }, onReplaceAndJumpTo: (RouteSettings setting) {
      if (_stack.isNotEmpty) {
        _stack.removeLast();
      }
      _setting = setting;
      changePage();
    }, onClearStack: () {
      _stack.clear();
      _setting = RouteSettings(
          name: RouterName.rootPage,
          arguments: BaseArgument()..name = RouterName.rootPage);
      changePage();
    }, onBack: () {
      if (_stack.isNotEmpty) {
        _stack.removeLast();
        if (_stack.isNotEmpty) {
          _setting = _stack.last;
        } else {
          _setting = RouteSettings(
              name: RouterName.rootPage,
              arguments: BaseArgument()..name = RouterName.rootPage);
        }

        changePage();
      }
    }));
  }

  @override
  RouteSettings? get currentConfiguration {
    return _stack.last;
  }

  @override
  Future<bool> popRoute() {
    if (_stack.length > 1) {
      _stack.removeLast();
      _setting = _stack.last;
      changePage();
      //非最后一个页面
      return Future.value(true);
    }
    //最后一个页面确认退出操作
    return _confirmExit();
  }

  Future<bool> _confirmExit() async {
    bool result = ExitUtil.doubleCheckExit(navigatorKey.currentContext!);
    // bool result = await ExitUtil.backToDesktop();
    return !result;
  }

  void addPage({required name, arguments}) {
    _setting = RouteSettings(name: name, arguments: arguments);
    changePage();
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      //解决物理返回建无效的问题
      onWillPop: () async => !await navigatorKey.currentState!.maybePop(),
      child: Navigator(
        key: navigatorKey,
        pages: _stack,
        onPopPage: _onPopPage,
      ),
    );
  }

  /// 按下返回的回调
  bool _onPopPage(Route<dynamic> route, dynamic result) {
    debugPrint("这里的试试");
    if (!route.didPop(result)) {
      return false;
    }
    return true;
  }

  changePage() {
    int index = getCurrentIndex(_stack, _setting!);
    List<Page> tempPages = _stack;

    if (index != -1) {
      // 要求栈中只允许有一个同样的页面的实例 否则开发模式热更新会报错
      // 要打开的页面在栈中已存在,则将该页面和它上面的所有页面进行出栈
      tempPages = tempPages.sublist(0, index);
      // 或者删除之前存在栈里的页面,重新创建
      // tempPages.removeAt(index);
    }
    Page page;

    if (_setting?.arguments is BaseArgument) {
      if ((_setting?.arguments as BaseArgument).name == RouterName.rootPage) {
        _stack.clear();
      }
    } else {
      if (_setting?.name == RouterName.rootPage) {
        _stack.clear();
      }
    }
    page = buildPage(name: _setting?.name, arguments: _setting?.arguments);
    tempPages = [...tempPages, page];
    NavigatorUtil().notify(tempPages, _stack);

    _stack = tempPages;
    notifyListeners();
  }

  @override
  Future<void> setInitialRoutePath(RouteSettings configuration) {
    return super.setInitialRoutePath(_setting);
  }

  @override
  Future<void> setNewRoutePath(RouteSettings configuration) async {
    if (configuration.arguments is BaseArgument) {
      if ((configuration.arguments as BaseArgument).name ==
          RouterName.rootPage) {
        _stack.clear();
      }
    } else {
      if (configuration.name == RouterName.rootPage) {
        _stack.clear();
      }
    }
    addPage(name: configuration.name, arguments: configuration.arguments);
  }
}

其中_stack是我们的路由栈,_settingRouteSettings,每执行一个新的路由跳转,都会创建一个RouteSettings对象并赋值给_setting,最终在插入_stack里。buildPage()的实现如下:

//建造页面
buildPage({required name, arguments}) {
  return MaterialPage(
      child: getPageChild(name: name, arguments: arguments),
      arguments: arguments,
      name: name,
      key: ValueKey(
          arguments is BaseArgument ? (arguments as BaseArgument).name : name));
}

其中MaterialPage继承了PagegetPageChild()实现如下:

Widget getPageChild({required name, arguments}) {
  Widget page;
  Map? arg;
  if (arguments is Map) {
    arg = arguments;
  }
  if (arguments is BaseArgument) {
    switch ((arguments as BaseArgument).name) {
      case RouterName.rootPage:
        page = TestHomePage();
        break;
      case RouterName.testChild1Page:
        page = TestChildPage1(
          argument: arguments.arguments as TestChild1PageArgument,
        );
        break;
      case RouterName.testChild2Page:
        page = TestChildPage2();
        break;
      default:
        page = TestHomePage();
    }
  } else {
    page = TestHomePage();
  }

  return page;
}

class RouterName {
  static const rootPage = "/";
  static const testChild1Page = "/testChild1Page";
  static const testChild2Page = "/testChild2Page";
}

我们可以看到,在真正返回Widget时,我们并没有使用传入的name参数,而是BaseArgumentname参数,这是为什么呢?这是在于我们为了实现无论页面怎么跳转,从头到尾浏览器只保留一个history,因此我们在页面跳转时RouteSettingsname并不发生变化,通过其arguments里面的参数变化返回不同的Widget。这样在路由跳转时,其实MaterialPage由于name一直会被直接复用,从而不会创建新的MaterialPage也就不会产生history。 NavigatorUtil是由业务调用的,创建跳转方法的抽象类,提供了onJumpTo()onReplaceAndJumpTo()onClearStack()onBack()四个方法供业务调用,我们可以看一下onJumpTo()的实现:

@override
  void onJumpTo(
      {required name,
      Object? stackArguments,
      Map<String, dynamic>? historyArgMap,
      BuildContext? context}) {
      var arg = BaseArgument();
      arg.name = name;
      arg.arguments = stackArguments;
      RouteSettings settings =
          RouteSettings(name: RouterName.rootPage, arguments: arg);
      return _function!.onJumpTo!(settings);
  }

可以看到在创建RouteSettings对象时,nameRouterName.rootPagearg时由业务传的真正的跳转页面相关的参数。我们看一下业务的调用:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Column(
          children: [
            Text("TestHomePage"),
            Text("history length is : " + window.history.length.toString()),
            Text("href: " + WebUtil.get().getWindow().location.href),
            TextButton(
                onPressed: () {
                  var arg = TestChild1PageArgument()..isSuccess = "false";
                  NavigatorUtil().onJumpTo(
                      name: RouterName.testChild1Page,
                      stackArguments: arg,
                      historyArgMap: arg.toJson(),
                      context: context);
                },
                child: Text("Go to TestChildPage1"))
          ],
        ),
      ),
    );
  }
@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Column(
          children: [
            Text("TestChildPage1"),
            Text("history length is : " + window.history.length.toString()),
            Text("href: " + WebUtil.get().getWindow().location.href),
            TextButton(
                onPressed: () {
                  NavigatorUtil().onJumpTo(
                      name: RouterName.testChild2Page, context: context);
                },
                child: Text("Go to TestChildPage2")),
            TextButton(
                onPressed: () {
                  NavigatorUtil().onBack();
                },
                child: Text("Back to TestHomePage")),
          ],
        ),
      ),
    );
  }
@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Column(
          children: [
            Text("TestChildPage2"),
            Text("history length is : " + window.history.length.toString()),
            Text("href: " + WebUtil.get().getWindow().location.href),
            TextButton(
                onPressed: () {
                  NavigatorUtil().onBack();
                },
                child: Text("Back to TestChild1page")),
            TextButton(
                onPressed: () {
                  NavigatorUtil().onClearStack();
                },
                child: Text("Back to Root")),
          ],
        ),
      ),
    );
  }

我们看一下截图展示:ed0664521819cfd77543692bd24831a2.jpegba508520efaeaed5d0c481309c870d2c.jpeg79a825d2a509384bdb03fd0f8dfd0693.jpeg

在这个过程中href不会发生变化,history也不会发生变化,完全符合我们的预期。

07

总结

FlutterNavigator 2.0引入了声明式的API,使页面路由管理更加灵活和强大。相较于Navigator 1.0Navigator 2.0支持更复杂的路由操作,如嵌套路由和动态路由配置。它使用不可变的Page对象列表来表示路由历史,与Flutter的不可变Widgets设计理念一致。Navigator 2.0还支持命名路由,通过简单的路由名称即可实现页面跳转,大大简化了路由管理的复杂度。此外,它还提供了更丰富的路由回调和状态管理功能,使开发者能够更轻松地构建复杂的Flutter应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2260523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【容器】k8s学习笔记基础部分(三万字超详细)

概念 应用部署方式演变 在部署应用程序的方式上&#xff0c;主要经历了三个时代&#xff1a; 传统部署&#xff1a;互联网早期&#xff0c;会直接将应用程序部署在物理机上 优点&#xff1a;简单&#xff0c;不需要其它技术的参与 缺点&#xff1a;不能为应用程序定义资源使…

PostgreSQL 常用运维SQL整理

一、查询并杀会话 -- 查询会话 select pid,usename,client_addr,client_port,query_start,query,wait_event from pg_stat_activity; -- 杀会话 select pg_terminate_backend(pid号); -- 使用如下命令自动生成杀会话语句 select datid,datname,pid,usesysid,usename,applicat…

前端0基础用Cursor完成管理系统页面 - 1

Cursor下载 下载链接: https://www.cursor.com/ Hello World! 作为完全不会前端的人&#xff0c;首先需要让AI帮我们搭建一个HelloWorld界面 确定语言框架 首先要给AI框定好前端语言和框架&#xff0c;由于AI的物料大量来自网上的开源项目&#xff0c;所以越是受欢迎的开源…

系统组件优化的思考框架

我之前的文章里有分享过自己总结的做技术选型的思考框架&#xff0c;本文将会分享一下我总结的做系统组件调优/优化的思考框架。 组件优化的思考框架 常见的互联网架构基本离不开数据库、缓存、消息队列、搜索、数据处理等等各种组件&#xff0c;虽然组件的形态不一、功能不同…

Linux shell的七大功能 ---自动补齐、管道机制、别名

1、自动补齐---TAB 输入命令的前几个字符&#xff0c;按下tab键&#xff0c;会自动补齐完整的字符&#xff0c;若有多个命令、文件或目录的前几个字符相同&#xff0c;按下tab将会全部列举出来 2、管道机制---| 例如&#xff1a;ls -- help |more 将有关ls的帮助内容传递给“|…

计算机网络-基础概念(HTTP,TPC/IP, DNS,URL)

HTTP不同的版本 HTTP0.9于1990年问世&#xff0c;此时HTTP并没有作为正式的标准被建立。HTTP正式被公布是1996年的5月&#xff0c;版本命名为HTTP/1.0。HTTP1.1&#xff0c;1997年1月公布&#xff0c;目前仍然是主流版本的HTTP协议版本。 TCP/IP 通常使用的网络是在TCP/IP协…

12.3【JAVA-EXP4-DEBUGSTUDY】

java升级版本 JDK 1.8 是 Java Development Kit 的第 8 版本&#xff0c;发布于 2014 年 3 月 18 日。这个版本是 Java SE&#xff08;Standard Edition&#xff09;的一部分&#xff0c;包含了 Java 编程语言的实现、编译器、调试工具和其他相关组件 JDK 1.8: 这里的 1.8 表…

在Windows上运行mediapipe:适合新手的AI框架

一、mediapipe简介 mediapipe可以被视为谷歌版的onnx&#xff0c;其设计目的在于跨平台部署AI模型&#xff0c;并提供一系列工具来监测不同平台、不同设备运行人工智能模型时的性能表现。 尽管mediapipe已经陆续支持训练自定义模型&#xff0c;但博主更推荐使用Pytorch/Tenso…

自然语言处理:我的学习心得与笔记

Pytorch 1.Pytorch基本语法 1.1 认识Pytorch 1.2 Pytorch中的autograd 2.Pytorch初步应用 2.1 使用Pytorch构建一个神经网络 2.2 使用Pytorch构建一个分类器 小节总结 学习了什么是Pytorch. 。Pytorch是一个基于Numpy的科学计算包,作为Numpy的替代者,向用户提供使用GPU强大…

IAR环境下STM32静态库编译及使用

IAR环境下STM32静态库编译及使用 前言 最近了解到了STM32的静态库与动态库&#xff0c;在此记录一下STM32静态库的生成与使用。 静态库的作用主要是对代码进行封装及保护&#xff0c;使其他使用者只知其然而不知其所以然&#xff0c;因为封装后的静态库只有.h文件没有.c文件。…

【常考前端面试题总结】---2025

React fiber架构 1.为什么会出现 React fiber 架构? React 15 Stack Reconciler 是通过递归更新子组件 。由于递归执行&#xff0c;所以更新一旦开始&#xff0c;中途就无法中断。当层级很深时&#xff0c;递归更新时间超过了 16ms&#xff0c;用户交互就会卡顿。对于特别庞…

Leetcode 面试150题 399.除法求值

系列博客目录 文章目录 系列博客目录题目思路代码 题目 链接 思路 广度优先搜索 我们可以将整个问题建模成一张图&#xff1a;给定图中的一些点&#xff08;点即变量&#xff09;&#xff0c;以及某些边的权值&#xff08;权值即两个变量的比值&#xff09;&#xff0c;试…

hbase读写操作后hdfs内存占用太大的问题

hbase读写操作后hdfs内存占用太大的问题 查看内存信息hbase读写操作 查看内存信息 查看本地磁盘的内存信息 df -h查看hdfs上根目录下各个文件的内存大小 hdfs dfs -du -h /查看hdfs上/hbase目录下各个文件的内存大小 hdfs dfs -du -h /hbase查看hdfs上/hbase/oldWALs目录下…

使用webrtc-streamer查看实时监控

摄像头配置&#xff08;海康摄像头为例&#xff09; 摄像头视频编码应改成H264格式 webrtc-streamer下载 webrtc-streamer下载地址 下载后解压出来双击运行&#xff0c;端口默认8000 VUE2项目引入文件 在项目静态文件“public”中需引入两个js文件“webrtcstreamer.js”与“…

L1-3流量分析

1. 初步分析 数据包下载 流量分析基础篇 使用科来网络分析系统&#xff0c;打开L1-3.pcapng数据包&#xff0c;查看数据包中ssh的协议占的比例较大。 2. 通过分析数据包L1-3&#xff0c;找出黑客的IP地址&#xff0c;并将黑客的IP地址作为FLAG(形式:[IP地址)提交; 获取的fl…

【经典】制造供应链四类策略(MTS、MTO、ATO、ETO)细说

关注作者 制造供应链的牛鞭问题与复杂问题主要是从两个方面解决&#xff0c;一是同步化供应链消减从需求到供应的放大效应&#xff0c;二是供应链细分&#xff0c;针对不同的客户、不同的需求供应的匹配策略来应对复杂性&#xff0c;更好的满足客户并以最低的总成本来实现。 对…

前端成长之路:CSS盒子模型

盒子模型是页面布局的核心&#xff0c;通过盒子模型才能更好的进行页面布局。 网页布局的本质 网页布局的核心本质其实是&#xff1a;HTML网页元素就是一个个的盒子box&#xff0c;通过CSS可以设置好盒子的样式&#xff0c;和盒子需要摆放的位置&#xff1b;简单说来就是通过…

LeetCode刷题 -- 字符串

目录 最长公共前缀题目解析算法原理代码 最长回文子串题目解析算法原理代码 二进制求和题目解析算法原理代码 字符串相乘题目解析算法原理代码 最长公共前缀 题目链接 题目解析 只需找出字符串中的公共的最长字符串即可 算法原理 1.法一&#xff1a;两两字符串比较&#xff0c;…

4G模块详解

在之前的教程中&#xff0c;无线通信技术我们学习了蓝牙和 WiFi&#xff0c;今天我们要来学习 4G。 4G 模块在距离上有个突破&#xff0c;它不像蓝牙短距离&#xff0c;也不像 WiFi 只能在局域网&#xff0c;4G 模块可使用户无论在哪&#xff0c;只要有 4G 网络信号覆盖&#…

uni-app实现小程序、H5图片轮播预览、双指缩放、双击放大、单击还原、滑动切换功能

前言 这次的标题有点长&#xff0c;主要是想要表述的功能点有点多&#xff1b; 简单做一下需求描述 产品要求在商品详情页的头部轮播图部分&#xff0c;可以单击预览大图&#xff0c;同时在预览界面可以双指放大缩小图片并且可以移动查看图片&#xff0c;双击放大&#xff0…