Flutter - Dart 学习笔记

520 阅读24分钟

 一套代码,多端运行” 口号的跨平台开发方案

前言:React Native

React Native 希望 在性能、展示、交互能力和迭代交付效率之间做到平衡。
它在 Web 容器方案的基础上, 优化了加载、解析和渲染这三大过程, 以相对简单的方式支持了构建移动端页面必要的 Web 标准,保证了便捷的前端开发体验;

并且在保留基本渲染能力的基础上,用原生自带的 UI 组件实现代替了核心的渲染引擎,
从而保证了良好的渲染性能。

缺点:
但是,由于 React Native 的技术方案所限,使用原生控件承载界面渲染,在牺牲了部分 Web 标准灵活性的同时,

固然解决了不少性能问题,但也引入了新的问题:除开通过 JavaScript 虚拟机进行原生接口的调用,而带来的通信低效不谈,

由于框架本身不负责渲染,而是由原生代理,因此我们还需要面对大量平台相关的逻辑。
而随着系统版本和 API 的变化,我们还需要处理不同平台的原生控件渲染能力上的差异, 修复各类怪异的 Bug,甚至还需要在原生系统上打各类补丁。 要用好 React Native,除了掌握这个框架外,开发者还必须同时熟悉 iOS 和 Android 系统

Flutter,则完全不同于 React Native

提供了一整套从底层渲染逻辑到上层开发语言的完整解决方案:

1. 视图渲染完全闭环在其框架内部,

不依赖于底层操作系统提供的任何组件,

从根本上保证了视图渲染在 Android 和 iOS 上的高度一致性;

2. Flutter 的开发语言 Dart,是 Google 专门为(大)前端开发量身打造的专属语言,

借助于先进的工具链和编译器,成为了少数同时支持 JIT 和 AOT 的语言之一,

开发期调试效率高,发布期运行速度快、执行性能好,在代码执行效率上可以媲美原生 App。

对比RN:

而这与 React Native 所用的只能解释执行的 JavaScript,又拉开了性能差距。

正是因为 Flutter 在开发阶段使用了 JIT 编译模式,使得通过热重载(Hot Reload)

这样的技术去进一步提升调试效率成为可能。

简单来说,热重载就是在无需重新编译代码、重启应用程序、丢失程序执行状态的情况下,

就能实时加载修改后的代码,查看改动效果。

体验 热重载:

将其根节点修改为 StatelessWidget:点击 Run 图标,然后试着修改一下代码,

保存后仅需几百毫秒就可以看到最新的显示效果。

热重载 局限性:

并不是所有的代码改动都可以通过热重载来更新。对 hello_world 示例而言,
由于 Flutter 并不会在热重载后重新执行 main 函数,而只会根据原来的根节点重新创建控件树,

因此我们刚才做了一些改造之后才支持热重载。
如果热重载不起作用的时候,我们也不需要进行漫长的重新编译加载等待, 只要点击位于工程面板左下角的热重启(Hot Restart)按钮就可以以秒级的速度进行代码重编译以及程序重启了,

而它与热重载的区别只是因为重启丢失了当前程序的运行状态而已,对实际调试也没什么影响。

Dart 语言

2011年谷歌推出,

目的:

如同 Kotlin 和 Swift 的出现,分别是为了解决 Java 和 Objective-C 在编写应用程序的一些实际问题一样,

Dart 的诞生正是要解决 JavaScript 存在的、在语言本质上无法改进的缺陷。

那么,JavaScript 到底有哪些问题和缺陷呢?

JavaScript 实际上是两类编程语言风格的混合产物:(简化的)函数式编程风格,与(简化的)面向对象编程风格。

语言设计时间短,考虑不周,导致 使用 JavaScript 开发的程序混乱不堪

JavaScript 的发展

原本 JavaScript 只能在浏览器中运行,但 Node.js 的出现让它开始有能力运行在服务端,

很快手机应用与桌面应用也成为了 JavaScript 的宿主容器,

一些明星项目比如 React、React Native、Vue、Electron、NW(node-webkit)也迅速发展

JavaScript 成为了前后端通吃的全栈语言,

