Flutter(二)第一个Flutter应用

news2024/11/18 11:45:34

1.默认应用

在Android Studio中创建好项目以后,项目的入口即是lib下的main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}
//无状态的组件(Stateless widget)
class MyApp extends StatelessWidget {}

//有状态的组件(Stateful widget)
class MyHomePage extends StatefulWidget {}

//Stateful widget 至少由两个类组成:
// 一个StatefulWidget类,一个State类
//StatefulWidget类本身是不变的,但是State类中的状态在 widget 生命周期中可能会发生变化。
//_MyHomePageState类是MyHomePage类对应的状态类,所以build方法在_MyHomePageState里面
class _MyHomePageState extends State<MyHomePage> {}

Stateful widget 至少由两个类组成: 一个StatefulWidget类,一个State类

StatefulWidget类本身是不变的,但是State类中的状态在 widget 生命周期中可能会发生变化。

_MyHomePageState类是MyHomePage类对应的状态类,所以build方法在_MyHomePageState里面

2.Widget 简介

Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行实践处理的,所以记住,Flutter 中万物皆为Widget

Widget 接口

在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素

Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在 Flutter 开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承widget类来实现。StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是 Flutter 中非常重要的两个抽象类

Flutter中的四棵树

Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:

1.根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
2.根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
3.根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。
在这里插入图片描述

真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。

三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应
渲染树在上屏前会生成一棵 Layer 树

StatelessWidget

StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法返回StatelessElement

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget

如下自定义回显字符串的Echo widget

class Echo extends StatelessWidget  {
  //在继承 widget 时,构造第一个参数通常应该是Key
  const Echo({
    //widget 的构造函数参数应使用命名参数
    Key? key,
    //命名参数中的必需要传的参数要添加required,这样有利于静态代码分析器进行检查
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
    //如果 widget 需要接收子 widget ,那么child或children参数通常应被放在参数列表的最后
  }):super(key:key);


  // widget 的属性尽可能的被声明为final,防止被意外改变
  final String text;
  final Color backgroundColor;

  @override
  //每一个 widget 都会对应一个 context 对象
  Widget build(BuildContext context) {
  //context参数提供了
  //从当前 widget 开始向上遍历 widget 树
  //以及按照 widget 类型查找父级 widget 的方法
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

调用

 Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

每一个 widget 都会对应一个 context 对象,context参数提供了
从当前 widget 开始向上遍历 widget 树
以及按照 widget 类型查找父级 widget 的方法

下面是在子树中获取父级 widget 的一个示例:


class ContextRoute extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

StatefulWidget

StatefulWidget也是继承自widget类,添加了一个新的接口createState(),重写createElement()方法返回StatefulElement,StatefulElement中可能会多次调用createState()来创建状态(State)对象。

State

State介绍

一个 StatefulWidget 类会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态

当State被改变时,可以手动调用其setState()方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build方法重新构建 widget 树,从而达到更新UI的目的

State生命周期

理解State的生命周期对flutter开发非常重要,StatefulWidget 生命周期如图
在这里插入图片描述

initState:

当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等

didChangeDependencies():

当State对象的依赖发生变化时会被调用
在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中Inherited widget发生了变化,那么此时InheritedWidget的子 widget 的didChangeDependencies()回调都会被调用,比如当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies也会被调用

build():

它主要是用于构建 widget 子树的,会在如下场景被调用:

在调用initState()之后。
在调用didUpdateWidget()之后。
在调用setState()之后。
在调用didChangeDependencies()之后。
在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后

reassemble():

此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。

didUpdateWidget ():

在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调,
widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用

deactivate():

当 State 对象从树中被移除时,会调用此回调

在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时

如果移除后没有重新插入到树中则紧接着会调用dispose()方法。

dispose():

当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

在 widget 树中获取State对象

我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。

通过Context获取State对象

context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象

// 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();

但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的

在 Flutter 开发中有一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见

所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:

// 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();

比如我们想显示 snack bar 的话可以通过下面代码调用:

 ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("我是SnackBar")),
      );

通过GlobalKey获取State对象

通过GlobalKey来获取! 步骤分两步:

1.给目标StatefulWidget添加GlobalKey。

//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)

2.通过GlobalKey来获取State对象

_globalKey.currentState.openDrawer()

GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey,那么我们便可以通过

globalKey.currentWidget获得该 widget 对象、
globalKey.currentElement来获得 widget 对应的element对象,
如果当前 widget 是StatefulWidget,则可以通过globalKey.currentState来获得该 widget 对应的state对象。

注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。

通过 RenderObject 自定义 Widget

Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。下面我们简单演示一下通过RenderObject定义组件的方式:

class CustomWidget extends LeafRenderObjectWidget{
  @override
  RenderObject createRenderObject(BuildContext context) {
    // 创建 RenderObject
    return RenderCustomObject();
  }
  @override
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 实现布局逻辑
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

LeafRenderObjectWidget 继承 RenderObjectWidget 继承Widget

如果组件不包含子组件,直接继承 LeafRenderObjectWidget
如果自定义的 widget 包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget

Flutter SDK内置组件库介绍

Flutter在基础组件库之上 Flutter 又提供了
一套 Material 风格( Android 默认的视觉风格)
和一套 Cupertino 风格(iOS视觉风格)的组件库。

要使用基础组件库,需要先导入:

import 'package:flutter/widgets.dart';

1.基础组件

Text (opens new window):文本组件。

Row (opens new window)、 Column (opens new window):
弹性布局类 widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。

Stack (opens new window): 取代线性布局 (和 Android 中的FrameLayout相似),允许子 widget 堆叠, 你可以使用 Positioned (opens new window)来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位布局模型设计的。

Container (opens new window): 可让您创建矩形视觉元素,Container 可以装饰一个BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。
Container (opens new window)也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。
另外, Container (opens new window)可以使用矩阵在三维空间中对其进行变换

2.Material组件

Material组件可以帮助我们构建遵循 Material Design 设计规范的应用程序。Material 应用程序以MaterialApp (opens new window) 组件开始
比如Theme组件,它用于配置应用的主题,我们已经使用过多个 Material 组件了,如:Scaffold、AppBar、TextButton等

import 'package:flutter/material.dart';

3.Cupertino组件

Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富

//导入cupertino  widget 库
import 'package:flutter/cupertino.dart';

class CupertinoTestRoute extends StatelessWidget  {
  @override
  widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
            color: CupertinoColors.activeBlue,
            child: Text("Press"),
            onPressed: () {}
        ),
      ),
    );
  }
}

由于 Material 和 Cupertino 都是在基础组件库之上的,所以如果我们的应用中引入了这两者之一,则不需要再引入flutter/ widgets.dart了,因为它们内部已经引入过了。

3.状态管理

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
  • 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。

如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理

全局状态管理 如 Provider、Redux

路由管理

路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈

//导航到新路由   
        Navigator.push( 
          context,
          MaterialPageRoute(builder: (context) {
            return NewRoute();
          })

MaterialPageRoute

MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类
MaterialPageRoute 构造函数的各个参数的意义

MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })

builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。

settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。

maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。

fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。

如果想自定义路由切换动画,可以自己继承 PageRoute 来实现,我们将在后面介绍动画时,实现一个自定义的路由组件

Navigator

Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。
最常用的两个方法

//返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据
Future push(BuildContext context, Route route)

//将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据
bool pop(BuildContext context, [ result ])

Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)

  • 打开新页面并传值到新页面

先定义新页面TipRoute

class TipRoute extends StatelessWidget {
  TipRoute({
    Key key,
    required this.text,  // 接收一个text参数
  }) : super(key: key);
  final String text;

// 打开`TipRoute`,并等待返回结果
          var result = Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return TipRoute(
                  // 路由参数
                  text: "我是提示xxxx",
                );
              },
            ),
          );
          //输出`TipRoute`路由返回结果
          print("路由返回值: $result");
  • 新页面点击返回键并回传数据
Navigator.pop(context, "我是返回值")

命名路由

给路由起一个名字,通过路由名字直接打开新的路由

Map<String, WidgetBuilder> routes;

MaterialApp(
  title: 'Flutter Demo',
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  } 
);

