因为接下来去日本工作的话,更多需要的是Flutter和getX技术栈。这俩项我并不够熟悉,接下来大约不到三个月时间做一些技术和语言准备,所以做一些学习整理于此。

我更多依然使用自己习惯的书评方式记录,对自己技术提高最有效。

《Flutter实战.第二版》

网络Link: https://book.flutterchina.club/

移动开发发展史

  • 原生开发:使用OC,Swift。跨平台性差,开发成本高,但性能最好,底层调用方便。
  • 跨平台技术:
    • 使用H5原生(微信小程序)。代码实际就是运行在WebView的沙盒里,系统底层权限不足。原生实现功能提供API给JS,将API通过WebView注册给JsBridge,再用JS调用。因为WebView限制,性能会有损失。
    • JS开发配合原生渲染(ReactNative,Weex)。React提出的响应式编程,就是当状态改变,UI随之自动改变。实现机制就是,当状态改变时,会通知到一个抽象虚拟DOM层,它会记录本帧修改的全部改动,然后批量通知渲染DOM树进行update。其实这里的DOM树就是远程组件组成的DOM树。因为这些是JS开发,人员容易找一些。另外底层是原生渲染,性能会好很多。但是依然需要JS和原生进行通讯,会有额外开销。JS作为脚本语言,需要解释执行,运行时性能略低。
    • 自绘UI(Qt, Flutter).这就是游戏的做法,使用自己的渲染引擎,系统底层渲染API绘制UI,不是使用原生UI组件,所以保证UI绝对一致,也不存在版本区别。但是必然是编译包,不会像JS那样解释运行。
    • Flutter:支持热加载,解释运行。但是发包时,又是编译后的,兼顾了开发调试速度,和运行时的执行速度。

Flutter简介和框架

简介

如上面所说,Flutter既没用 webView ,也没用系统原生的UI,它是自己用了一套渲染引擎,所以对于跨平台一致性确实很好,底层用的Skia作为2D绘图引擎。

简单看了下Skia,就是个C++开发的跨平台引擎,底层还是老老实实OpenGLES这些,不同平台略有不同而已。接口也是常规的 drawText , drawLine, drawImage 这套,嗯,想不通google为啥费用这个,官方说明也没有看出任何特色。

所以简而言之,flutter 底层用的就是个标准的C++的2D渲染引擎。然后它本身用Dart语言开发,这个语言也没看出很强的特色,只有一点,支持运行时解释,类似JS;又支持预编译AOT,类似C++这些。勉强比C++配合Lua统一一些;因为有google Chrome V8团队支持,内存分配管理也比较可靠一点。

反正个人没觉得好到哪儿,只能说是有金主支持而已。

框架

上层Framework: Dart语言编写。包括:Foundarion, Animation, Painting, Gestures底层支持。中间渲染层 Rendering 负责管理渲染树, 上层封装一些widgets便于使用的UI。最后封装Material, Cupertino俩套风格模板。 中层Engine: C++语言编写,也就是Skia。包括的主要就是 dart 支持,帧渲染,渲染管道,资源管理,消息事件等常规2D渲染引擎的事。 下层:平台袭来,就是一些简单的渲染接口DX/OpenGL,线程,事件,文件打包等。

基本使用Flutter

  • 下载flutterSDK: https://flutter.dev/docs/development/tools/sdk/releases 需要的话设置环境变量
  • 检查flutter: 命令行执行 flutter doctor
  • 通常上面命令会报错: Android toolchain 有问题,于是需要安装 Android studio,这个IDE会顺道下载SDK版本依赖等。
    • 安装 Android studio,需要额外 File>Settings>Plugins 中安装flutter插件和dart插件
    • 然后创建 flutter 项目,一般在Android studio右侧需要对设备做一些操作,创建设备,启动设备,然后再执行程序,即可再虚拟手机设备中运行 flutter 了。
    • 建议安装 Android studio 运行顺利之后,再考虑使用 VSCode 进行开发调试。

默认的flutter项目:计数器

  • 入口代码在 lib/main.dart 里。 dart void main() => runApp(MyApp()); // 其实就是 下面代码 的语法糖 void main(){ runApp(MyApp()); }
  • 一个简单的Widget如下 ```dart // MyApp 就是一个 widget class MyApp extends StatelessWidget { // Widget 一般用 build 方法来构建UI界面 @override Widget build(BuildContext context) { return MaterialApp( //应用名称
    title: ‘Flutter Demo’, //应用首页路由 , MyHomePage 也是一个 widget home: MyHomePage(title: ‘Flutter Demo Home Page’), ); } }

class MyHomePage extends StatefulWidget { MyHomePage({Key? key, required this.title}) : super(key: key); final String title;

@override _MyHomePageState createState() => _MyHomePageState(); }

class _MyHomePageState extends State { … }

- Widget分为俩类,statefulWidget代表内部有状态更变;statelessWidget是一个无状态的简单widget
  - statefulWidget必然有俩部分组成,一个是继承于 stateFulWidget 的类,一个是state类。stateFulWidget类基本不变,其变化的状态都是放在state类里面,连build方法一般也都在state类里面了。
- state类如下
```dart
int _counter = 0; //用于记录按钮点击的总次数

// 下面的函数,就是先自增 _counter ,然后调用 setState() 函数通知发生了状态改变,然后底层会调用 build() 函数来更新UI。
void _incrementCounter() {
  setState(() {
     _counter++;
  });
}

Widget build(BuildContext context) {
  return Scaffold(  // 这是一种手脚架widget,自带导航栏,标题和主屏幕body
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(   // 主屏幕body里面放置了一个类型为 Center 的 Widget
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('You have pushed the button this many times:'),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton( // 主屏幕body又添加了一个在右下角落的一个悬浮类型的按钮
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ), 
  );
}

Widget简介

在flutter中,万物都是Widget,这些widget也被翻译为“组件,控件”,但它不仅仅表示某个UI对象,甚至手势检测对象,一个主题风格对象,一个文本样式,一种对齐方式 都被设计为Widget。所以,可以简单的认为它类似有些游戏引擎里的 Object就好。

下面是官方 Widget 类的声明:

@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key? key;

  @protected
  @factory
  Element createElement();

  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;

  @override
  @nonVirtual
  int get hashCode => super.hashCode;

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

其中需要关注几点:

  • 这个类是 @immutable 不可变类型的。即,该类类的变量被迫全部都是 final 类型的,即不可变类型,不允许属性发生变化。一旦 widget 中属性变化,则需要重新创建构造 widget 树,即创建了一个新的 widget 对象。
  • 该类继承自 DiagnosticableTree ,这是个诊断树,可以提供一些调试信息。
  • Key 这个变量,可以参考 CanUpdate() 函数,相当于这个 widget 对象的唯一值;该值更变,就会重新创建一个新的 widget,而非复用之前的 widget。
  • 上面的 Widget 类有个 createElement 函数。一个 Widget 允许包含一个或多个 Element(??)

实际开发中,我们都是继承 statefulWidget 或 statelessWidget 类来实现自己的组件,而不会直接继承自 Widget 类,这里了解到这里即可。

flutter的四棵树

  • Widget树:即我们代码中 Widget 的层级关系
  • Element树:引擎会根据我们的 Widget 树去生成 Element 树。element和widget基本是一一对应的,它算是 Widget -> Render 对象转换的一个中间产物。
  • Render树:引擎会根据 Element 树去生成 RenderObject 树;这里要注意,有些 element 未必对应一个 renderObject ,因为例如一个抽象的空 StatefulWidget 只要没有自身需要渲染的,就不会产生 RenderObject 对象。RenderObject更对应的是渲染引擎的对象,例如一个图片,一个文字,一个像素等。
  • Layer树:引擎会根据 Render 树去生成Layer树。这个多半是渲染引擎自己负责的遮挡剔除关系的树,用来减少重绘区,提高性能的。

总之,后面三棵树都不用我们操心,引擎自己负责。

StatelessWidget类

这个类比较简单,没有需要额外的状态。基本的一个实现类如下:

class Echo extends StatelessWidget  {
  const Echo({
    Key? key,  
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

这里可以看到这个 Echo 类里有俩个变量,构造时,强制传入一个 text 变量。然后它就在屏幕中间显示一行文字。

使用时如下:

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

这个类很简单,但是很实用,大部分自己扩展的 Widget 都基于这个类变形而来。

Context对象

Widget构造的核心函数 build() 有个参数 context,它记录的是 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>();
          // 直接返回父级 widget 的  appBar 的title。 此处实际上就是 Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

StatefulWidget类

该类类似于 StatelessWidget 类,但额外多留了一个接口 createState() 用来创建 State 对象。值得注意的是,如果一个 StatefulWidget 类被创建多次,那么 createState() 类也会被调用多次,例如我们将一个 widget 插入到 Widget 树多个位置时,就会创建多次 createState() .

State对象

一个 StatefulWidget 类会对应一个 State 类,该State类会保存了 StatefulWidget 要维护的状态,该状态可以: - 在 Widget 构建时被读取 - 可以在 StatefulWidget 生命周期中被改变,当 State 被改变时,可手动调用 setState() 方法通知 flutter 调用 build 方法重新构建 widget 树,以实现更新UI的目的。

一个 State 对象中有俩个属性: - 一个是 Widget ,就是自己绑定关联的 Widget 实例。 - 一个是 context ,类似 StatelessWidget 类的 BuildContext,也是访问 Widget 上下文所用。

例如:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;  // 这里依然只能 final?

  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

//----------------------------------------------------------------

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    //初始化状态
    _counter = widget.initValue;        // 这里我们可以发现,state可以直接访问关联 widget 属性。
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed: () => setState(    // 手动调用  setState() ,也可以单独封装一个函数,更容易理解
            () => ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget ");
  }

  @override
  void deactivate() {
    super.deactivate();
    print("deactivate");
  }

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

上面的代码执行后可以看到控制台中输出的Log为:

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

通过该LOG,可以得知,首先会调用 initState() 函数,该函数显然可以用来初始化各种变量。然后,我们点击热重载按钮,我们得到控制台中的新Log如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget 
I/flutter ( 5436): build

此时我们可以发现 initState() 函数和 didChangeDependencies() 函数没有被调用,但被调用了 didUpdateWidget() 函数。

如果此时移除 CounterWidget() 函数,则会发现Log如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

于是我们得到结论为: - 当 Widget 第一次插入到 Widget 树时会调用 initState() 函数,这里一般会执行一次初始化操作。例如,变量值的初始化,时间订阅等。但注意:此时 Widget 树还没有建成,不能使用 BuildContext 获取上下文。 - build() 函数出现在: - initState() 之后 - didChangeDependencies() 之后 - reassemble() 这仅仅是提供给热重载时调用,在正式发布 Release 模式时不会被调用。 - 当 Widget 被移除时,会顺序调用 deactivate() 和 dispose() 函数 - 当 Widget 在 Widget 树中发生更变时,会调用 didUpdateWidget() 函数

Widget类和State类互相访问

State类中访问Widget类对象很容易,上面代码演示了,直接使用 widget 变量即可。如下:

class _CounterWidgetState extends State<CounterWidget> {
  @override
  void initState() {
    super.initState();
    _counter = widget.initValue;    // 这里就已经访问到了关联的 widget
    print("initState");
  }

  //....
}

反之,要在 Widget 类中访问 State 类对象,也可以直接在 createState() 时直接获取,如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  @override
  _CounterWidgetState createState() => _CounterWidgetState();   //  这里就已经是 State 对象了
}

class _CounterWidgetState extends State<CounterWidget> {
    // ...
}

但如果我们需要在 State 中访问父类的 State 对象,则需要如下方式:

  • 1. 通过 Context 获取父类 State 对象
class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}
  • 2. 通过 of 获取父类 State 对象,这种方式不是绝对通用的,仅仅是一种开发者的默认约定。
Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
      // 直接访问调用
      Scaffold.of(context).showSnackBar(
        SnackBar(content: Text("我是SnackBar")),
      );
    },
    child: Text('打开抽屉菜单2'),
  );
}),
  • 3. 还有一种使用 GlobalKey 的方式来获取,步骤分为两步:
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
//...
Scaffold(
    key: _globalKey , //设置这个Widget的key
    //...  
)