如同 Atwood 定律描述的:凡是能用 JavaScript 写出来的系统,最终都会用 JavaScript 写出来

Dart 核心特性

JIT 与 AOT借助于先进的工具链和编译器,Dart 是少数同时支持

JIT(Just In Time,即时编译)

和 AOT(Ahead of Time,运行前编译)的语言之一。

语言在运行之前通常都需要编译,JIT 和 AOT 则是最常见的两种编译模式。

JIT 在运行时即时编译,

在开发周期中使用,可以动态下发和执行代码,开发测试效率高,

但 运行速度 和 执行性能 则会因为运行时即时编译受到影响。

AOT 即提前编译,

可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需要提前编译,开发测试效率低。

AOT 的典型代表是 C/C++,它们必须在执行前编译成机器码;

而 JIT 的代表,则包括了如 JavaScript、Python 等几乎所有的脚本语言。

总结JIT & AOT

在开发期使用 JIT 编译,可以缩短产品的开发周期。

Flutter 最受欢迎的功能之一热重载,正是基于此特性。

而在发布期使用 AOT,就不需要像 React Native 那样在跨平台 JavaScript 代码

和原生 Android、iOS 代码之间建立低效的方法调用映射关系。

所以说,Dart 具有运行速度快、执行性能好的特点。

Dart 内存分配与垃圾回收

Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,

内存增长始终是线性的,省去了查找可用内存的过程。

在 Dart 中,并发是通过 Isolate 实现的。

Isolate 是类似于线程但不共享内存,独立运行的 worker。

这样的机制,就可以让 Dart 实现无锁的快速分配。

Dart 的垃圾回收,则是采用了多生代算法。

新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,

然后整体释放当前空间的所有内存。

回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,

这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。

线程优势:

其他原因的并发多线程
支持并发执行线程的高级语言(比如,C++、Java、Objective-C),大都以抢占式的方式切换线程, 即:每个线程都会被分配一个固定的时间片来执行,超过了时间片后线程上下文将被抢占后切换。 如果这时正在更新线程间的共享资源,抢占后就可能导致数据不同步的问题。 解决这一问题的典型方法是,使用锁来保护共享资源,但锁本身又可能会带来性能损耗, 甚至出现死锁等更严重的问题。

Dart 是单线程模型的优势就体现出来了,因为它天然不存在资源竞争和状态同步的问题。

这就意味着,一旦某个函数开始执行,就将执行到这个函数结束,而不会被其他 Dart 代码打断。

Dart 中并没有线程,只有 Isolate(隔离区)。

Isolates 之间不会共享内存,就像几个运行在不同进程中的 worker,

通过事件循环(Event Looper)在 事件队列(Event Queue)上传递消息通信。

在 Dart 社区目前最顶级的产品就是 Flutter 和 Fuchsia 了,

我觉得 Dart 是否能够成功,目前来看主要取决于 Flutter 和 Fuchsia 能否成功。

而,Flutter 是构建 Fuchsia 的 UI 开发框架,因此这个问题也变成了 Flutter 能否成功。

//////////////

跨平台开发方案的三个时代

根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。

Web 容器时代:

基于 Web 相关技术通过浏览器组件来实现界面及功能,
典型的框架包括 Cordova(PhoneGap)、Ionic 和微信小程序。

泛 Web 容器时代:

采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,
代表框架有 React Native、Weex 和快应用,广义的还包括天猫的 Virtual View 等。

自绘引擎时代:

自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。 Flutter,是为数不多的代表。

我们在做技术选型时,可以参考以上维度,从开发效率、技术栈、性能表现、维护成本和社区生态来进行综合考虑。比如,是否必须支持动态化?是只解决 Android、iOS 的跨端问题,还是要包括 Web?对性能要求如何?对多端体验的绝对一致性和维护成本是否有强诉求?从各个维度综合考量,React Native 和 Flutter 无疑是最均衡的两种跨平台开发方案,而其他的方案或多或少都“偏科严重”。
React Native 依托于 Facebook,经过 4 年多的发展已经成长为跨平台开发方案的实际领导者,并拥有较为丰富的第三方库和开发社区;Flutter 以挑战者姿态出现在我们的面前,可以提供更彻底的跨平台技术解决方案。
flutter 的产品路线图 https://mp.weixin.qq.com/s/aRhQdMd0R74adph9_V0wgQ