//有参数注册路由表
MaterialApp(
  ... //省略无关代码
  routes: {
   "tip2": (context){
     return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
   },
 }, 
);

通过路由名打开新路由页
Future pushNamed(BuildContext context, String routeName,{Object arguments})

//无参打开
Navigator.pushNamed(context, "new_page");
//带参数打开新页面
Navigator.of(context).pushNamed("new_page", arguments: "hi");

//通过RouteSetting获取路由参数  
 var args=ModalRoute.of(context).settings.arguments;

路由生成钩子

需求:登录前可以看商品,登录后才能看订单
MaterialApp有一个onGenerateRoute属性。
Navigator.pushNamed(…)打开命名路由时,如果路由表中没有注册,会调用onGenerateRoute来生成路由

MaterialApp(
  ... //放弃使用路由表,使用此方法动态跳转
  onGenerateRoute:(RouteSettings settings){
	  return MaterialPageRoute(builder: (context){
		   String routeName = settings.name;
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 其他情况则正常打开路由。
     }
   );
  }
);

onGenerateRoute 只会对命名路由生效

包管理

依赖Pub仓库

Flutter 使用pubspec.yaml来管理第三方依赖包。

name: flutter_in_action
description: First Flutter Application.

version: 1.0.0+1

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true

各个字段的意义:

name:应用或包名称。
description: 应用或包的描述、简介。
version:应用或包的版本号。
dependencies:应用或包依赖的其他包或插件。
dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
flutter:flutter相关的配置选项。

如何添加、下载并使用第三方包?
仓库:https://pub.dev/

比如添加english_words:https://pub.dev/packages/english_words/install

dependencies:
  english_words: ^4.0.0

之后执行flutter packages get 命令来下载依赖包
或者
在pubspec.yaml单击右上角的 Pub get

依赖本地包和git仓库

上面依赖方式是依赖Pub仓库的,我们还可以依赖本地包和git仓库。

//依赖本地包
dependencies:
	pkg1:
        path: ../../code/pkg1

//依赖git包(假设包在根目录)
dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git
//依赖git包(假设包不在根目录可以指定目录)
dependencies:
  package1:
    git:
      url: git://github.com/flutter/packages.git
      path: packages/package1   

更多依赖方式:https://dart.dev/tools/pub/dependencies

资源管理(assets)

Flutter 也使用pubspec.yaml 来管理资源

//asset的实际目录可以是任意文件夹
flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

assets构建过程中,会在相邻子目录中查找具有相同名称的任何文件

建议使用 DefaultAssetBundle (opens new window)来获取当前 BuildContext 的AssetBundle

//加载图片,返回ImageProvider
AssetImage('graphics/background.png') 
//或者,返回widget
Image.asset('graphics/background.png') 

//假设您的应用程序依赖于一个名为“my_icons”的包:…/icons/heart.png
AssetImage('icons/heart.png', package: 'my_icons')
Image.asset('icons/heart.png', package: 'my_icons')

如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets
Android导航到…/android/app/src/main/res目录设置
IOS 导航到…/ios/Runner/Assets.xcassets/AppIcon.appiconset

如果不在main()方法中调用runApp 启动屏幕将永远持续显示。

调试Flutter应用

print: 输出到系统控制台
debugPrint:如果你一次输出太多,那么Android有时会丢弃一些日志行。为了避免这种情况,我们可以使用Flutter的foundation库中的debugPrint()
flutter logs:查看日志

插入编程式断点使用debugger

Widget 树

要转储Widgets树的状态,请调用debugDumpApp()

I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559):MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559):ScrollConfiguration()
I/flutter ( 6559):AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559):Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559):WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559):CheckedModeBanner()
I/flutter ( 6559):Banner()
I/flutter ( 6559):CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559):DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559):MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559):LocaleQuery(null)
I/flutter ( 6559):Title(color: Color(0xff2196f3))

渲染树

如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()转储渲染树