然后,实际使用时可以直接使用这个 globalKey 如下:

_globalKey.currentState.openDrawer()        // 直接获取 state (注意:如果是 statelessWidget 则会失败)
_globalKey.currentWidge.xxx()               // 还可以直接获取对应的 Widget

注意的是:使用GlobalKey有额外开销;且一个GlobalKey必须全树唯一,不可重复。

自定义Widget

上面介绍比较多的 StatelessWidget 和 StatefulWidget 是用于组合其他组件的,本身并不存在 RenderObject。但实际上,大部分的 flutter 组件库中基础组件都不是通过这俩类实现的,例如 Text, Column, Align 等都是继承自RenderObject,所以如果我们要实现自定义 Widget,建议继承自 ReanderObject。

下面是一个自定义Widget类样例:

// RenderObject包装 Widget
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) {
    // 实现真正的绘制
  }
}

从上面代码可知,实现一个自定义的 Widget 有以下三个步骤。

  • 1, 上述代码中,LeafRenderObjectWidget 类继承自 RenderObjectWidget。如果自定义 Widget 包含子组件,也可以根据子组件的数量来选择继承 SingleChildRenderObjectWidget 或 MutilChildRenderObjectWidget。

  • 2, 继承这些建议的 RenderObjectWidget 后,我们需要重写其 createRenderObject() 方法,我们只需实现 createRenderObject() 函数所返回的渲染对象即可。

  • 3, 接下来 createRenderObject() 的渲染对象,即样例中的 RenderCustomObject 类继承自 RenderBox ,该类继承自 RenderObject ,我们只需在其中实现 布局,绘制,事件响应 逻辑即可。

Flutter常见的Widget库组件

  • 1,先前提到过,底层的基础组件就是
import 'package:flutter/widgets.dart';

这其中包含有底层组件 > Text: 带格式的文本组件 > Row, Column: 常规的布局类组件 > Stack: 允许堆叠的布局组件,可以通过 Positioned 来进行绝对位置定位,更类似游戏开发 > Container: 矩形容器类组件,一般包含个背景,边框,填充,大小约束等常见功能

  • 2, Material组件
import 'package:flutter/material.dart';

这其中包含了一些常见组件,包括有 > TextButton > Scaffold > AppBar

  • 3, Cupertino组件,就是iOS风格的组件,比起Material组件还没有那么完善。
import 'package:flutter/cupertino.dart';

状态管理简介

StatefulWidget中的状态管理常见方式

通常有以下四种方式: - Widget内部管理自己的状态 : - 界面外观效果状态,例如颜色,动画等 - 父Widget管理子Widget状态 : - 通常是用户数据,例如 复选框是否选中,滑块的位置等状态就直接给父Widget管理。 - 若一个状态由多个Widget共享,最好由于他们共同的父Widget管理。 - 父Widget和子Widget混合管理状态 : - 若部分状态为UI表现外观状态,部分为用户数据状态,就可以分别保存在父Widget和子Widget中管理。 - 全局状态管理: - 例如程序使用的语言,这种状态属于全局状态,影响全部Widget,此时可以实现一个全局事件总线类,将语言状态更变对应为一个事件,然后在各个Widget中的 initState() 函数中订阅该事件。一旦发生了语言状态更变,则订阅该事件的Widget即可在收到通知处调用 setState() 方法,然后内部会调用 build() 函数进行更新。 - 这个在后面的 Provider 功能和 全局事件总线 功能中会细解。

样例

例如:如果我们点击一个TapBox,该盒子背景色在绿色和灰色之间进行切换,此时我们记录一个状态 _active ,为 true 时为绿色;为 false 时则为灰色。

  • 若 Widget 管理自己的状态,则代码如下:

自身继承 StatelessWidget 类,负责管理状态变量 自身负责 setState() 事件 自身接收UI消息,调用 setState() 处理即可

class TapboxA extends StatefulWidget {
  TapboxA({Key? key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false; // 自身的状态

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector( // 手势检测
      onTap: _handleTap,    // 点击事件,修改状态
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

  • 若由父Widget管理子Widget状态,则代码如下

其中父类继承 StatefulWidget;子类继承 StatelessWidget 类,不负责状态变量管理 父类管理状态变量 父类负责 setState() 事件 子类留有 final 状态对象,由父类进行update 子类接收UI消息,通过事件回调交给父类处理,父类去调用 setState() 处理即可

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false; // 父类管理着状态

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(   // 父类内留有子类
        active: _active,
        onChanged: _handleTapboxChanged,  // 子类被点击时回调到父类的函数中
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

// 子类直接继承 StatelessWidget 类,因为自身状态都给了父类管理
class TapboxB extends StatelessWidget {
  TapboxB({Key? key, this.active: false, required this.onChanged})
      : super(key: key);

  final bool active;  // 这里仅仅是一个final类型
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);   // 将事件通知到父类
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,  // 点击事件,直接回调给父类去处理
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}
  • 若 Widget 中部分状态为UI状态,部分为用户数据状态,则分开保存如下:
//---------------------------- ParentWidget ----------------------------

class ParentWidgetC extends StatefulWidget {
  @override
  _ParentWidgetCState createState() => _ParentWidgetCState();
}

class _ParentWidgetCState extends State<ParentWidgetC> {
  bool _active = false;   // 父类依然保存 _active 用户数据状态

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {  // 子类被迫依然使用 StatefulWidget,因为内部有自己的highlight状态
  TapboxC({Key? key, this.active: false, required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;
  
  @override
  _TapboxCState createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;  // 子类管理 highlight 效果状态

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active); // 通知到Widget,传递给父类
  }

  @override
  Widget build(BuildContext context) {
    // 在按下时添加绿色边框,当抬起时,取消高亮  
    return GestureDetector(
      onTapDown: _handleTapDown, // 处理按下事件
      onTapUp: _handleTapUp, // 处理抬起事件
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        child: Center(
          child: Text(
            widget.active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

路由管理简介

路由 Route 在移动开发就是指页面的跳转。在Android和iOS里面都是维护一个路由栈,进行入栈打开一个页面,出栈对应一个页面的关闭。

flutter对于页面路由跳转有如下三种方式:

  • MaterialPageRoute: 最简单的上下俩个页面切换
  • Navigator 的匿名路由切换: 相对复杂一点点,容易理解和使用。但因为需要页面之间的关联,需要多处import,会使代码比较分散。
  • Navigator 命名路由: 功能强大,最为推荐,但需要维护路由表,使用略麻烦一些。

MaterialPageRoute页面切换组件

MaterialPageRoute样例

// 创建一个简单的新页面
class NewPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New page"),
      ),
      body: Center(
        child: Text("This is new page"),
      ),
    );
  }
}

// 在主页面添加一个跳转按钮如下

{
  // ... 无关代码
    TextButton(   
      child: Text("open new page"),
      onPressed: () {
        //导航到新页面  
        Navigator.push( 
          context,
          MaterialPageRoute(builder: (context) {
            return NewPage(); // 这里进行跳转到新页面
          }),
        );
      },
    ),
}

MaterialPageRoute参数解释

这个 MaterialPageRoute 类就是负责页面跳转管理的类,该函数参数比较重要如下:

  MaterialPageRoute({
    WidgetBuilder builder,        // 这里要传入的就是一个 Widget 对象,就是新页面的实例
    RouteSettings settings,       // 页面的配置信息,包括页面名称,是否是初始页面(首页)等
    bool maintainState = true,    // 默认情况下,当入栈(打开)一个页面时,先前的页面仍然会被保存在内存中,如果确定前一个页面没有用,想释放其中的资源,可设置该值为 false
    bool fullscreenDialog = false,// 新页面是否是一个全屏的模态对话框(即非一个标准页面)
  })

Navigator 类则管理整个页面路由栈,其中。手机屏幕中显示的就是栈定的页面。它的功能很强大,这里记录俩个最常用函数:

  • Future push(BuildContext context, Route route);

该函数负责将参数中的页面进行入栈(即显示到屏幕),返回值是一个 Future 对象,用来接收新页面出栈(即关闭)时的返回数据。

  • bool pop(BuildContext context, [result]);

该函数将栈顶的页面出栈(即关闭), result 即当页面关闭时,返回给上一个页面的返回数据。

  • 还有一些其他功能函数,例如 Navigator.replace, Navigator.popUntil 等。

有些时候,我们打开一个新页面时,需要带一些参数给新页面。

例如打开一个商品细节信息页面,需要传递商品ID到下一个页面;

同时,我们可能需要在新页面中做一些操作后,还要将一些新页面中的数据,传递给上一个页面。

例如新页面中填写了用户信息,用户点了 完成 按钮,页面返回上一页,上一页中需要用户确认信息是否正确。

此时我们需要新页面和老页面之间有数据传递,此时可样例代码如下:

//---------------------------------- 子页面 ----------------------------------
class TipRoutePage extends StatelessWidget {
  TipRoutePage({
    Key key,
    required this.text,  // 构造函数处,添加一个text参数,作为接收参数
  }) : super(key: key);
  final String text;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("提示"),
      ),
      body: Padding(
        padding: EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(text),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),  // 按了这个按钮会返回上一个页面
                child: Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

//---------------------------------- 父页面 ----------------------------------
class RouterTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        onPressed: () async {
          // 打开`TipRoutePage`,并等待返回结果
          var result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return TipRoutePage(
                  // 传递的路由参数
                  text: "我是提示xxxx",
                );
              },
            ),
          );
          //输出`TipRoutePage`路由返回结果
          print("路由返回值: $result");
        },
        child: Text("打开提示页"),
      ),
    );
  }
}