因此,如果是中短期项目的话,我建议使用 React Native。但作为技术选型,我们要看得更远一些。Flutter 的设计理念比较先进,解决方案也相对彻底,在渲染能力的一致性以及性能上,和 React Native 相比优势非常明显。
Flutter 的野心不仅仅是移动端。前段时间,Google 团队已经完成了 Hummingbird,即 Flutter 的 Web 的官方 Demo,在桌面操作系统的探索上也取得了进展,未来大前端技术栈是否会由 Flutter 完成统一,值得期待。

Flutter 是怎么完成组件渲染的呢?

图像的显示:
在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。
CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。

操作系统在呈现图像时遵循了这种机制,而 Flutter 作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释 Flutter 的绘制原理。

图 1 Flutter 绘制原理

Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染。

Skia 是什么?

要想了解 Flutter,你必须先了解它的底层图像渲染引擎 Skia。

因为,通过 Skia 帮助 Flutter 实现 向 GPU 提供视图数据

Skia 是一款用 C++ 开发的、性能彪悍的 2D 图像绘制引擎,其前身是一个向量绘图软件。

Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。

架构于 Skia 之上的 Flutter,也因此拥有了彻底的跨平台渲染能力。
底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。

为什么是 Dart?

Dart 因为同时支持 AOT 和 JIT,所以具有运行速度快、执行性能好的特点外,Google 公司给出的原因很简单也很直接:Dart 语言开发组就在隔壁

1. Dart 同时支持即时编译 JIT 和事前编译 AOT。

2.Dart 作为一门现代化语言,集百家之长,拥有其他优秀编程语言的诸多特性(比如,完善的包管理机制)

3. Dart 避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错。

Flutter 的原理

Flutter 架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework

Embedder 是操作系统适配层,
实现了渲染 Surface 设置,线程设置,以及平台插件等平台相关特性的适配。
从这里我们可以看到,Flutter 平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对较低。
**Engine 层主要包含 Skia、Dart 和 Text,实现了 Flutter 的渲染引擎、文字排版、事件处理和 Dart 运行时等功能。**Skia 和 Text 为上层接口提供了调用底层渲染和排版的能力,Dart 则为 Flutter 提供了运行时调用 Dart 和渲染引擎的能力。而 Engine 层的作用,则是将它们组合起来,从它们生成的数据中实现视图渲染。
Framework 层则是一个用 Dart 实现的 UI SDK,包含了动画、图形绘制和手势识别等功能。为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,Flutter 还基于这些基础能力,根据 Material 和 Cupertino 两种视觉设计风格封装了一套 UI 组件库。我们在开发 Flutter 的时候,可以直接使用这些组件库。

接下来,我以界面渲染过程为例,和你介绍 Flutter 是如何工作的。

页面中的各界面元素(Widget)以树的形式组织,即控件树。
Flutter 通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。
而渲染对象树在 Flutter 的展示过程分为四个阶段:布局、绘制、合成和渲染。

布局

Flutter 采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。

为了防止因子节点发生变化而导致整个控件树重新布局,Flutter 加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。

绘制

布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter 会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。以下图为例:节点 1 在绘制完自身后,会再绘制节点 2,然后绘制它的子节点 3、4 和 5,最后绘制节点 6。

可以看到,由于一些其他原因(比如,视图手动合并)导致 2 的子节点 5 与它的兄弟节点 6 处于了同一层,这样会导致当节点 2 需要重绘的时候,与其无关的节点 6 也会被重绘,带来性能损耗。为了解决这一问题,Flutter 提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter 会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。

重绘边界的一个典型场景是 Scrollview。ScrollView 滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。

合成和渲染

终端设备的页面越来越复杂,因此 Flutter 的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
合并完成后,Flutter 会将几何图层数据交由 Skia 引擎加工成二维图像数据,最终交由 GPU 进行渲染,完成界面的展示。

那么,我们学习 Flutter 都需要掌握哪些知识呢?