I/flutter ( 6559): RenderView
I/flutter ( 6559):  │ debug mode enabled - android
I/flutter ( 6559):  │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559):  │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559):  │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559):  │
I/flutter ( 6559):  └─child: RenderCustomPaint
I/flutter ( 6559):    │ creator: CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559):    │   WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559):    │   Theme ← AnimatedTheme ← ScrollConfiguration ← MaterialApp ←
I/flutter ( 6559):[root]
I/flutter ( 6559):    │ parentData: <none>
I/flutter ( 6559):    │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):    │ size: Size(411.4, 683.4)

当调试布局问题时,关键要看的是size和constraints字段。约束沿着树向下传递,尺寸向上传递

Layer树

渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree()

I/flutter : TransformLayer
I/flutter :  │ creator: [root]
I/flutter :  │ offset: Offset(0.0, 0.0)
I/flutter :  │ transform:
I/flutter :[0] 3.5,0.0,0.0,0.0
I/flutter :[1] 0.0,3.5,0.0,0.0
I/flutter :[2] 0.0,0.0,1.0,0.0
I/flutter :[3] 0.0,0.0,0.0,1.0
I/flutter :  │
I/flutter :  ├─child 1: OffsetLayer
I/flutter :  │ │ creator: RepaintBoundary ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355] ← Stack ← Overlay-[GlobalKey 625702218] ← Navigator-[GlobalObjectKey _MaterialAppState(859106034)] ← Title ← ⋯
I/flutter :  │ │ offset: Offset(0.0, 0.0)
I/flutter :  │ │
I/flutter :  │ └─child 1: PictureLayer
I/flutter :  │
I/flutter :  └─child 2: PictureLayer

要找出相对于帧的开始/结束事件发生的位置,可以切换debugPrintBeginFrameBanner (opens new window)和debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。

可视化调试

我们也可以通过设置debugPaintSizeEnabled为true以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。

调试动画

调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次

调试性能问题

要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹

统计应用启动时间

$ flutter run --trace-startup --profile

DevTools

Flutter DevTools 是 Flutter 可视化调试工具

在这里插入图片描述

Flutter异常捕获

Dart单线程模型

在 Java 和 Objective-C(以下简称“OC”)中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart或JavaScript中则不会!这和它们的运行机制有关系。

Java 和 OC 都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但 Dart 和 JavaScript 不会,它们都是单线程模型
在这里插入图片描述
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。从图中可以发现,微任务队列的执行优先级高于事件队列。

Dart线程运行过程,如上图中所示,入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止。

在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。

在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的

Flutter异常捕获

Dart中可以通过try/catch/finally来捕获代码块异常

@override
void performRebuild() {
 ...
  try {
    //执行build方法  
    built = build();
  } catch (e, stack) {
    // 有异常时则弹出错误提示  
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
  } 
  ...
}

在Dart中,异常分两类:同步异常和异步异常,同步异常可以通过try/catch捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:

try{
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
    print(e)
}

Dart中有一个runZoned(…) 方法,Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常


//我们 APP 中所有调用print方法输出日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将异常信息和日志统一上报。

另外我们还拦截了未被捕获的异步错误,这样一来,结合上面的 FlutterError.onError 我们就可以捕获我们Flutter应用错误了并进行上报了!
void collectLog(String line){
    ... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
    ... //上报错误和日志逻辑
}

FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
    ...// 构建错误信息
}

void main() {
  var onError = FlutterError.onError; //先将 onerror 保存起来
  FlutterError.onError = (FlutterErrorDetails details) {
    onError?.call(details); //调用默认的onError
    reportErrorAndLog(details); //上报
  };
  runZoned(
  () => runApp(MyApp()),
  zoneSpecification: ZoneSpecification(
    // 拦截print
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
      collectLog(line);
      parent.print(zone, "Interceptor: $line");
    },
    // 拦截未处理的异步错误
    handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
                          Object error, StackTrace stackTrace) {
      reportErrorAndLog(details);
      parent.print(zone, '${error.toString()} $stackTrace');
    },
  ),
 );
}

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

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

相关文章

基于paddlex的C#环境配置及其部署【附带安装包】

前言 最近应老师要求部署一个基于paddlex的C#环境&#xff0c;踩了一些坑&#xff0c;经过几个版本的安装测试&#xff0c;最终成功&#xff0c;这里记录一下。此次用到的所有软件的安装包如下&#xff1a; 补充的vs 2019安装包&#xff1a; 官方参考链接&#xff08;没有环…