命名路由

命名路由就是为每个页面设置一个名字,然后通过这个名字就可以直接打开新的页面,该方式逻辑上最直观简单。

路由表

要使用命名路由,就需要先注册一个路由表,即页面名字和页面之间的关系表,如下:

Map<String, WidgetBuilder> routes;
注册路由表
MaterialApp(
  //注册路由表
  routes:{
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
   "new_page1":(context) => NewPage1(),
   "new_page2":(context) => NewPage2(),
  } 
);
通过路由名打开新页面
onPressed: () {
  Navigator.pushNamed(context, "new_page1");
},
命名路由传递参数
// 注册路由表
 routes:{
   "new_page3":(context) => EchoRoute(),
  } ,

// 路由页面代码
class EchoRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var args = ModalRoute.of(context).settings.arguments;   //获取路由参数
    //...省略无关代码
  }
}

// 打开该页面的代码
Navigator.of(context).pushNamed("new_page3", arguments: "hi");

对于不想修改页面代码,但又想使用命名路由且传递参数的,可以使用如下代码:

// 注册路由表
 routes:{
   "new_page3":(context){
    return TipRoutePage(text: ModalRoute.of(context)!.settings.arguments);
   }
  } ,

// 路由页面代码
class TipRoutePage extends StatelessWidget {
  TipRoutePage({
    Key key,
    required this.text,  // 构造函数处,添加一个text参数,作为接收参数
  }) : super(key: key);
  final String text;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("提示"),
      ),
      body: Padding(
        padding: EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(text),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),  // 按了这个按钮会返回上一个页面
                child: Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

包管理简介

在 java 里有.jar包, Web开发有npm包,Flutter中使用的包管理工具则是 pubspec.yaml 文件来管理第三方依赖包,使用的仓库则是 Pub http://pub.dev/ 这个Google官方的Dart packages仓库(类似node的npm库),我们可以在 Pub 网站里找到我们需要的包或者插件,然后在 pubspec.yaml 文件中添加自己的依赖项。

简单示例
# 应用或者包名称
name: flutter_in_action
# 应用或者包简介
description: First Flutter Application.
# 应用或者包的版本号
version: 1.0.0+1

# 依赖的包或插件
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

# 开发环境所依赖的工具包(并非flutter应用自身所依赖的包,不会发布到最终的安装包中)
dev_dependencies:
  flutter_test:
    sdk: flutter
    
# flutter相关的配置选项
flutter:
  uses-material-design: true

例如我们现在使用 getX 这个依赖项,打开 pub.dev ,找到 get 包 的网址如下:https://pub.dev/packages/get ,打开后发现当前最新版本是 4.7.2 ,也支持 flutter,于是我们修改 pubspec.yaml 文件,将该依赖添加到配置文件中

dependencies:
  flutter:
    sdk: flutter
  get: ^4.7.2 # 新添加的依赖
  cupertino_icons: ^0.1.2

然后,在Android Studio的IDE中,打开 pubspec.yaml 文件,右上角有个 Pub get 按钮,点击后,该依赖包会被安装到我们的项目中,控制台中我们可以看到以下内容

flutter packages get
Running "flutter packages get" in flutter_in_action...
Process finished with exit code 0

当然,如果是使用 VSCode 或其他IDE,则可在控制台中,进入工程目录,手动运行 flutter package get 命令来手动下载依赖项。

然后,只需在代码中引用该包即可。

import 'package:get/get.dart';
引用本地包

假设我们本地硬盘上有个包角 pkg1, 则可修改 pubspec.yaml 文件,下面的 path 可以是绝对路径,也可以是项目相对路径

dependencies:
    pkg1:
        path: ../../code/pkg1
引用Git包

假如我们需要引用一个 Git 仓库的包,可以修改 pubspec.yaml 文件如下

dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git

如果我们需要引用的包,甚至不是 Git 仓库根目录,只是其中某路径下的包,则可以修改 pubspec.yaml 文件如下

dependencies:
  package1:
    git:
      url: git://github.com/flutter/packages.git
      path: packages/package1 

资源管理简介

和包管理一样,Flutter也使用 pubspec.yaml 来管理程序所需资源,样例格式如下

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

但值得注意的是,flutter支持资源变体,即,在 pubspec.yaml 的assets部分指定资源路径时,构建过程中,会在相邻子目录下查找具有相同名称的文件,该文件也会被包含在 asset bundle 中。例如我们有如下两个文件:

../graphics/background.png ../graphics/dark/background.png

此时,我们只需在 pubspec.yaml 文件中包含

flutter:
  assets:
    - graphics/background.png

就会将上面两个 background.png 都打包到 asset bundle 中。前者 graphics/background.png 会视为主资源,而后者 graphics/dark/background.png 会被视为一种变体。

这种机制在匹配设备分辨率时使用到。

Assets的加载

加载文本Assets
  • 可以使用 rootBundle 对象加载。

每个 flutter 应用程序都有一个 rootBundle 对象,可以用该对象访问著资源包,可以直接使用 package:flutter/services.dart 中的全局静态 rootBundle 对象来加载资源即可。

import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}
  • 可以使用 DefaultAssetBundle 对象加载。

可以使用 DefaultAssetBundle 来获取当前 BuildContext 的 AssetBundle。该方法不是使用程序默认构建的 AssetBundle,而是使用父 Widget 运行时的 AssetBundle,可用于测试。

加载图片
  • 声明分辨率相关的图片Assets

AssetImage 可以将 Asset 的加载请求映射到最接近当前设备像素比例dpi的资源。它要求资源必须按照特定目录方式来保存,例如:

../my_icon.png ../1.0x/my_icon.png ../2.0x/my_icon.png

然后在 pubspec.yaml 的 assets 区域添加该资源,最后进行图片加载

Widget build(BuildContext context) {
  return DecoratedBox(
    decoration: BoxDecoration(
      image: DecorationImage(
        image: AssetImage('../my_icon.png'),  // 该图片会被动态替换
      ),
    ),
  );
}

这里需要注意的是,AssetImage 不是一个 Widget,它是一个 ImageProvider 对象,如果需要一个 Widget 对象,可如下使用 Image.asset() 函数:

Widget build(BuildContext context) {
  return Image.asset('graphics/background.png');
}
  • 加载第三方包内的资源图片

要加载依赖包中的图像,必须给 AssetImage 提供的 Package 参数。例如,如果我们的应用程序依赖于一个名为 “ThirdPart” 的包,该第三方包具有如下目录结构:

../pubspec.yaml ../icons/logo.png ../icons/1.5x/logo.png ../icons/2.0x/logo.png

我们加载该图像,可以使用如下代码:

AssetImage('icons/logo.png', package: 'ThirdPart')

// 或

Image.asset('icons/logo.png', package: 'ThirdPart')
  • 访问第三方包中的依赖包资源图片

如果我们的项目,依赖一个 “ThirdPart” 的包,包内又依赖了一个lib,该“ThirdPart” 的包文件结构如下:

../lib/icons/background1.png ../lib/icons/background2.png

要使用该图片,需要在 pubspec.yaml 的 assets 部分声明如下:

flutter:
  assets:
    - packages/ThirdPart/icons/background1.png

注意:这路径里没有 lib/ 。

特殊资源:软件图标和启动ICON
  • Android软件图标: 替换flutter项目中的 …/android/app/src/main/res 目录下图片文件即可。
  • iOS软件图标:替换flutter项目中的 …/ios/Runner 目录下图片文件即可。
  • Android启动页: 替换项目中 …/android/app/src/main 下的 res/drawable/launch_background.xml,自定义drawable来自定义启动界面,或直接更换图片。
  • iOS启动页:替换flutter项目中的 ../ios/Runner 目录下 Assets.xcassets/LaunchImage.imageset 的图片即可。

Flutter代码调试

基本调试
  • 使用 debugger() 函数制作断点。这依赖于IDE支持。
import 'dart:developer'

void someFunction(double offset) {
  debugger(when: offset > 30.0);    // 该断点可选添加when参数。
  // ...
}
  • 使用 print(), debugPrint() 函数打印日志,许多类都有 toString() 实现。

  • 使用 assert() 函数进行断言。如果想关闭断言,可以使用 flutter run -release 运行,断言将无效。

  • 可以使用 IDE 上打断点。

Widget调试
  • 使用 debugDumpApp() 打印Widget树。如果是我们自己的Widget,则可覆盖 debugFillProperties() 函数来添加DEBUG信息。
class AppHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: TextButton(
          onPressed: () {
            debugDumpApp(); // 这里可以打印出 widget 树信息
          },
          child: Text('Dump App'),
        ),
      ),
    );
  }
}

打印出的日志信息如下

/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))
... #省略剩余内容
  • 使用 debugDumpRenderTree() 打印渲染树。如果是我们自己的Widget,则可覆盖 debugFillProperties() 函数来添加DEBUG信息。
import'package:flutter/rendering.dart';

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)
... # 省略
  • 使用 debugDumpLayerTree() 打印Layer层树。

打印出的层信息如下:

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
  • 使用 debugDumpSemanticsTree() 打印语义树。

打印出的信息如下:

I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :  ├SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :  │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter :  └SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :    └SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")
帧调用堆栈信息
  • 使用 debugPrintBeginFrameBanner() 和 debugPrintEndFrameBanner() 可打印 帧开启/结束 信息;这里还可以使用 debugPrintScheduleFrameStacks() 来打印当前帧的调用堆栈。
UI可视化调试
  • 可以设置 debugPaintSizeEnabled = true 来进行可视化调试
  • 或者开启 debugPaintBaselinesEnabled = true
  • 开启 debugPaintPointersEnabled = true
  • 开启 debugPaintLayerBordersEnabled = true
  • 开启 debugRepaintRainbowEnabled = true
  • 开启这些参数后,在屏幕上可以看到以不同颜色进行的特殊格子,文字的标识。
调试动画
  • 建议调整 scheduler 库中的 timeDilation 变量,从1.0例如调整到50.0,可以减慢动画速度。
