Flutter Navigator2.0的原理和Web端实践
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
能够满足不同平台(如iOS
、Android
、Web
等)的导航需求,使得开发者能够更加方便地构建跨平台的应用。强大的页面操作能力
Navigator 2.0
提供了更加丰富的页面操作能力,如移除栈内中间页面、交换页面等。这些操作在Navigator 1.0
中很难实现或需要编写复杂的代码,而在Navigator 2.0
中则变得简单直接。
03
核心组件
Router 在
Navigator 2.0
中,Router
组件是路由管理的核心。它负责根据当前的路由信息(RouteInformation
)和路由信息解析器(RouteInformationParser
)来构建和更新UI
。Router
组件接收三个主要参数:1.routeInformationProvider:提供当前的路由信息;
2.routeInformationParser:将路由信息解析为路由配置;
3.routerDelegate:根据路由配置构建和更新
UI
。RouteInformationProvider
RouteInformationProvider
是一个提供当前路由信息的组件。它通常与平台相关的路由信息源(如浏览器的URL
、Android
的Intent
等)集成,以获取当前的路由信息。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
对象。其中根据路由配置构建和更新UI
,RouteInformationParser
负责将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
对象,之前章节提到过,传了RouteInformationParser
和RouterDelegate
这两个对象。当传递了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
:
@overrideWidget 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
的调用时机:
@overridevoid initState() {super.initState();widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);widget.routerDelegate.addListener(_handleRouterDelegateNotification);}
我们可以看到在initState()
时,也就是在Router
被初始化的时候由widget.routeInformationProvider
来监听一些状态实现新页面的切换。我们来看一下routeInformationProvider
。RouteInformationProvider
在我们自己没有创建的情况下,系统会默认为我们创建一个PlatformRouteInformationProvider
对象。它实际上是个ChangeNotifier
。系统会监听每一帧的信号发送,调用其父类routerReportsNewRouteInformation()
方法,我们看看它的实现:
@overridevoid 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()
方法:
@overrideFuture<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()
的地方:
@overridevoid 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();@overrideWidget build(BuildContext context) {return MaterialApp.router(debugShowCheckedModeBanner: false,routeInformationParser: MyRouteParser(),routerDelegate: delegate,);}
Parser
实现非常简单:
class MyRouteParser extends RouteInformationParser<RouteSettings> {@override///parseRouteInformation() 方法的作用就是接受系统传递给我们的路由信息 routeInformationFuture<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@overrideGlobalKey<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();}}));}@overrideRouteSettings? get currentConfiguration {return _stack.last;}@overrideFuture<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();}@overrideWidget 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();}@overrideFuture<void> setInitialRoutePath(RouteSettings configuration) {return super.setInitialRoutePath(_setting);}@overrideFuture<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
是我们的路由栈,_setting
是RouteSettings
,每执行一个新的路由跳转,都会创建一个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
继承了Page
。getPageChild()
实现如下:
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
参数,而是BaseArgument
的name
参数,这是为什么呢?这是在于我们为了实现无论页面怎么跳转,从头到尾浏览器只保留一个history
,因此我们在页面跳转时RouteSettings
的name
并不发生变化,通过其arguments
里面的参数变化返回不同的Widget
。这样在路由跳转时,其实MaterialPage
由于name
一直会被直接复用,从而不会创建新的MaterialPage
也就不会产生history
。 NavigatorUtil
是由业务调用的,创建跳转方法的抽象类,提供了onJumpTo()
,onReplaceAndJumpTo()
,onClearStack()
,onBack()
四个方法供业务调用,我们可以看一下onJumpTo()
的实现:
@overridevoid 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
对象时,name
为RouterName.rootPage
,arg
时由业务传的真正的跳转页面相关的参数。我们看一下业务的调用:
@overrideWidget 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"))],),),);}
@overrideWidget 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")),],),),);}
@overrideWidget 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")),],),),);}
我们看一下截图展示:
在这个过程中href
不会发生变化,history
也不会发生变化,完全符合我们的预期。
07
总结
Flutter
的Navigator 2.0
引入了声明式的API
,使页面路由管理更加灵活和强大。相较于Navigator 1.0
,Navigator 2.0
支持更复杂的路由操作,如嵌套路由和动态路由配置。它使用不可变的Page
对象列表来表示路由历史,与Flutter
的不可变Widgets
设计理念一致。Navigator 2.0
还支持命名路由,通过简单的路由名称即可实现页面跳转,大大简化了路由管理的复杂度。此外,它还提供了更丰富的路由回调和状态管理功能,使开发者能够更轻松地构建复杂的Flutter
应用。
相关文章:

Flutter Navigator2.0的原理和Web端实践
01 背景与动机 在Navigator 2.0推出之前,Flutter主要通过Navigator 1.0和其提供的 API(如push(), pop(), pushNamed()等)来管理页面路由。然而,Navigator 1.0存在一些局限性,如难以实现复杂的页面操作(如移…...

初次使用uniapp编译到微信小程序编辑器页面空白,真机预览有内容
uniapp微信小程序页面结构 首页页面代码 微信小程序模拟器 模拟器页面为空白时查了下,有几个说是“Hbuilder编译的时候应该编译出来一个app.js文件 但是却编译出了App.js”,但是我的小程序结构没问题,并且真机预览没有问题 真机调试 根据defi…...

【HF设计模式】03-装饰者模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。 摘要 《Head First设计模式》第3章笔记:结合示例应用和代码,介绍装饰者模式,包括遇到的问题、遵循的 OO 原则、达到的效果。 …...
【人工智能-中级】模型部署与优化:从本地实验到云端与边缘部署
模型部署与优化:从本地实验到云端与边缘部署 在机器学习和深度学习模型训练完成后,如何高效、稳定地将模型部署到生产环境中,是实际应用中的关键环节。模型部署不仅涉及技术实现,还需要考虑性能优化、资源管理和安全性等多方面因素。本文将全面探讨模型部署与优化的相关内…...
Jenkins 编写Pipeline 简介及使用初识详解
一、Jenkins Pipeline简介 Jenkins Pipeline是Jenkins的一个重要功能,Jenkins 2.0 以上才会有,一系列 Jenkins 插件将整个持续集成用解释性代码 Jenkinsfile 来描述,它允许开发者以代码的方式定义整个持续集成和交付(CI/CD)流程,包括构建、测试、部署和监控等步骤。Jenk…...

uboot移植网络驱动过程,无法ping通mx6ull和ubuntu问题解决方案
开发板:mx6ull-ALPHA_V2.4 ubuntu版本:20.04 1.现在虚拟机设置中添加网路适配器用于开启桥接模式 2.在编辑中打开“虚拟网络编辑器” 我的电脑本身只有VMnet1和VMnet8,需要底下“添加网络”,增加这个VMnet0 ,并且进行…...

精准预测美国失业率和贫困率,谷歌人口动态基础模型PDFM已开源,可增强现有地理空间模型
疾病、经济危机、失业、灾害……人类世界长期以来被各种各样的问题「侵扰」,了解人口动态对于解决这类复杂的社会问题至关重要。 政府相关人员可以通过人口动态数据来模拟疾病的传播,预测房价和失业率,甚至预测经济危机。然而,在过…...
C#速成(文件读、写操作)
导包 using System.IO;1、写入文件(重要) StreamWriter sw new StreamWriter("C:\Users\29674\Desktop\volumn.txt");//创建一个TXT的文件 sw.WriteLine(textBox2.Text);//写入文件的内容 sw.Close();//关闭2、读取文件(不重要&…...

SQL server学习03-创建和管理数据表
目录 一,SQL server的数据类型 1,基本数据类型 2,自定义数据类型 二,使用T-SQL创建表 1,数据完整性的分类 2,约束的类型 3,创建表时创建约束 4,任务 5,由任务编写…...

【UE5 “RuntimeLoadFbx”插件】运行时加载FBX模型
前言 为了解决在Runtime时能够直接根据FBX模型路径直接加载FBX的问题,推荐一款名为“RuntimeLoadFBX”的插件。 用法 插件用法如下,只需要指定fbx的地址就可以在场景中生成Actor模型 通过指定输入参数“Cal Collision”来设置FBX模型的碰撞 还可以通过…...

【潜意识Java】深入理解 Java 面向对象编程(OOP)
目录 什么是面向对象编程(OOP)? 1. 封装(Encapsulation) Java 中的封装 2. 继承(Inheritance) Java 中的继承 3. 多态(Polymorphism) Java 中的多态 4. 抽象&…...
windows同时使用多个网卡
windows同时链接了有线网络,多个无线网卡,默认会使用有线网络,如果想要局域网内使用某个特定的网络,可以设置静态ip 1. 首先删除原来的静态网络(不冲突可以不删除),我这里usb无线网卡切换过usb插口,这里需要删除原来的. 使用 route print 查看接口列表及静态路由信息 route p…...

Spark执行计划解析后是如何触发执行的?
在前一篇Spark SQL 执行计划解析源码分析中,笔者分析了Spark SQL 执行计划的解析,很多文章甚至Spark相关的书籍在讲完执行计划解析之后就开始进入讲解Stage切分和调度Task执行,每个概念之间没有强烈的关联,因此这中间总感觉少了点…...
B4X编程语言:B4X控件方法汇总
1、AddNode、AddView方法 AddNode(Node As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J控件 AddView(View As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J的B4XView …...

基于XML配置Bean和基于XML自动装配
目录 基于XML配置Bean id分配规则 通过id获取bean 通过类型获取bean 通过C命名空间配置bean 使用C命名空间 通过P命名空间配置bean 通过util:list进行配置bean 指定id,直接ref引用过来 通过外部属性文件配置Bean Bean信息重用(继承)…...
全排列 dfs
给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。 我们假设对于小写字母有 a<b<…<y<z ,而且给定的字符串中的字母已经按照从小到大的顺序排列。 输入格式 输入只有一行,是一个由不同的小写字母组成的字符串…...
linux内存相关命令的尝试
文章目录 前言freeMem 部分的解释Swap 部分的解释 vmstatProcs (进程)Memory (内存)Swap (交换)IO (磁盘 I/O)System (系统)CPU (处理器) pidstat标题行解释数据列解释 sar字段含义解释示例分析 总结 前言 菜就多练,昨天看了一篇有关剖析 RocksDB 内存超限问题的文…...

Vue2 基础
Vue 2 是 Vue.js 的第二个主要版本,于 2016 年发布。它是一个渐进式的 JavaScript 框架,以其简单、灵活、易用性高而广受欢迎。Vue 2 主要专注于构建用户界面(UI),并且非常适合用于构建单页应用(SPA&#x…...

递归问题(c++)
递归设计思路 数列递归 : 如果一个数列的项与项之间存在关联性,那么可以使用递归实现 ; 原理 : 如果一个函数可以求A(n),那么该函数就可以求A(n-1),就形成了递归调用 ; 注意: 一般起始项是不需要求解的,是已知条件 这就是一个典型…...

系统思考—战略决策
别用管理上的勤奋,来掩盖经营上的懒惰。 日本一家物业公司,因经营不善,面临生死存亡的危机。老板为了扭转局面,采取了很多管理手段——提高员工积极性,推行业绩与绩效挂钩,实施各种考核制度。然而…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用
文章目录 一、背景知识:什么是 B-Tree 和 BTree? B-Tree(平衡多路查找树) BTree(B-Tree 的变种) 二、结构对比:一张图看懂 三、为什么 MySQL InnoDB 选择 BTree? 1. 范围查询更快 2…...

数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...
Python网页自动化Selenium中文文档
1. 安装 1.1. 安装 Selenium Python bindings 提供了一个简单的API,让你使用Selenium WebDriver来编写功能/校验测试。 通过Selenium Python的API,你可以非常直观的使用Selenium WebDriver的所有功能。 Selenium Python bindings 使用非常简洁方便的A…...

作为点的对象CenterNet论文阅读
摘要 检测器将图像中的物体表示为轴对齐的边界框。大多数成功的目标检测方法都会枚举几乎完整的潜在目标位置列表,并对每一个位置进行分类。这种做法既浪费又低效,并且需要额外的后处理。在本文中,我们采取了不同的方法。我们将物体建模为单…...