Meta利用视觉信息来优化3D音频模型,未来将用于AR/VR

我们知道&#xff0c;Meta为了给AR眼镜打造智能助手&#xff0c;专门开发了第一人称视觉模型和数据集。与此同时&#xff0c;该公司也在探索一种将视觉和语音融合的AI感知方案。相比于单纯的语音助手&#xff0c;同时结合视觉和声音数据来感知环境&#xff0c;可进一步增强智能…

ERD Online 4.0.9 在线数据库建模、元数据管理平台(免费、私有部署)

ERD Online 是全球第一个开源、免费在线数据建模、元数据管理平台。提供简单易用的元数据设计、关系图设计、SQL查询等功能&#xff0c;辅以版本、导入、导出、数据源、SQL解析、审计、团队协作等功能、方便我们快速、安全的管理数据库中的元数据。 4.0.9 ❝ feat(erd): 主键生…

Opencv项目实战:21 美国ASL手势识别

0、项目介绍 首先&#xff0c;我可以保证在这里&#xff0c;你并不需要多么了解深的机器学习算法&#xff0c;我的初衷是通过本项目&#xff0c;激发大家学习机器学习的动力。选择这种手势原因是因为只有24个字母&#xff0c;你的电脑足以带的动&#xff0c;虽然我只训练A、B、…

group by聚合分组后如何获取分组数据

之前用group by分组后一直困惑怎么把分组后的数据拿到&#xff0c;因为分组后同一组的只有一条数据&#xff0c;最后发现了group_concat函数。记录一下&#xff0c;以后能用。语法&#xff1a;group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator…

【MySQL - InnoDB 存储结构】行格式详解

我们平时对 MySQL 的了解都只是限制在使用层面上&#xff0c;但是难道你就没有一个时刻好奇 MySQL 的内部结构嘛&#xff0c;我们通过 SQL 语句插入的一条条记录在 MySQL 底层到底是以什么格式存储的呢 &#xff1f; 本文就将以 InnoDB 存储引擎为例子&#xff0c;介绍 MySQL 存…

专科top4|临床医生CSC公派博士后美国凯斯西储大学医院赴职

Q医生符合CSC公派博士后申报条件&#xff0c;我们先为其取得Hopkins的邀请函并获CSC批准&#xff0c;后因导师失联&#xff0c;为保险起见&#xff0c;我们又继续申请并获得凯斯西储大学医学院彩虹宝宝和儿童医院的邀请函&#xff0c;该院连续20年被评为全美最好儿童医院&#…

【vulhub漏洞复现】CVE-2015-5254 ActiveMQ反序列化漏洞

一、漏洞详情Apache ActiveMQ是美国阿帕奇&#xff08;Apache&#xff09;软件基金会所研发的一套开源的消息中间件&#xff0c;它支持Java消息服务&#xff0c;集群&#xff0c;Spring Framework等。配置Apache ActiveMQ需要提前有jdk的环境。基于MQTT&#xff0c;消息订阅和分…

Javascript的API基本内容(三)

一、事件流 假设页面里有个div&#xff0c;当触发事件时&#xff0c;会经历两个阶段&#xff0c;分别是捕获阶段、冒泡阶段简单来说&#xff1a;捕获阶段是 从父到子 冒泡阶段是从子到父实际工作都是使用事件冒泡为主 二、页面加载事件 加载外部资源&#xff08;如图片、外联CS…

Portraiture4免费磨皮插件支持PS/LR

Portraiture 4免去了繁琐的手工劳动&#xff0c;选择性的屏蔽和由像素的平滑&#xff0c;以帮助您实现卓越的肖像润色。智能平滑&#xff0c;并删除不完善之处&#xff0c;同时保持皮肤的纹理和其他重要肖像的细节&#xff0c;如头发&#xff0c;眉毛&#xff0c;睫毛等。 一键…

Python Flask + Echarts 轻松制作动态酷炫大屏( 附代码)