调试性能问题
  • 可以使用 debugPrintMarkNeedsLayoutStacks 和 debugPrintMarkNeedsPaintStacks 标志,来查看每个渲染盒被重新布局和重新绘制信息。也可以使用 services 库中的 debugPrintStack() 函数来打印堆栈信息。
调试统计应用启动时间
  • 可以运行 flutter run –trace-startup –profile 来获取程序每个阶段的消耗时间 > 进入引擎时间 > 展示应用的第一帧时间 > 初始化Flutter框架时间 > 完成Flutter框架初始化时间

这些数据会被保存到 start_up_info.json 文件中。样例如下:

{
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}
根据部分代码块的执行时间

在代码块前后加入 dart:developer 库中的 Timeline 工具代码可以查看,例如:

Timeline.startSync('interesting function');
// YourFunc();
Timeline.finishSync();

此时建议使用 flutter run 时带有 –profile 标志,可以获得更精确的性能。

使用第三方IDE

可以使用类似 flutter DevTools 的可视化调试工具。

Flutter异常捕获

如果在 flutter 程序中有异常未被捕获,它不会像 Java 或 C++ 语言之类的程序发生崩溃。这源于 Dart 是一种单线程模型。

Dart单线程模型

Dart单线程模型是:

startApp() -> main() -> 检查Microtask队列,如果有Microtask,就执行Microtask;如果没有Microtask -> 就检查 Event 队列,如果没有Event就退出程序;如果有,则执行Event。

可见 Dart 单线程中是以消息循环机制运行的,其中包含了俩个任务队列,一个是Microtask队列,一个是Event队列,其中,Microtask队列的执行优先级高于Event队列。

一般外部事件,例如用户点击,计时器,绘制事件,IO操作等,都在Event队列中;而Dart内部的一些任务都在Microtask队列中,通常这些Microtask比较少。

一旦有一个任务出现了异常且未被捕获的话,则会导致当前任务的后续代码无法被执行,但不会影响其他任务以及主线程的正常执行的。

通常的异常捕获方法
  • 将捕获的错误弹出一个ErrorWidget,例如:
void func() {
 ...
  try {
    //执行build方法  
    built = build();
  } catch (e, stack) {
    // 有异常时则弹出错误提示  
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
  } 
  ...
} 
  • 将捕获的错误上报到报警平台,例如:
void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    reportError(details);
  };
 ...
}

我们在这里自己提供一个自定义的错误处理回调即可处理被捕获的异常了。

  • 完整的错误上报代码样例,例如:
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');
    },
  ),
 );
}

常用基础组件

文本和样式

文本Text

用来显示简单的样式文本,样例如下:

Text("Hello world",
  textAlign: TextAlign.left,        // 文本的对齐方式
);

Text("Hello world! I'm Jack. "*4,
  maxLines: 1,                      // 指定文本显示的最大行数
  overflow: TextOverflow.ellipsis,  // 指定文本的截断方式
);

Text("Hello world",
  textScaleFactor: 1.5,             // 指定文字的大小缩放比例
);
文本样式TextStyle

用来指定文本显示的样式,包括字体颜色,字体,粗细,背景等,样例如下:

Text("Hello world",
  style: TextStyle(
    color: Colors.blue,
    fontSize: 18.0,                           // 字体大小
    height: 1.2,  
    fontFamily: "Courier",                    // 字体集
    background: Paint()..color=Colors.yellow,
    decoration:TextDecoration.underline,
    decorationStyle: TextDecorationStyle.dashed
  ),
);
文本片段样式TextSpan

上面的 TextStyle 只能设置整个 Text 的样式,如果一个Text要分段显示不同的样式,就可以使用 TextSpan,样例如下:

Text.rich(TextSpan(
    children: [       // 下面是文本字段
     TextSpan(
       text: "Home: "
     ),
     TextSpan(
       text: "xxxx",
       style: TextStyle(
         color: Colors.blue
       ),  
     ),
    ]
))

从这里也可以看出, Text本身就是 RichText 的封装,所以其内允许有多个TextSpan对象,而TextSpan对象也允许有子TextSpan对象。

文本默认样式DefaultTextStyle

在Widget树中,文本的默认样式是可以被继承的,所以,如果在Widget树中某一节点设置一个默认字体样式,则该节点的子节点文本都会使用该默认样式,例如:

DefaultTextStyle(
  //1.设置文本默认样式  
  style: TextStyle(
    color:Colors.red,
    fontSize: 20.0,
  ),
  textAlign: TextAlign.start,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text("hello world"),
      Text("I am Jack"),
      Text("I am Jay",
        style: TextStyle(
          inherit: false, //2.不继承默认样式
          color: Colors.grey
        ),
      ),
    ],
  ),
);

改代码执行后,其中的“Hello world, I am Jack”都会受到上面的文本样式影响,呈现红色。但”I am Jay”则不受到文本样式影响,呈现灰色。

使用第三方字体
  • 首先需要在 pubspec.yaml 中声明第三方字体,可以确保字体被加载到应用程序中
flutter:
  fonts:
    - family: Raleway
      fonts:
        - asset: assets/fonts/Raleway-Regular.ttf
        - asset: assets/fonts/Raleway-Medium.ttf
          weight: 500
        - asset: assets/fonts/Raleway-SemiBold.ttf
          weight: 600
    - family: AbrilFatface
      fonts:
        - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
  • 然后可以使用 TextStyle 属性使用字体。
// 声明文本样式
const textStyle = const TextStyle(
  fontFamily: 'Raleway',
);

// 使用文本样式
var buttonText = const Text(
  "Use the font for this text",
  style: textStyle,
);
使用Package中的字体

假设该字体在 my_package 包中,则创建 TextStyle 的过程如下:

const textStyle = const TextStyle(
  fontFamily: 'Raleway',
  package: 'my_package', //指定包名
);

按钮

在 Material 组件库中提供了许多按钮组件,包括 ElevatedButton、TextButton、OutlinedButton 等,它们都有一个 onPressed 属性来设置点击回调。

  • ElevatedButton漂浮按钮
ElevatedButton(
  child: Text("normal"),
  onPressed: () {},
);
  • TextButton文本按钮
TextButton(
  child: Text("normal"),
  onPressed: () {},
)
  • OutlinedButton带边框按钮
OutlinedButton(
  child: Text("normal"),
  onPressed: () {},
)
  • IconButton图标按钮
IconButton(
  icon: Icon(Icons.thumb_up),
  onPressed: () {},
)
  • 上述按钮也都可以同时带图标带文字
ElevatedButton.icon(
  icon: Icon(Icons.send),
  label: Text("发送"),
  onPressed: _onPressed,
),
OutlinedButton.icon(
  icon: Icon(Icons.add),
  label: Text("添加"),
  onPressed: _onPressed,
),
TextButton.icon(
  icon: Icon(Icons.info),
  label: Text("详情"),
  onPressed: _onPressed,
),

图片以及Icon

在Flutter中,使用 Image 组件来加载并显示图片, Image 的数据源可以是 asset, 文件,内存以及网络。

ImageProvider

ImageProvider是一个抽象类,它有个重要的图片接口 load(),从不同的数据源获取图片需要实现不同的 ImageProvider。例如,AssetImage 可以从 Asset 中加载图片;NetworkImage 实现了从网络中加载图片。这都是 ImageProvider。

ImageWidget

ImageWidget 有一个必选的 Image 参数,该参数就是需要提供一个 ImageProvider。下面有俩个例子,分别演示如何从 asset 和 网络 加载图片。

  • 从 asset 中加载图片
    • 创建一个 image 目录,其中放置一个 avatar.png 图片。
    • 在 pubspec.yaml 中添加如下内容 ```yaml assets:
    • images/avatar.png ```
    • 代码中加载该图片 dart Image( image: AssetImage("images/avatar.png"), width: 100.0 ); // 或 Image.asset("images/avatar.png", width: 100.0, )
  • 从网络加载图片 dart Image( image: NetworkImage( "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"), width: 100.0, ) // 或 Image.network( "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4", width: 100.0, )
Image参数
const Image({
  ...
  this.width,   //图片的宽
  this.height,  //图片高度
  this.color,   //图片的混合色值
  this.colorBlendMode, //混合模式
  this.fit,     //缩放模式
  this.alignment = Alignment.center,  //对齐方式
  this.repeat = ImageRepeat.noRepeat, //重复方式
  ...
})

// 使用样例例如:

Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0,
  color: Colors.blue,
  colorBlendMode: BlendMode.difference,
  repeat: ImageRepeat.repeatY ,
);
ICON

Flutter中,可以像 Web 开发一样使用 iconfont,相较于 image,Icon有如下优势:

体积更小,安装包体积更小 是矢量图标,放大缩小是不会影响其清晰度的 可以使用文本样式,像文本一样改变图标的颜色,大小和对齐机制等 可以通过TextSpan和文本混合使用

使用MaterialDesign的字体图标

使用该 MaterialDesign 的字体图标,需要在 pubspec.yaml 中开启配置如下:

flutter:
  uses-material-design: true

然后就可以使用一些标准的icon,可以在官网查看: https://material.io/tools/icons/

例子如下:

String icons = "";
// accessible: 0xe03e
icons += "\uE03e";
// error:  0xe237
icons += " \uE237";
// fingerprint: 0xe287
icons += " \uE287";

Text(
  icons,
  style: TextStyle(
    fontFamily: "MaterialIcons",
    fontSize: 24.0,
    color: Colors.green,
  ),
);

// 也可以使用另一种方式调用如下:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.accessible,color: Colors.green),
    Icon(Icons.error,color: Colors.green),
    Icon(Icons.fingerprint,color: Colors.green),
  ],
)
使用自定义字体图标
  • 导入字体图标文件,如下 ```yaml fonts:
    • family: myIcon #指定一个字体名 fonts:
      • asset: fonts/iconfont.ttf ```
  • 定义一个类,将字体文件中的图标定义为静态变量 dart class MyIcons{ // book 图标 static const IconData book = const IconData( 0xe614, fontFamily: 'myIcon', matchTextDirection: true ); // 微信图标 static const IconData wechat = const IconData( 0xec7d, fontFamily: 'myIcon', matchTextDirection: true ); }
  • 使用该类 dart Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Icon(MyIcons.book,color: Colors.purple), Icon(MyIcons.wechat,color: Colors.green), ], )

单选开关和复选框

在 Material 组件库中,提供了单选开关 Switch 和复选框 Checkbox, 它们继承自 StatefulWidget,但本身不保存被选中状态,选中状态需要父组件管理。当 Switch 或 Checkbox 被点击后,会触发 onChanged() 回调,例如:

class SwitchAndCheckBoxTestRoute extends StatefulWidget {
  @override
  _SwitchAndCheckBoxTestRouteState createState() => _SwitchAndCheckBoxTestRouteState();
}