工程结构

首先,我们打开 Android Studio,创建一个 Flutter 工程应用 flutter_app。Flutter 会根据自带的应用模板,自动生成一个简单的计数器示例应用 Demo。

图 2 Flutter 工程目录结构

可以看到,除了 Flutter 本身的代码、资源、依赖和配置之外,Flutter 工程还包含了 Android 和 iOS 的工程目录。
这也不难理解,因为 Flutter 虽然是跨平台开发方案,但却需要一个容器最终运行到 Android 和 iOS 平台上,所以 Flutter 工程实际上就是一个同时内嵌了 Android 和 iOS 原生子工程的父工程:我们在 lib 目录下进行 Flutter 代码的开发,
而某些特殊场景下的原生功能,则在对应的 Android 和 iOS 工程中提供相应的代码实现,供对应的 Flutter 代码引用。
Flutter 会将相关的依赖和构建产物注入这两个子工程,最终集成到各自的项目中。而我们开发的 Flutter 代码,最终则会以原生工程的形式运行。

工程代码

这个简单示例中,从基础的组件、布局到手势的监听,再到状态的改变,Flutter 最核心的思想在这 60 余行代码中展现得可谓淋漓尽致。

第一部分是应用入口、应用结构以及页面结构,可以帮助你理解构建 Flutter 程序的基本结构和套路;
第二部分则是页面布局、交互逻辑及状态管理,能够帮你理解 Flutter 页面是如何构建、如何响应交互,以及如何更新的。

第一部分的代码,也就是应用的整体结构:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
          home: MyHomePage(title: 'Flutter Demo Home Page')
   );
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Widget build(BuildContext context) => {...};
}

在本例中,Flutter 应用为 MyApp 类的一个实例,而 MyApp 类继承自 StatelessWidget 类,这也就意味着应用本身也是一个 Widget。事实上,在 Flutter 中,Widget 是整个视图描述的基础,在 Flutter 的世界里,包括应用、视图、视图控制器、布局等在内的概念,都建立在 Widget 之上,Flutter 的核心设计思想便是一切皆 Widget。

Widget 是组件视觉效果的封装,是 UI 界面的载体,因此我们还需要为它提供一个方法,来告诉 Flutter 框架如何构建 UI 界面,这个方法就是 build。

在 build 方法中,我们通常通过对基础 Widget 进行相应的 UI 配置,或是组合各类基础 Widget 的方式进行 UI 的定制化。比如在 MyApp 中,我通过 MaterialApp 这个 Flutter App 框架设置了应用首页,即 MyHomePage。当然,MaterialApp 也是一个 Widget。

MaterialApp 类是对构建 material 设计风格应用的组件封装框架,里面还有很多可配置的属性,比如应用主题、应用名称、语言标识符、组件路由等。但是,这些配置属性并不是本次分享的重点,如果你感兴趣的话,可以参考 Flutter 官方的API 文档,来了解 MaterialApp 框架的其他配置能力。

StatelessWidget 和 StatefulWidget

是flutter的基础组件,日常开发中自定义Widget都是选择继承这两者之一。

两者的区别在于状态的改变,StatelessWidget面向那些始终不变的UI控件,比如标题栏中的标题;而StatefulWidget则是面向可能会改变UI状态的控件,比如有点击反馈的按钮。

StatelessWidget就没什么好研究的了,
StatefulWidget的创建需要指定一个State,在需要更新UI的时候调用setState(VoidCallback fn),并在VoidCallback中改变一些变量数值等,组件会重新build以达到刷新状态也就是刷新UI的效果。

MaterialApp和Scaffold是Flutter提供的两个Widget,

MaterialApp 它封装了应用程序实现Material Design所需要的一些Widget Scaffold组件是Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API。

