当前位置:   article > 正文

Flutter(二)第一个Flutter应用_开发第一个fullter 应用

开发第一个fullter 应用

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> {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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),
      ),
    );
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

调用

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

每一个 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;
        }),
      ),
    );
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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();
  • 1
  • 2
  • 3
  • 4

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

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

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

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

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

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

通过GlobalKey获取State对象

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

1.给目标StatefulWidget添加GlobalKey。

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

2.通过GlobalKey来获取State对象

_globalKey.currentState.openDrawer()

  • 1
  • 2

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) {
    // 实现绘制
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

LeafRenderObjectWidget 继承 RenderObjectWidget 继承Widget

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

Flutter SDK内置组件库介绍

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

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

import 'package:flutter/widgets.dart';
  • 1

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';
  • 1

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: () {}
        ),
      ),
    );
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

3.状态管理

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

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

全局状态管理 如 Provider、Redux

路由管理

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

//导航到新路由   
        Navigator.push( 
          context,
          MaterialPageRoute(builder: (context) {
            return NewRoute();
          })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

MaterialPageRoute

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

MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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 ])
  • 1
  • 2
  • 3
  • 4
  • 5

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

// 打开`TipRoute`,并等待返回结果
          var result = Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return TipRoute(
                  // 路由参数
                  text: "我是提示xxxx",
                );
              },
            ),
          );
          //输出`TipRoute`路由返回结果
          print("路由返回值: $result");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 新页面点击返回键并回传数据
Navigator.pop(context, "我是返回值")
  • 1

命名路由

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

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);
   },
 }, 
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

通过路由名打开新路由页
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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

路由生成钩子

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

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

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

各个字段的意义:

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
  • 1
  • 2

之后执行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   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

资源管理(assets)

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

//asset的实际目录可以是任意文件夹
flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png
  • 1
  • 2
  • 3
  • 4
  • 5

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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果要给我们的应用设置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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

渲染树

如果我们尝试调试布局问题,那么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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

当调试布局问题时,关键要看的是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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

要找出相对于帧的开始/结束事件发生的位置,可以切换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

  • 1
  • 2

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));
  } 
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

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');
    },
  ),
 );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/926657
推荐阅读
相关标签
  

闽ICP备14008679号