class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
  bool _switchSelected = true;    // 维护单选开关状态
  bool _checkboxSelected = true;  // 维护复选框状态
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Switch(
          value: _switchSelected,//当前状态
          onChanged:(value){
            //重新构建页面  
            setState(() {
              _switchSelected=value;
            });
          },
        ),
        Checkbox(
          value: _checkboxSelected,
          activeColor: Colors.red, //选中时的颜色
          onChanged:(value){
            setState(() {
              _checkboxSelected=value;
            });
          } ,
        )
      ],
    );
  }
}
  • Switch 和 Checkbox 属性比较简单,它们有如下一些属性:
    • activeColor属性,用于设置激活态的颜色
    • Switch 有宽度可以自定义
    • Checkbox 有一个 tristate 属性,标识是否为三态:若为三态,则对应的 value 包括 true, false, null 三种。

输入框

在 Material 组件库中,提供了输入框组件 TextField 和表单组件 Form。

  • 现在看一下 TextField 文本输入框的属性如下:
const TextField({
  ...
  TextEditingController controller,   // 编辑框的控制器,获取编辑器的内容,监视编辑文本改变事件
  FocusNode focusNode,                // 用于控制 TextField 是否占有当前键盘的输入焦点
  InputDecoration decoration = const InputDecoration(), // 控制 TextField 的外观,包括文本,背景,边框等
  TextInputType keyboardType,       // 枚举输入内容格式,例如多行文本,数字,电话,日期等
  TextInputAction textInputAction,
  TextStyle style,                  // 文本样式
  TextAlign textAlign = TextAlign.start, // 编辑框内文本在水平方向的对齐方式
  bool autofocus = false,           // 是否自动获取焦点
  bool obscureText = false,         // 是否隐藏正在编辑的文本,例如输入密码时,会用 * 替换。
  int maxLines = 1,                 // 输入框最大行数,若为null则表示无行数限制
  int maxLength,
  this.maxLengthEnforcement,
  ToolbarOptions? toolbarOptions,   // 长按或鼠标右击时出现的菜单
  ValueChanged<String> onChanged,   // 输入框内容发生更变时的回调函数
  VoidCallback onEditingComplete,   // 输入框输入完成时触发函数
  ValueChanged<String> onSubmitted, // 输入框输入完成时触发函数
  List<TextInputFormatter> inputFormatters, // 输入框指定的输入格式
  bool enabled, 
  this.cursorWidth = 2.0, // 光标样式
  this.cursorRadius,
  this.cursorColor,
  this.onTap,
  ...
})
  • 例如一个登陆表单,样例如下:
Column(
  children: <Widget>[
    TextField(
      autofocus: true,
      decoration: InputDecoration(
        labelText: "用户名",
        hintText: "用户名或邮箱",
        prefixIcon: Icon(Icons.person)
      ),
    ),
    TextField(
      decoration: InputDecoration(
        labelText: "密码",
        hintText: "您的登录密码",
        prefixIcon: Icon(Icons.lock)
      ),
      obscureText: true,
    ),
  ],
);
  • 获取输入内容

    • 第一种方式,定义俩个变量,用于保存用户名和密码,在 onChange() 函数中保存输入内容
    • 第二种方式,通过 controller 直接获取。
  • 使用 Controller 的方式,首先定义一个 controller

    TextEditingController _unameController = TextEditingController();
    
  • 然后,可以设置输入框 controller

    TextField(
    autofocus: true,
    controller: _unameController, //设置controller
    ...
    )
    
  • 最后,就可以通过 controller 获取输入框内容

    print(_unameController.text)
    
  • 监听文本变化

    • 第一种方式,设置 onChange() 回调,如下 dart TextField( autofocus: true, onChanged: (v) { print("onChange: $v"); } )
    • 第二种方式,使用 controller 监听,如下 dart @override void initState() { //监听输入改变 _unameController.addListener((){ print(_unameController.text); }); }
    • 这俩种方式相比, onChanged() 只能监听文本变化,但 controller 功能既可监听文本变化,还可以设置默认值和选择文本,如下: dart TextEditingController _selectionController = TextEditingController(); _selectionController.text="hello world!"; // 默认值 _selectionController.selection=TextSelection( baseOffset: 2, // 设置从第三个字符开始,默认选中后面的字符 extentOffset: _selectionController.text.length ); // 设置 controller TextField( controller: _selectionController, )
自定义输入框样式

我们可以通过 decoration 属性来定义输入框样式,样例代码如下

TextField(
  decoration: InputDecoration(
    labelText: "请输入用户名",
    prefixIcon: Icon(Icons.person),
    // 未获得焦点下划线设为灰色
    enabledBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.grey),
    ),
    //获得焦点下划线设为蓝色
    focusedBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.blue),
    ),
  ),
),

控制输入焦点

焦点可以通过 FocusNode 和 FocusScopeNode 来控制,我们可以通过 FocusNode.of(context) 来获取 Widget 树中默认的 FocusScopeNode。例如,下面一个示例,该示例中创建了俩个 TextField ,第一个自动获取焦点,然后创建俩个按钮:第一个按钮可将焦点从第一个 TextField 转移到第二个 TextField 上;第二个按钮可以关闭键盘。

class FocusTestRoute extends StatefulWidget {
  @override
  _FocusTestRouteState createState() => _FocusTestRouteState();
}

class _FocusTestRouteState extends State<FocusTestRoute> {
  FocusNode focusNode1 = FocusNode();
  FocusNode focusNode2 = FocusNode();
  FocusScopeNode? focusScopeNode;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: <Widget>[
          TextField(
            autofocus: true, 
            focusNode: focusNode1,//关联focusNode1
            decoration: InputDecoration(
                labelText: "input1"
            ),
          ),
          TextField(
            focusNode: focusNode2,//关联focusNode2
            decoration: InputDecoration(
                labelText: "input2"
            ),
          ),
          Builder(builder: (ctx) {
            return Column(
              children: <Widget>[
                ElevatedButton(
                  child: Text("移动焦点"),
                  onPressed: () {
                    //将焦点从第一个TextField移到第二个TextField
                    // 这是一种写法 FocusScope.of(context).requestFocus(focusNode2);
                    // 这是第二种写法
                    if(null == focusScopeNode){
                      focusScopeNode = FocusScope.of(context);
                    }
                    focusScopeNode.requestFocus(focusNode2);
                  },
                ),
                ElevatedButton(
                  child: Text("隐藏键盘"),
                  onPressed: () {
                    // 当所有编辑框都失去焦点时键盘就会收起  
                    focusNode1.unfocus();
                    focusNode2.unfocus();
                  },
                ),
              ],
            );
          },
          ),
        ],
      ),
    );
  }
}

表单Form

Form组件和输入框的区别是,它可以对输入框进行分组,然后做一些统一操作,例如输入内容的校验,输入框重置和输入内容保存。

Form类的结构

Form 类继承自 StatefulWidget 对象,它对应的状态类为 FormState ,其结构如下:

Form({
  required Widget child,
  bool autovalidate = false,  // 是否自动校验输入内容。若该值为true,则内容发生变化时自动校验合法性;若该值为false,则可通过 FormState.validate() 来手动校验
  WillPopCallback onWillPop,  // 决定 Form 所在的页面是否可以直接返回。该回调返回一个 Futurn 对象,若 Futurn 的最终结果是false,则该页面不会返回;若该值为 true,则返回到上一个页面
  VoidCallback onChanged,     // 当 Form 的任何一个子输入框FormField内容发生变化时会调用此回调
})
FormField类

Form的子元素必须是 FormField 类,该类定义如下:

const FormField({
  ...
  FormFieldSetter<T> onSaved, //保存回调
  FormFieldValidator<T>  validator, //验证回调
  T initialValue, //初始值
  bool autovalidate = false, //是否自动校验。
})

它有一个继承类 TextFormField ,可以更方便的使用。

FormState类

FormState 为 Form 的 State 类,可以通过 Form.of() 或 GlobalKey 获得。我们可以通过 FormState 来对Form的子 FormField 进行统一操作,例如:

  • FormState.validate() 此函数会调用 Form 的全部 FormField 的 validate 回调,如果有一个校验失败,则会返回 false.
  • FormState.save() 此函数会调用 Form 的全部 FormField 的 save 回调,保存表单内容.
  • FormState.reset() 此函数会将 Form 的全部 FormField 的内容清空.
样例
  • 一个用户名输入框,如果该值为空,则提示“用户名不能为空”
  • 一个密码输入框,若该值小于6位,则提示“密码不能少于6位”
  • 下方有个“登陆”按钮
import 'package:flutter/material.dart';

class FormTestRoute extends StatefulWidget {
  @override
  _FormTestRouteState createState() => _FormTestRouteState();
}

class _FormTestRouteState extends State<FormTestRoute> {
  TextEditingController _unameController = TextEditingController();
  TextEditingController _pwdController = TextEditingController();
  GlobalKey _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, //设置globalKey,用于后面获取FormState
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: <Widget>[
          TextFormField(
            autofocus: true,
            controller: _unameController,
            decoration: InputDecoration(
              labelText: "用户名",
              hintText: "用户名或邮箱",
              icon: Icon(Icons.person),
            ),
            // 校验用户名
            validator: (v) {
              return v!.trim().isNotEmpty ? null : "用户名不能为空";
            },
          ),
          TextFormField(
            controller: _pwdController,
            decoration: InputDecoration(
              labelText: "密码",
              hintText: "您的登录密码",
              icon: Icon(Icons.lock),
            ),
            obscureText: true,
            //校验密码
            validator: (v) {
              return v!.trim().length > 5 ? null : "密码不能少于6位";
            },
          ),
          // 登录按钮
          Padding(
            padding: const EdgeInsets.only(top: 28.0),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: ElevatedButton(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Text("登录"),
                    ),
                    onPressed: () {
                      // 通过_formKey.currentState 获取FormState后,
                      // 调用validate()方法校验用户名密码是否合法,校验
                      // 通过后再提交数据。
                      if ((_formKey.currentState as FormState).validate()) {
                        //验证通过提交数据
                      }
                    },
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

进度指示器

Material 组件库中提供了两种进度指示器: LinearProgressIndicator 和 CircularProgressIndicator。

LinearProgressIndicator

LinearProgressIndicator 是一个线性条状的进度条,其定义如下:

LinearProgressIndicator({
  double value,               // 当前进度,取值范围[0,1],若该值为null,则会循环播放动画
  Color backgroundColor,      // 进度指示器的背景色
  Animation<Color> valueColor,// 进度指示器的进度条颜色,该值是可Animation所以可以执行动画
  ...
})

使用样例如下:

// 模糊进度条(会执行一个动画)
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5, 
)
CircularProgressIndicator

CircularProgressIndicator 是一个圆形进度条,其定义如下:

CircularProgressIndicator({
  double value,
  Color backgroundColor,
  Animation<Color> valueColor,
  this.strokeWidth = 4.0, // 圆形进度条的粗细
  ...   
})

使用样例如下:

// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%,会显示一个半圆
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5,
),
自定义尺寸

如果我们希望线形进度条的线细一些,或者希望圆形进度条的圆大一些,我们可以将父容器的尺寸进行调整,用来调整进度条自身的大小,例如:

// 线性进度条高度指定为3
SizedBox(
  height: 3,
  child: LinearProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .5,
  ),
),
// 圆形进度条直径指定为100
SizedBox(
  height: 100,
  width: 100,
  child: CircularProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .7,
  ),
),
进度动画
import 'package:flutter/material.dart';