例如:

  @override
  Widget build(BuildContext context) {
    initData();
    return new MaterialApp(
      theme: new ThemeData(
        // 设置主题颜色
        primaryColor: const Color(0xFF63CA6C)
      ),
      home: new Scaffold(
        // 设置App顶部的AppBar
        appBar: new AppBar(
          // AppBar的标题
          title: new Text(appBarTitles[_tabIndex], 
          // 标题文本的颜色
          style: new TextStyle(color: Colors.white)),
          // AppBar上的图标的颜色
          iconTheme: new IconThemeData(color: Colors.white)
        ),
        body: _body,
        // 页面底部的导航栏
        bottomNavigationBar: new CupertinoTabBar(
          items: <BottomNavigationBarItem>[
            new BottomNavigationBarItem(
                icon: getTabIcon(0),
                title: getTabTitle(0)),
            new BottomNavigationBarItem(
                icon: getTabIcon(1),
                title: getTabTitle(1)),
            new BottomNavigationBarItem(
                icon: getTabIcon(2),
                title: getTabTitle(2)),
            new BottomNavigationBarItem(
                icon: getTabIcon(3),
                title: getTabTitle(3)),
          ],
          currentIndex: _tabIndex,
          // 底部Tab的点击事件处理
          onTap: (index) {
            setState((){
              _tabIndex = index;
            });
          },
        ),
        // 侧滑菜单,这里的MyDrawer是自定义的Widget
        drawer: new MyDrawer(),
      ),
    );
  }

布局容器组件

Center组件

Center组件中的子组件会居中显示。Center组件会尽可能的大,如果你不给它设置任何约束。下面是Center组件的使用方法

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: "Test",
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Test")
        ),
        body: new Center(
          child: new Text("hello world")
        ),
      ),
    );
  }
}

Container组件

Container是使用非常多的一个布局容器,关于Container容器的显示规则,有如下几条:

  1. Container是使用非常多的一个布局容器,关于Container容器的显示规则,有如下几条:
  2. 如果Container中没有子组件,则Container会尽可能的大
  3. 如果Container中有子组件,则Container会适应子组件的大小
  4. 如果给Container设置了大小,则Container按照设置的大小显示
  5. Container的显示规则除了跟自身约束和子组件有关,跟它的父组件也有关
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: "Test",
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Test")
        ),
        body: new Container(
          width: 100.0,
          height: 100.0,
          color: Colors.red,
          child: new Text("Flutter!"),
        )
      ),
    );
  }
}

Padding组件

Padding组件专门用于给它的子组件设置内边距,用法比较简单:

Align组件

Align组件用于将它的子组件放置到确定的位置,比如下面的代码展示了将Text组件放置到100*100的容器的右下角:

new Container(
  width: 100.0,
  height: 100.0,
  color: Colors.red,
  child: new Align(
    child: new Text("hello"),
    alignment: Alignment.bottomRight,
  ),
)

FittedBox组件

FittedBox组件根据fit属性来确定子组件的位置,fit属性是一个BoxFit类型的值,BoxFit是个枚举类,取值有如下几种:

enum BoxFit {
  fill,
  contain,
  cover,
  fitWidth,
  fitHeight,
  none,
  scaleDown,
}

AspectRatio组件

AspectRatio组件用于让它的子组件按一定的比例显示,下面是示例代码:

        body: new AspectRatio(
          // Container组件按16:9(width / height)显示
          aspectRatio: 16.0 / 9.0,
          child: new Container(
            color: Colors.red,
          ),
        )

ConstrainedBox组件

ConstrainedBox组件用于给它的子组件强制加上一些约束,比如下面的代码:

        body: new ConstrainedBox(
          constraints: const BoxConstraints.expand(width: 50.0, height: 50.0),
          child: new Container(
            color: Colors.red,
            width: 200.0,
            height: 200.0,
          )
        )

IntrinsicWidth & IntrinsicHeight

这两个组件的作用是将他们的子组件调整到组件本身的宽度/高度。

这个类是非常有用的,例如,当宽度/高度没有任何限制时,你会希望子组件按更合理的宽度/高度显示而不是无限的扩展。

LimitedBox组件

LimitedBox是一个当其自身不受约束时才限制其大小的容器。
如果这个组件的最大宽度是没有约束,那么它的宽度就限制在maxWidth。类似地,如果这个组件的最大高度没有约束,那么它的高度就限制在maxHeight。

Offstage组件

Offstage组件用于显示或隐藏它的子组件,如下代码所示:

new Offstage(
  offstage: false, // true: 隐藏, false: 显示
  child: new Text("hello world"),
)

OverflowBox & SizedOverflowBox

OverflowBox组件它给它的子组件带来不同的约束,而不是从它的父组件中得到,可能允许子组件溢出到父组件中。

SizedOverflowBox组件是一个指定大小的组件,它的约束会传递给子组件,子组件可能溢出。

SizedBox组件

SizedBox是一个指定了大小的容器。

如果指定了SizedBox的大小,则子组件会使用SizedBox的大小,如果没有指定SizedBox的大小,则SizedBox会使用子组件的大小。如果SizedBox没有子组件,SizedBox会按它自己的大小来显示,将nulls当作0。

new SizedBox(
  // 如果指定width和height,则Container按照指定的大小显示,而不是Container自己的大小,如果没有指定width和height,则SizedBox按照Container的大小显示
  width: 50.0,
  height: 50.0,
  child: new Container(
    color: Colors.red,
    width: 300.0,
    height: 300.0,
  ),
)

Transform组件

Transform用于在绘制子组件前对子组件进行某些变换操作,比如平移、旋转、缩放等。

示例代码如下:

new Container(
  color: Colors.black,
  child: new Transform(
    alignment: Alignment.topRight,
    // 需要导包:import 'dart:math' as math;
    transform: new Matrix4.skewY(0.3)..rotateZ(-math.pi / 12.0),
    child: new Container(
      padding: const EdgeInsets.all(8.0),
      color: const Color(0xFFE8581C),
      child: const Text('Apartment for rent!'),
    ),
  ),
)

包含多个子Widget的布局容器

Row组件

Row组件字面理解就是代表一行,在一行中可以放入多个子组件。

        body: new Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text("hello"),
            new Container(
              width: 50.0,
              height: 50.0,
              color: Colors.red,
            ),
            new Text("world")
          ],
        )

Column组件

Column组件表示一列,可以在一列中放入多个组件,如下代码所示:

        body: new Column(
          children: <Widget>[
            new Text("hello"),
            new Text("world"),
            new Text("nihao~")
          ],
        )

Stack组件

Stack组件类似于Android中的FrameLayout,其中的子组件是一层层堆起来的,并不像Row或者Column中的子组件,按水平或垂直方向排列,下面用代码说明

        body: new Stack(
          children: <Widget>[
            new Container(
              width: 100.0,
              height: 100.0,
              color: Colors.red,
            ),
            new Container(
              width: 30.0,
              height: 30.0,
              color: Colors.green,
            )
          ],
        )
      ),

在上面的Stack组件中,放入了两个Container,其中第一个Container是100x100大小,第二个Container是30x30大小,在模拟器上运行效果如下图:

IndexedStack组件

IndexedStack用于根据索引来显示子组件,index为0则显示第一个子组件,index为1则显示第二个子组件,以此类推,下面用代码说明:

Table组件

Table组件用于显示多行多列的布局,如果只有一行或者一列,使用Row或者Column更高效。下面用一段代码展示Table的用法:

class MyApp extends StatelessWidget {

  // 生成Table中的数据
  List<TableRow> getData() {
    var data = [
      "hello",
      "world"
    ];
    List<TableRow> result = new List<TableRow>();
    TextStyle style = new TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold);
    for (int i = 0; i < data.length; i++) {
      String str = data[i];
      List<Widget> row = new List();
      for (int j = 0; j < str.length; j++) {
        row.add(new Text(" ${str[j]} ", style: style));
      }
      result.add(new TableRow(
        children: row
      ));
    }
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: "Test",
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Test")
        ),
        body: new Table(
          children: getData()
        )
      ),
    );
  }
}

Wrap组件

Wrap组件可以在水平或垂直方向上多行显示其子组件,下面是示例代码:

        body: new Wrap(
          spacing: 5.0, // 水平方向上两个子组件的间距
          runSpacing: 20.0, // 两行的垂直间距
          children: <Widget>[
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
            new Text("hello"),
          ],
        )
      ),

如果你把上面代码中的Wrap换成Row,你会发现Row中的子组件超过屏幕宽度后,不会自动换行显示。