目录一、确定需求方案二、整体架构设计三、编码实现 &#xff08;关键代码&#xff09;四、完整代码五、运行效果1.动态实时更新数据效果图 说明: 其中 今日抓拍&#xff0c;抓拍总数&#xff0c;预警信息统计&#xff0c;监控点位统计图表 做了动态实时更新处理。 ​ 2.静态…

Java岗面试题--Java并发(volatile 专题)

目录1. 面试题一&#xff1a;谈谈 volatile 的使用及其原理补充&#xff1a;内存屏障volatile 的原理2. 面试题二&#xff1a;volatile 为什么不能保证原子性3. 面试题三&#xff1a;volatile 的内存语义4. 面试题四&#xff1a;volatile 的实现机制5. 面试题五&#xff1a;vol…

用c语言模拟实现常用字符串函数

目录 一.常用字符串函数介绍 1.strlen 2. strcpy 3.strcmp 4.strcat 5.strstr 二.模拟实现常用字符串函数 1.strlen 2.strcpy 3.strcmp 4.strcat 5.strstr 一.常用字符串函数介绍 1.strlen 字符串strlen是用来求字符串长度的&#xff0c;我们可以打开cpp网站查看有关…

浅谈模型评估选择及重要性

作者&#xff1a;王同学 来源&#xff1a;投稿 编辑&#xff1a;学姐 模型评估作为机器学习领域一项不可分割的部分&#xff0c;却常常被大家忽略&#xff0c;其实在机器学习领域中重要的不仅仅是模型结构和参数量&#xff0c;对模型的评估也是至关重要的&#xff0c;只有选择那…

vector迭代器失效与深浅拷贝问题

目录 1、vector迭代器失效问题 1.1、insert迭代器失效 扩容导致野指针 意义变了 官方库windows下VS和linux下对insert迭代器失效的处理 1.2、erase迭代器失效 官方库windows下VS和linux下对erase迭代器失效的处理 1.3、迭代器失效总结 2、深浅拷贝问题 1、vector迭…

GeoServer发布数据进阶

GeoServer发布数据进阶 GeoServer介绍 GeoServer是用于共享地理空间数据的开源服务器。 它专为交互操作性而设计&#xff0c;使用开放标准发布来自任何主要空间数据源的数据。 GeoServer实现了行业标准的 OGC 协议&#xff0c;例如网络要素服务 &#xff08;WFS&#xff09;…

Java【优先级队列】模拟实现 + 【PriorityQueue】介绍

文章目录一、什么是优先级队列二、模拟实现1, 实现堆的基本操作1.1, 创建堆1.2.1, 向下调整1.2, 堆的插入1.2.1, 向上调整1.2, 堆的删除2, 实现优先级队列2.1, offer -- 插入数据2.1, poll -- 删除数据三、Java提供的PriorityQueue1, PriorityQueue说明2, 使用PriorityQueue2.1…

【Linux】安装Tomcat教程

目录 1.上传安装包 2.解压安装包 3.启动Tomcat 4.查看启动日志 5.查看进程 6.开放端口 7.停止Tomcat 1.上传安装包 使用FinalShell自带的上传工具将Tomcat的二进制发布包上传到Linux(与前面上传JDK安装包步骤 一致)。 2.解压安装包 将上传上来的安装包解压到指定目录…

2023年想跳槽,什么类型的人才需求最多?

某招聘网站资深HR对此表示&#xff1a;纵观当前招聘市场&#xff0c;无论是比较火爆的互联网行业还是传统行业&#xff0c;技能型人才都是最受欢迎的人才之一&#xff1b;那些拥有职场一技之能的跳槽者往往跳的结果更好&#xff0c;包括薪酬水平和发展空间、重视程度等。 那选择…

一个更适合Java初学者的轻量级开发工具:BlueJ

Java是世界上最流行的编程语言之一&#xff0c;它被广泛用于从Web开发到移动应用的各种应用程序。大部分Java工程师主要是用IDEA、Eclipse为主&#xff0c;这两个开发工具由于有强大的能力&#xff0c;所以复杂度上就更高一些。如果您刚刚开始使用Java&#xff0c;或者您更适合…