class ProgressRoute extends StatefulWidget {
  @override
  _ProgressRouteState createState() => _ProgressRouteState();
}

class _ProgressRouteState extends State<ProgressRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  @override
  void initState() {
    //动画执行时间3秒  
    _animationController = AnimationController(
        vsync: this, //注意State类需要混入SingleTickerProviderStateMixin(提供动画帧计时/触发器)
        duration: Duration(seconds: 3),
      );
    _animationController.forward();
    _animationController.addListener(() => setState(() => {}));
    super.initState();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
            Padding(
            padding: EdgeInsets.all(16),
            child: LinearProgressIndicator(
              backgroundColor: Colors.grey[200],
              valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
                .animate(_animationController), // 从灰色变成蓝色
              value: _animationController.value,
            ),
          );
        ],
      ),
    );
  }
}

布局类组件

基本简介

一个布局类组件可能包含一个或多个子组件,不同的布局类组件对自组建的排列方式不同,例如:

  • LeafRenderObjectWidget : 此类 Widget 不存在子节点。例如 Image 等基础组件都属于此类。
  • SingleChildRenderObjectWidget :此类 Widget 包含一个子 Widget,例如 ConstrainedBox, DecoratedBox 等
  • MultiChildRenderObjectWidget :此类 Widget 包含多个子 Widget,一般都有一个 children 参数,可接受一个 Widget 数组,例如 Row, Column, Stack 等组件。

这里提及的 布局类组件,都是继承自 SingleChildRenderObjectWidget 和 MultiChildRenderObjectWidget,它们通常都有一个 child 或 children 属性用来接收子 children。

盒布局模型

Flutter有俩种布局模型:

  • 基于 RenderBox 的盒模型布局
  • 基于 RenderSliver 按需加载列表布局

这俩种布局方式整体布局流程如下:

  • 1,上层组件向下层组件传递约束条件。
  • 2,下层组件确定自己的大小,然后告知上层组件。(注意,下层组件的大小必须符合父组件的约束)
  • 3,上层组件确定下层组件对于自身的偏移,来确定自身的大小。

其中,RenderSliver 布局模型在之后的可滚动模型种解释,这里主要说明 RenderBox 盒模型,它有如下特点:

  • 1,盒布局模型的所有对象,都继承自 RenderBox 类。
  • 2,在布局过程中,父级Widget传递给子Widget的约束信息由 BoxConstraints 描述。
BoxConstraints约束

该结构是盒模型布局过程中,父Widget渲染对象传递给子Widget渲染对象的约束信息,包括最大宽高信息等,子组件大小需要在约束的范围内,该结构如下:

const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})
ConstrainedBox约束

ConstrainedBox对象用于对子组件添加额外的约束。例如:如果你想让子组件的最小高度是80像素,我们可以使用 const BoxConstraints(minHeight: 80) 作为子组件的约束。例如:

// 定义一个 redBox, 不指定宽度和高度
Widget redBox = DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);

// 然后我们定义一个最小高度50像素,无限宽度的容器装在这个 redBox
ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
    height: 3.0, 
    child: redBox ,
  ),
)

此时,我们会得到一个高度为50像素的 redBox ,虽然我们定义这个 Container 高度为3像素,但最终高度却是50 像素;这正是 ConstrainedBox 的最小高度限制起了作用。

SizedBox约束

SizedBox 用于指定元素固定的宽高,如下:

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

但实际上 SizedBox 只是 ConstrainedBox 的一个定制,上面的代码就相当于:

ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

// 也就相当于

ConstrainedBox(
  constraints: BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)
  child: redBox, 
)
多重限制冲突

如果一个组件有多个父级 BoxConstraints 限制,那么最终生效的会是父子种数据较大的限制。例如:

// 限制1
ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), // 父
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),// 子
    child: redBox,
  ),
)

// 限制2
ConstrainedBox(
  constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), // 父
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), // 子
    child: redBox,
  )
)

这俩个限制的结果是相同的,最终的 redBox 大小都是 Width=90, Height=60。

UnconstrainedBox去除约束

盒布局模型的限制仅限于父子之间。即如果A的子组件是B,而B的子组件是C,那么C仅受B的约束,而不受A的约束。如果希望A直接约束到C,则需要穿透B。此时,B组件就是不受A约束的组件,这次就需要使用 UnconstrainedBox,例如:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),  //父
  child: UnconstrainedBox( //“去除”父级限制
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    ),
  )
)

如果没有 UnconstrainedBox,则 redBox 大小即为 90X100 像素,因为多重限制时取最大值;但因为存在了UnconstrainedBox ,redBox 的最终大小则为 90X20 像素,因为父类的约束不再对子类有效。

其他约束类容器

除了 BoxConstraints 对尺寸大小限制的容器外,还有一些其他尺寸的容器,例如: - 限制子组件长宽比的容器 AspectRatio - 限制最大宽高的容器 LimitedBox - 根据父容器宽高百分比设置子容器宽高的容器 FractionallySizedBox 等等

线性布局

线性布局就是沿着水平或垂直方向排列子组件,flutter中使用 Row 和 Column 来实现,这俩个类都继承自 Flex 弹性布局。

  • 主轴和纵轴

    • 如果布局是水平方向Row,则主轴就是指水平方向,纵轴就是垂直方向;相应的,若布局是垂直方向Column,则主轴就是垂直方向,纵轴就是水平方向。在对齐时,对齐方式的枚举 MainAxisAlignment和CrossAxisAlignment,分别代表主轴对齐和纵轴对齐。
  • Row的结构定义

Row({
  ...  
  TextDirection textDirection,    // 子组件布局顺序(从左向右,还是从右向左)
  MainAxisSize mainAxisSize = MainAxisSize.max,    // 子组件在主轴上能占的最大空间大小
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 子组件的左右中对齐方式
  VerticalDirection verticalDirection = VerticalDirection.down,  // 子组件的上下对齐方式
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,  // 子组件的上下对齐方式
  List<Widget> children = const <Widget>[], // 子组件数组
})
  • Row的示例如下
Column(
  // 测试Row对齐方式,排除Column默认居中对齐的干扰
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
  ],
);

Flex/Expanded弹性布局

Flex

Flex 组件可以沿着水平或垂直方向排列子组件。Flex 继承自 MultiChildRenderObjectWidget,其对应的 RenderObject 为 RenderFlex 。Row 和 Column 继承自 Flex。通常来说,使用 Row 和 Column 就可以。

Flex 的结构和 Row 和 Column 基本相同,使用方法相同。

Expanded

Expanded 只能作为 Flex/Row/Column 的子Widget,它可以按比例“扩展” Flex 的子组件所占用的空间。下面看一个例子:

class FlexLayoutTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        // Flex的两个子widget按1:2来占据水平空间  
        Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Container(
                height: 30.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                height: 30.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 20.0),
          child: SizedBox(
            height: 100.0,
            // Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间  
            child: Flex(
              direction: Axis.vertical,
              children: <Widget>[
                Expanded(
                  flex: 2,
                  child: Container(
                    height: 30.0,
                    color: Colors.red,
                  ),
                ),
                Spacer( // Spacer是Expanded的一个包装类
                  flex: 1,
                ),
                Expanded(
                  flex: 1,
                  child: Container(
                    height: 30.0,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

Flow/Warp流式布局

在 Row,Column中,如果子Widget超出屏幕范围,则会由屏幕溢出错误(屏幕溢出部分显示一行错误图片UI)。

如果有超过屏幕显示范围的部分,会自动进行折行的布局被称为流式布局。在 Flutter 中通过 Wrap 和 Flow 来支持流式布局。

下面是 Wrap 定义:

Wrap({
  ...
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0, // 主轴方向,子Widget的间距
  this.runAlignment = WrapAlignment.start, // 纵轴方向的对齐方式
  this.runSpacing = 0.0,  // 纵轴方向的间距
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})

而Flow用的比较少,它使用比较复杂,需要自己实现 FlowDelegate 的 paintChildren() 方法,但使用复杂,这里就不记录了。

Stack/Positioned层叠布局

层叠布局的子组件可以根据父容器四个角的位置来确定自身位置,也允许子组件进行堆叠。其中,Stack是允许子组件进行堆叠的容器,而Positioned可以根据 Stack 的四个角来确定子组件的位置。

Stack

该组件定义如下:

Stack({
  this.alignment = AlignmentDirectional.topStart, // 此参数决定如何去对齐没有使用 Positioned 定位的子组件。
  this.textDirection, // 该参数用于确定子组件对齐的坐标系
  this.fit = StackFit.loose, // 该参数用于确定没有 Positioned 定位的子组件如何去适应 Stack 的大小。
  this.clipBehavior = Clip.hardEdge, // 该参数用于决定超出 Stack 显示控件的子组件如何进行剪裁
  List<Widget> children = const <Widget>[],
})
Positioned

该组件定义如下:

const Positioned({
  Key? key,
  this.left,    // 此组件距离 Stack 的 左边 的距离
  this.top,     // 此组件距离 Stack 的 上边 的距离
  this.right,   // 此组件距离 Stack 的 右边 的距离
  this.bottom,  // 此组件距离 Stack 的 下边 的距离
  this.width,   // 此组件自身宽度(注意,如果定义了 left, right, 就不能定义 width,因为会自动计算)
  this.height,  // 此组件自身高度(注意,如果定义了 top, bottom, 就不能定义 height,因为会自动计算)
  required Widget child,
})
Stack和Positioned使用样例
ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: Stack(
    alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
    // fit: StackFit.expand, //未定位widget占满Stack整个空间
    children: <Widget>[
      Container(
        // 未定位,所以 "Hello World" 会被屏幕居中显示。
        child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        // 仅定位了水平方向位置,所以“I am Jack”会在高度上显示在屏幕中部,但离屏幕左测 18 像素
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        // 仅定位了垂直方向位置,所以“Your friend”会在屏幕上方的中央
        child: Text("Your friend"),
      )        
    ],
  ),
);

如果上面代码中,指定 Stack 的 fit: StackFit.expand ,就会出现子组件的互相覆盖。

Align对齐和相对定位

Stack和Positioned可以指定一个或多个子组件相对于父组件各个边的精确偏移位置,且可以重叠,但如果我们仅是简单的控制一个子元素在父元素中的位置的话,使用 Align 组件则更为简单便捷。

该组件定义如下:

Align({
  Key key,
  this.alignment = Alignment.center,  // 该子组件在父组件中的起始位置
  this.widthFactor,  或 this.width  // 子组件的宽高 或 子组件的宽高缩放比
  this.heightFactor, 或 this.height
  Widget child,
})

其中,Alignment 可以是枚举常量,例如 Alignment.topRight,它的定义是

// 在父组件的右上角
static const Alignment topRight = Alignment(1.0, -1.0); 
// 在父组件的左上角
static const Alignment topLeft = Alignment(-1.0, -1.0);

这里可以看出 Alignment 是一个点,有俩个属性 x,y 分别表示在水平和垂直方向的偏移,并且其原点是父容器的中心点。

// 在父组件的右上角
FractionalOffset(1.0, 0.0); 
// 在父组件的左上角
FractionalOffset(0.0, 0.0);

此时,我们还可以使用 FractionalOffset 结构,该结构继承自 Alignment,也是只有 x,y 俩个属性,但它的不同是,FractionalOffset 结构的坐标点原点是左上角,这和布局系统一致,会比较容易理解。

Align和Stack对比

Align 和 Stack/Positioned的都可以用于指定子元素相对于父元素的位置,但区别如下:

  • 定位参考系不同。Stack/Positioned 定位是父容器的四个顶点;Align则根据不同的 Alignment 类型对应不同的原点:Alignment以父容器的中心点,FractionalOffset则以父容器的左上角。
  • Stack允许多个子元素,且子元素可以堆叠;但Align仅可有一个子元素,也就不存在堆叠。
Center组件

Center组件之前曾用过,用来居中子元素。其本质就是一个继承自 Align 的类,看其定义,可知其参数作用。

class Center extends Align {
  const Center({ Key? key, double widthFactor, double heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, alignment:Alignment.center, child: child);
}

所以,我们知道 widthFactor/heightFactor 就是其子组件的宽高缩放比例,而其位置就是父组件中间。

LayoutBuilder动态布局

如果我们需要在布局过程中,根据父组件传递来的约束信息,动态的调整构建不同的布局,就可以使用 LayoutBuilder。

例如,如果我们需要一个 Column 组件,当可用的宽度小于200时,将子组件显示为一列;当可用宽度大于等于200像素时,将子组件显示为两列,我们可以如下编码:

class ResponsiveColumn extends StatelessWidget {
  const ResponsiveColumn({Key? key, required this.children}) : super(key: key);

  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    // 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        if (constraints.maxWidth < 200) {
          // 最大宽度小于200,显示单列
          return Column(children: children, mainAxisSize: MainAxisSize.min);
        } else {
          // 大于200,显示双列
          var _children = <Widget>[];
          for (var i = 0; i < children.length; i += 2) {
            if (i + 1 < children.length) {
              _children.add(Row(
                children: [children[i], children[i + 1]],
                mainAxisSize: MainAxisSize.min,
              ));
            } else {
              _children.add(children[i]);
            }
          }
          return Column(children: _children, mainAxisSize: MainAxisSize.min);
        }
      },
    );
  }
}

// 使用样例
class LayoutBuilderRoute extends StatelessWidget {
  const LayoutBuilderRoute({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var _children = List.filled(6, Text("A"));
    // Column在本示例中在水平方向的最大宽度为屏幕的宽度
    return Column(
      children: [
        // 限制宽度为190,小于 200
        SizedBox(width: 190, child: ResponsiveColumn(children: _children)),
        ResponsiveColumn(children: _children),
        LayoutLogPrint(child:Text("xx")) // 下面介绍
      ],
    );
  }
}

// 这个组件类,是为了DEBUG输出Log的UI。
class LayoutLogPrint<T> extends StatelessWidget {
  const LayoutLogPrint({
    Key? key,
    this.tag,
    required this.child,
  }) : super(key: key);

  final Widget child;
  final T? tag; //指定日志tag

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // assert在编译release版本时会被去除
      assert(() {
        print('${tag ?? key ?? child}: $constraints');
        return true;
      }());
      return child;
    });
  }
}

容器类组件

容器类组件和前面说的布局类组件,都可以约束作用其子组件,区别是: - 布局类Widget一般需要接收一个 Children Widget 数组,它们多是直接或间接继承自 MultiChildRenderObjectWidget ; 而容器类Widget一般仅接收一个 Child Widget,它们多是直接或间接继承自 SingleChildRenderObjectWidget。 - 布局类Widget一般是对子Widget进行排列;而容器类Widget则主要是对子Widget进行包装添加修饰(背景色,变换旋转剪裁,限制等)

Padding填充

该组件是为其子节点添加留白,和边距效果类似,定义如下:

Padding({
  ...
  EdgeInsetsGeometry padding, // 这是一个抽象类,一般我们使用其子类 EdgeInsets
  Widget child,
})

其使用方式样例如下:

class PaddingTestRoute extends StatelessWidget {
  const PaddingTestRoute({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      //上下左右各添加16像素补白
      padding: const EdgeInsets.all(16),
      child: Column(
        //显式指定对齐方式为左对齐,排除对齐干扰
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: const <Widget>[
          Padding(
            //左边添加8像素补白
            padding: EdgeInsets.only(left: 8),
            child: Text("Hello world"),
          ),
          Padding(
            //上下各添加8像素补白
            padding: EdgeInsets.symmetric(vertical: 8),
            child: Text("I am Jack"),
          ),
          Padding(
            // 分别指定四个方向的补白
            padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
            child: Text("Your friend"),
          )
        ],
      ),
    );
  }
}

DecoratedBox装饰容器

该组件可以为其子组件添加一些装饰,例如背景,边框,渐变等。其定义如下:

const DecoratedBox({
  Decoration decoration,  // 将要进行绘制的装饰
  DecorationPosition position = DecorationPosition.background,  // 绘制位置,子组件前景还是背景
  Widget? child
})

其中,Decoration类是一个抽象类,它有一个子类叫 BoxDecoration 类,其结构定义如下:

BoxDecoration({
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})

利用样例如下:

 DecoratedBox(
   decoration: BoxDecoration(
     gradient: LinearGradient(colors:[Colors.red,Colors.orange.shade700]), //背景渐变
     borderRadius: BorderRadius.circular(3.0), //3像素圆角
     boxShadow: [ //阴影
       BoxShadow(
         color:Colors.black54,
         offset: Offset(2.0,2.0),
         blurRadius: 4.0
       )
     ]
   ),
  child: Padding(
    padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
    child: Text("Login", style: TextStyle(color: Colors.white),),
  )
)
Transform变换

该组件也可以在其子组件绘制时,对齐应用一些矩阵的变换来实现特效。其样例如下:

Container(
  color: Colors.black,
  child: Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
)

// 平移
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  //默认原点为左上角,左移20像素,向上平移5像素  
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
)

// 旋转
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.rotate(
    //旋转90度
    angle:math.pi/2 ,
    child: Text("Hello world"),
  ),
)

// 缩放
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.scale(
    scale: 1.5, //放大到1.5倍
    child: Text("Hello world")
  )
);


// 带子组件一起缩放
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      //将Transform.rotate换成RotatedBox  
      child: RotatedBox(
        quarterTurns: 1, //旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
),

Container容器组件

Container本身是一个组合类容器,它不具备具体的 RenderObject,但它是 DecoratedBox, ConstrainedBox, Transform, Padding, Aligh 等组件组合的容器,所以,只需要使用Container,就可以同时实现 装饰,变换,限制 等效果。下面是 Container 的定义:

Container({
  this.alignment,
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰
  Decoration foregroundDecoration, //前景装饰
  double width,//容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件
  this.margin,//容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
  ...
})

这里需要注意的是,Container容器的大小可以通过 width, height 来指定,也可以通过 constraints 来指定;但 width, height 优先。

Clip剪裁类组件

Clip剪裁组件,用于对组件进行剪裁,其子类型有如下:

  • ClipOval 将子组件剪裁为椭圆形
  • ClipRRect 将子组件剪裁为圆角矩形
  • ClipRect 默认模式,剪裁掉子组件在布局控件外的绘制内容
  • ClipPath 按照自定义的路径进行剪裁

使用样例如下:

import 'package:flutter/material.dart';

class ClipTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 头像  
    Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
    return Center(
      child: Column(
        children: <Widget>[
          avatar, //1,不剪裁
          ClipOval(child: avatar), //2,剪裁为圆形
          ClipRRect( //3,剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ), 
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Align(
                alignment: Alignment.topLeft,
                widthFactor: .5,//宽度设为原来宽度一半,另一半会溢出
                child: avatar,
              ),
              Text("你好世界", style: TextStyle(color: Colors.green),)
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(//将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5,//宽度设为原来宽度一半
                  child: avatar,
                ),
              ),
              Text("你好世界",style: TextStyle(color: Colors.green))
            ],
          ),
        ],
      ),
    );
  }
}

// 其中最重要的是自定义剪裁,使用的比较复杂,如下:

class MyClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}

DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
    clipper: MyClipper(), //使用自定义的clipper
    child: avatar
  ),
)

FittedBox空间适配组件

当子组件超过父组件大小时,如果不做任何处理,会在UI上显示一个错误警告的UI,同时控制台上会打印错误日志,例如:

Padding(
  padding: const EdgeInsets.symmetric(vertical: 30.0),
  child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)

虽然根据 flutter 布局协议,父组件会将最大的显示空间作为约束传递给子组件,子组件应当遵守父组件的约束,但是,例如Text之类的组件,一旦父组件过小,则Text受到制约会出现自动换行等效果,但如果我们需要Text受制约时进行缩放,而非换行的话,就需要调整子组件适配父组件的方式,此时就需要使用 FittedBox 组件来设置,该组件定义如下:

const FittedBox({
  Key? key,
  this.fit = BoxFit.contain, // 适配方式
  this.alignment = Alignment.center, //对齐方式
  this.clipBehavior = Clip.none, //是否剪裁
  Widget? child,
})

该组件本质就是解除了父组件对子组件的约束,使子组件受到 fittedBox 组件限制。

Scaffold页面骨架组件

该组件默认包含一个导航栏(右上有个分享按钮),(左上有个抽屉菜单),一个底部导航,(右下方)一个悬浮的动作按钮。其基本实现样例如下:

class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      appBar: AppBar( //1,导航栏
        title: Text("App Name"), 
        actions: <Widget>[ //导航栏右侧菜单
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
      ),

      drawer: MyDrawer(), //2,抽屉菜单

      bottomNavigationBar: BottomNavigationBar( // 3,底部导航
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),

      floatingActionButton: FloatingActionButton( //4,悬浮按钮
          child: Icon(Icons.add),
          onPressed:_onAdd
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){
  }
}

// 其中,抽屉菜单代码如下:
class MyDrawer extends StatelessWidget {
  const MyDrawer({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        //移除抽屉菜单顶部默认留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

可滚动组件

之前描述过,Flutter 有俩种布局模型:一个是基于 RenderBox 的盒模型布局;一个是基于 RenderSliver 按需加载的列表布局。

这里的可滚动组件主要就是 RenderSliver 类型的布局,它只有自己的 Sliver(薄片) 需要渲染时才会加载模型。一般而言,可滚动组件主要由三个部分组成: - Scrollable: 处理滑动手势,确定滑动偏移,在偏移变化时构建 Viewprot - Viewport: 即列表的可视区域。它是 Scrollable 的子组件。 - Sliver: 即列表中显示的元素薄片。它是 Viewport 的子组件。

其布局过程是: - 首先,Scrollable 监听到用户的滑动行为后,根据滑动的偏移构建 Viewport - 然后,Viewport 将当前视口信息和配置信息通过 SliverConstraints 传递给 Sliver - 最后,Sliver 对子组件按需进行构建和布局,然后确定自身位置等信息,保存到一个 SliverGeometry 类型的对象中。

Scrollable定义

该组件用于处理滑动手势,确定滑动偏移,然后在偏移变化时构建 Viewport,其定义如下:

Scrollable({
  ...
  this.axisDirection = AxisDirection.down,  // 滚动方向
  this.controller, // 一个ScrollController的对象,用来控制滚动位置和监听滚动事件。例如默认支持的 ClampingScrollPhysics 可实现Android的滑块滑到边界则不可移动效果;以及 BouncingScrollPhysics 模仿 iOS 的拖到边界后的弹性效果
  this.physics, // 一个ScrollPhysics类型的对象,用来决定可滚动组件如何响应用户操作
  required this.viewportBuilder, // 构建Viewport的回调。用户滑动时,会使用该回调重构viewport
})

Viewport定义

Viewport用于渲染当前视口中需要显示的Sliver.

Viewport({
  Key? key,
  this.axisDirection = AxisDirection.down,
  this.crossAxisDirection,
  this.anchor = 0.0,
  required ViewportOffset offset, // 用户的滚动偏移
  // 类型为Key,表示从什么地方开始绘制,默认是第一个元素
  this.center,
  this.cacheExtent, // 预渲染区域(对应缓存多少像素,或多少切片长度)
  //该参数用于配合解释cacheExtent的含义,也可以为主轴长度的乘数
  this.cacheExtentStyle = CacheExtentStyle.pixel, // 该值是一个枚举,有 pixel(显示多少像素) 和 viewport(显示几个切片) 俩个值。
  this.clipBehavior = Clip.hardEdge,
  List<Widget> slivers = const <Widget>[], // 需要显示的 Sliver 列表
})

Sliver和其他

Sliver对应的渲染对象是 RenderSliver。 RenderSliver和RenderBox都是继承自 RenderOject 类。但是RenderBox 在布局时进行约束的是仅约束最大宽高的 BoxConstraints; 而RenderSliver进行约束的是 SliverConstraints。

基本全部的可滚动组件在构造时,都可以指定 ScrollDirection(主轴), reverse(是否反向), controller(监听滑块事件和控制滑块位置), physics(用来确定如何响应用户操作), cacheExtent(预渲染区域大小),然后这些属性会穿透传给对应的 Scrollable 和 Viewport,这些属性被视为可滚动组件的通用属性。

SingleChildScrollView简易版可滚动组件

该组件仅可接收一个子组件,其定义如下:

SingleChildScrollView({
  this.scrollDirection = Axis.vertical, // 标准参数:滚动方向,默认是垂直方向
  this.reverse = false, // 标准参数:是否反向
  this.padding, 
  bool primary, // 该参数意思是是否使用 widget 树中的默认的 PrimaryScrollController 
  this.physics, // 标准参数
  this.controller, // 标准参数
  this.child, // 唯一的子组件
})

该类型不支持 Sliver 的延迟加载,如果 Sliver 太多,则性能会很差,此时,推荐使用支持Sliver延迟架子啊的可滚动组件,如 ListView 等。

其使用样例如下:

class SingleChildScrollViewTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar( // 显示进度条
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.0),
        child: Center(
          child: Column( 
            //动态创建一个List<Widget>  
            children: str.split("") 
                //每一个字母都用一个Text显示,字体为原来的两倍
                .map((c) => Text(c, textScaleFactor: 2.0,)) 
                .toList(),
          ),
        ),
      ),
    );
  }
}

ListView最常见的可滚动组件

该组件是最常见的可滚动组件,可以支持延迟加载,其定义如下:

ListView({
  ...  
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController? controller,
  bool? primary,
  ScrollPhysics? physics,
  EdgeInsetsGeometry? padding,
  
  //ListView各个构造函数的共同参数  
  double? itemExtent, // 若该值不为空,则会强制children的长度为itemExtent的值。
  Widget? prototypeItem, // 列表项原型。如果我们确定列表项长度都一致,但不支持其高度多少,则可指定该项。widget树在开始会自动计算一次列表项长度,而无需每次构建子组件时重新计算。
  bool shrinkWrap = false, // 若该值为true,则整个listView整体的长度为子item的总长度
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double? cacheExtent, // 预渲染区域长度
    
  //子widget列表,只有少量的子组件数量使用;若大量子组件,建议使用 ListView.builder 动态构建
  List<Widget> children = const <Widget>[],
})

下面是个简单的使用样例:

ListView(
  shrinkWrap: true, 
  padding: const EdgeInsets.all(20.0),
  children: <Widget>[
    const Text('I\'m dedicating every day to you'),
    const Text('Domestic life was never quite my style'),
    const Text('When you smile, you knock me out, I fall apart'),
    const Text('And I thought I was so smart'),
  ],
);

当每个子组件的长度相同时,建议指定 itemExtent 或 prototypeItem 以提高性能,示例如下:

class FixedExtentList extends StatelessWidget {
  const FixedExtentList({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        prototypeItem: ListTile(title: Text("1")),
      //itemExtent: 56,
      itemBuilder: (context, index) {
        //LayoutLogPrint是一个自定义组件,在布局时可以打印当前上下文中父组件给子组件的约束信息
        return LayoutLogPrint(
          tag: index, 
          child: ListTile(title: Text("$index")),
        );
      },
    );
  }
}

当子组件比较多,或者子组件数量不确定情况下,可使用 ListView.builder ,其定义如下:

ListView.builder({
  // ListView公共参数已省略  
  ...
  required IndexedWidgetBuilder itemBuilder, // 当列表滚动到具体的index位置时,会调用该构建器去构建一个列表项
  int itemCount, // 列表的数量,若为null,则是无限列表
  ...
})

使用样例如下:

ListView.builder(
  itemCount: 100,
  itemExtent: 50.0, //强制高度为50.0
  itemBuilder: (BuildContext context, int index) {
    return ListTile(title: Text("$index")); // 这里需要返回一个 widget
  }
);

另外,可以在列表项中加入分割线,下面用个例子,让单数行显示绿色,双数行显示蓝色。

class ListView3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //下划线widget预定义以供复用。  
    Widget divider1=Divider(color: Colors.blue,);
    Widget divider2=Divider(color: Colors.green);
    return ListView.separated(
      itemCount: 100,
      //列表项构造器
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      },
      //分割器构造器
      separatorBuilder: (BuildContext context, int index) {
        return index%2==0?divider1:divider2;
      },
    );
  }
}

值得注意的是:

-

getX状态管理

Dio网络请求

SharedPreferences本地存储

SQLite本地存储

Socket.IO即时通讯

FFmpeg多媒体处理

MediaKit多媒体处理

项目规划概述

1. 整体架构概览

  • 开发框架 :使用Flutter构建的跨平台应用,支持iOS、Android、Windows和macOS
  • 架构模式 :采用MVC/MVVM混合架构,结合GetX进行状态管理
  • 核心分层 :

    • API层:从服务器获取数据
    • 数据模型层:解析和存储数据
    • 管理器层:处理业务逻辑
    • UI层:展示数据并处理用户交互
    • 消息处理层:事件系统连接各层通信

      2. 核心模块设计

  • 2.1 API层

    • 位于 lib/api/ 目录,如 chat.dart
    • 负责与服务器通信,封装各种网络请求
    • 使用重试机制和离线请求管理确保网络稳定性
  • 2.2 数据模型层

    • 位于 lib/object/ 目录,如 message.dart
    • 定义应用核心数据结构,包括消息、用户、群组等
    • 使用枚举定义消息类型、状态等常量
  • 2.3 管理器层

    • 位于 lib/managers/ 目录,如 object_mgr.dart
    • 采用单例模式,提供各种服务和功能
    • 核心管理器包括:聊天管理器、用户管理器、消息管理器、网络管理器等
    • 通过 ObjectMgr 作为全局对象根,统一管理各个子管理器
  • 2.4 UI层

    • 位于 lib/views/ 和 lib/home/ 目录
    • 使用GetX进行页面路由和状态管理
    • 路由配置集中在 routes.dart
    • 支持多语言国际化和主题切换
  • 2.5 消息处理层

    • 位于 lib/message_handlers/ 目录
    • 采用策略模式设计,实现消息类型的可扩展处理
    • 通过 MessageTypeHandler 抽象类定义接口
    • 通过 MessageTypeRegistry 注册中心管理各种消息处理器

      3. 特色架构设计

  • 3.1 消息类型扩展架构

    • 基于策略模式,新增消息类型只需创建处理器并注册
    • 处理器负责消息的UI展示、菜单选项等逻辑
    • 支持渐进式迁移现有消息类型到新架构
  • 3.2 多端适配

    • 通过条件编译和平台特定代码处理不同平台特性
    • 针对不同设备尺寸优化UI布局
  • 3.3 离线处理机制

    • 支持离线请求队列,网络恢复后自动重试
    • 本地数据库缓存重要数据