跳转至正文

使用 Flutter 构建界面

介绍如何用 Flutter 构建界面

Flutter widget 采用受 React 启发的现代框架构建。核心思想是用 widget 构建 UI。 Widget 根据当前配置和状态描述其视图应有的外观。当 widget 的状态改变时,widget 会重建其描述,框架将其与先前的描述进行 diff,以确定底层渲染树从一种状态过渡到另一种状态所需的最小变更。

Hello world

#

最简 Flutter 应用只需用 widget 调用 runApp() 函数:

import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.blue),
      ),
    ),
  );
}

runApp() 函数接收给定的 Widget 并将其设为 widget 树的根。本例中 widget 树由两个 widget 组成:Center 及其子节点 Text。框架强制根 widget 覆盖整个屏幕,因此「Hello, world」文字会显示在屏幕中央。此例需指定文字方向;使用 MaterialApp widget 时会自动处理,后文将演示。

编写应用时,你通常会编写继承 StatelessWidget 或 StatefulWidget 的新 widget,取决于 widget 是否管理状态。 widget 的主要工作是实现 build() 函数,用更低层级的 widget 描述自身。框架依次构建这些 widget,直至底层由表示 RenderObject 的 widget 结束,由后者计算并描述 widget 的几何信息。

基础 widget

#

Flutter 自带一系列强大的基础 widget,以下是常用的一些:

Text
Text widget 让你在应用中创建一段样式化文本。

Row, Column
这些 flex widget 让你在水平 (Row) 和垂直 (Column) 方向创建灵活布局,其设计基于 Web 的 flexbox 布局模型。

Stack
Stack widget 不按线性方向(水平或垂直)排列,而按绘制顺序将 widget 叠放。可在 Stack 的子节点上使用 Positioned widget,相对于栈的上、右、下、左边缘定位。Stack 基于 Web 的绝对定位布局模型。

Container
Container widget 用于创建矩形视觉元素,可用 BoxDecoration 装饰背景、边框或阴影,也可设置外边距、内边距和尺寸约束,还可用矩阵在三维空间中变换。

下面是组合这些及其他 widget 的一些简单示例:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  const MyAppBar({required this.title, super.key});

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        children: [
          const IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child
          // to fill the available space.
          Expanded(child: title),
          const IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  const MyScaffold({super.key});

  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece
    // of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: [
          MyAppBar(
            title: Text(
              'Example title',
              style:
                  Theme.of(context) //
                      .primaryTextTheme
                      .titleLarge,
            ),
          ),
          const Expanded(child: Center(child: Text('Hello, world!'))),
        ],
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: 'My app', // used by the OS task switcher
      home: SafeArea(child: MyScaffold()),
    ),
  );
}

请确保在 pubspec.yaml 的 flutter 段中包含 uses-material-design: true 条目。这样你才能使用预定义的 Material 图标 集。若使用 Materials 库,通常建议包含这一行。

yaml
name: my_app
flutter:
  uses-material-design: true

许多 Material Design widget 需要放在 MaterialApp 内才能正确显示并继承主题数据。因此请用 MaterialApp 运行应用。

MyAppBar widget 创建一个高度为 56 逻辑像素的 Container,左右内边距各为 8 像素。在容器内,MyAppBar 使用 Row 布局组织子节点。中间的 title widget 标记为 Expanded,表示它会扩展以填满其他子节点未占用的剩余空间。可以有多个 Expanded 子节点,并通过 Expanded 的 flex 参数决定它们占用可用空间的比例。

MyScaffold widget 在垂直列中组织子节点。列顶部放置 MyAppBar 实例,并向应用栏传入用作标题的 Text widget。将 widget 作为参数传给其他 widget 是一种强大技巧,可创建可在多种场景复用的通用 widget。最后,MyScaffold 用 Expanded 以居中消息填充剩余空间作为 body。

更多信息请参阅 布局。

使用 Material 组件

#

Flutter 提供多种 widget,帮助你构建符合 Material Design 的应用。 Material 应用以 MaterialApp widget 开头,它在应用根节点构建多种实用 widget,包括 Navigator——管理以字符串标识的 widget 栈,即「路由」。Navigator 让你在应用各界面间平滑过渡。使用 MaterialApp widget 完全可选,但是良好实践。

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(title: 'Flutter Tutorial', home: TutorialHome()));
}

class TutorialHome extends StatelessWidget {
  const TutorialHome({super.key});

  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for
    // the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: const Text('Example title'),
        actions: const [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: const Center(child: Text('Hello, world!')),
      floatingActionButton: const FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        onPressed: null,
        child: Icon(Icons.add),
      ),
    );
  }
}

现在代码已从 MyAppBar 和 MyScaffold 切换为 AppBar 和 Scaffold widget,并改用 material.dart,应用开始更具 Material 风格。例如,应用栏带有阴影,标题文字会自动继承正确样式,还添加了浮动操作按钮。

注意 widget 会作为参数传给其他 widget。 Scaffold widget 接收多种不同 widget 作为命名参数,各自放在 Scaffold 布局的合适位置。同样,AppBar widget 让你为 leading、title 的 actions 传入 widget。这一模式在框架中反复出现,设计自己的 widget 时也可考虑采用。

更多信息请参阅 Material 组件 widget。

处理手势

#

大多数应用都包含与系统的某种用户交互。构建交互式应用的第一步是检测输入手势。通过创建一个简单按钮来了解其工作原理:

import 'package:flutter/material.dart';

class MyButton extends StatelessWidget {
  const MyButton({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50,
        padding: const EdgeInsets.all(8),
        margin: const EdgeInsets.symmetric(horizontal: 8),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5),
          color: Colors.lightGreen[500],
        ),
        child: const Center(child: Text('Engage')),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(body: Center(child: MyButton())),
    ),
  );
}

GestureDetector widget 没有视觉表现,而是检测用户做出的手势。当用户点击 Container 时,GestureDetector 会调用其 onTap() 回调,本例中向控制台打印消息。你可以用 GestureDetector 检测多种输入手势,包括点击、拖动和缩放。

许多 widget 内部使用 GestureDetector 为其他 widget 提供可选回调。例如,IconButton、ElevatedButton 和 FloatingActionButton widget 具有 onPressed() 回调,在用户点击 widget 时触发。

更多信息请参阅 Flutter 中的手势。

根据输入更改 widget

#

到目前为止,本页只使用了无状态 widget。无状态 widget 从父 widget 接收参数,并存入 final 成员变量。当要求 widget build() 时,它用这些存储的值为其创建的 widget 推导新参数。

要构建更复杂的体验——例如以更有趣的方式响应用户输入——应用通常需要持有一些状态。 Flutter 用 StatefulWidget 表达这一概念。 StatefulWidget 是知道如何生成 State 对象的特殊 widget,再由 State 对象保存状态。下面是一个使用前文 ElevatedButton 的基础示例:

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // This class is the configuration for the state.
  // It holds the values (in this case nothing) provided
  // by the parent and used by the build  method of the
  // State. Fields in a Widget subclass are always marked
  // "final".

  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework
      // that something has changed in this State, which
      // causes it to rerun the build method below so that
      // the display can reflect the updated values. If you
      // change _counter without calling setState(), then
      // the build method won't be called again, and so
      // nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called,
    // for instance, as done by the _increment method above.
    // The Flutter framework has been optimized to make
    // rerunning build methods fast, so that you can just
    // rebuild anything that needs updating rather than
    // having to individually changes instances of widgets.
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(onPressed: _increment, child: const Text('Increment')),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(body: Center(child: Counter())),
    ),
  );
}

你可能会疑惑为何 StatefulWidget 与 State 是分开的对象。在 Flutter 中,这两类对象生命周期不同。 Widget 是临时对象,用于构建应用在某一状态下的呈现。 State 对象则在多次调用 build() 之间保持存在,从而能记住信息。

上面的示例接受用户输入并直接在 build() 方法中使用结果。在更复杂的应用中,widget 树的不同部分可能负责不同关注点;例如,一个 widget 可能展示用于收集日期或位置等特定信息的复杂界面,另一个 widget 则可能用这些信息改变整体呈现。

在 Flutter 中,变更通知通过回调沿 widget 层次结构向上流动,当前状态则向下流向负责呈现的无状态 widget。重定向这一流动的共同父级是 State。下面稍复杂的示例展示其实际运作方式:

import 'package:flutter/material.dart';

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({required this.count, super.key});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  const CounterIncrementor({required this.onPressed, super.key});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: onPressed, child: const Text('Increment'));
  }
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        CounterIncrementor(onPressed: _increment),
        const SizedBox(width: 16),
        CounterDisplay(count: _counter),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(body: Center(child: Counter())),
    ),
  );
}

注意这里创建了两个新的无状态 widget,清晰分离了 显示 计数器 (CounterDisplay) 与 修改 计数器 (CounterIncrementor) 的职责。尽管总体结果与前一示例相同,职责分离使各 widget 能封装更复杂的逻辑,同时保持父 widget 简洁。

更多信息请参阅:

综合示例

#

下面是一个更完整的示例,综合上述概念:假设某购物应用展示待售商品,并维护意向购买的购物车。先从定义呈现类 ShoppingListItem 开始:

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = void Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: ShoppingListItem(
            product: const Product(name: 'Chips'),
            inCart: true,
            onCartChanged: (product, inCart) {},
          ),
        ),
      ),
    ),
  );
}

ShoppingListItem widget 遵循无状态 widget 的常见模式:将构造函数接收的值存入 final 成员变量,并在 build() 函数中使用。例如,inCart 布尔值在两种视觉外观间切换:一种使用当前主题的主色,另一种使用灰色。

当用户点击列表项时,widget 不会直接修改 inCart 值,而是调用从父 widget 收到的 onCartChanged 函数。这一模式让你能把状态保存在 widget 层次结构更高处,使状态持续更久。极端情况下,传给 runApp() 的 widget 上保存的状态会贯穿整个应用生命周期。

当父级收到 onCartChanged 回调时,会更新内部状态,从而触发父级重建并创建带有新 inCart 值的 ShoppingListItem 新实例。尽管父级重建时会创建新的 ShoppingListItem 实例,但这一操作开销很小,因为框架会将新构建的 widget 与先前构建的 widget 比较,并仅将差异应用到底层 RenderObject。

下面是一个保存可变状态的父 widget 示例:

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = void Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

class ShoppingList extends StatefulWidget {
  const ShoppingList({required this.products, super.key});

  final List<Product> products;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

  @override
  State<ShoppingList> createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shopping List')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8),
        children: widget.products.map((product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: 'Shopping App',
      home: ShoppingList(
        products: [
          Product(name: 'Eggs'),
          Product(name: 'Flour'),
          Product(name: 'Chocolate chips'),
        ],
      ),
    ),
  );
}

ShoppingList 类继承 StatefulWidget,表示该 widget 保存可变状态。当 ShoppingList widget 首次插入树时,框架调用 createState() 创建新的 _ShoppingListState 实例并与树中该位置关联。(注意 State 的子类通常以下划线开头,表示它们是私有实现细节。)当该 widget 的父级重建时,父级会创建新的 ShoppingList 实例,但框架会复用树中已有的 _ShoppingListState 实例,而不会再次调用 createState。

要访问当前 ShoppingList 的属性,_ShoppingListState 可使用其 widget 属性。若父级重建并创建新的 ShoppingList,_ShoppingListState 会用新的 widget 值重建。若希望在 widget 属性变化时收到通知,可重写 didUpdateWidget() 函数,它会传入 oldWidget 以便你将旧 widget 与当前 widget 比较。

处理 onCartChanged 回调时, _ShoppingListState 通过向 _shoppingCart 添加或移除商品来变更内部状态。为向框架表明内部状态已改变,这些调用应包在 setState() 中。调用 setState 会将该 widget 标记为 dirty,并在应用下次需要更新屏幕时安排重建。若在修改 widget 内部状态时忘记调用 setState,框架不会知道 widget 已 dirty,可能不会调用 widget 的 build() 函数,界面也就可能不会反映变更后的状态。用这种方式管理状态时,你无需为创建和更新子 widget 分别编写代码,只需实现 build 函数,它可同时处理两种情况。

响应 widget 生命周期事件

#

在 StatefulWidget 上调用 createState() 之后,框架将新的 state 对象插入树,然后在该 state 对象上调用 initState()。 State 的子类可重写 initState 以执行只需进行一次的工作,例如配置动画或订阅平台服务。initState 的实现必须先调用 super.initState。

当不再需要 state 对象时,框架会在该 state 对象上调用 dispose()。可重写 dispose 函数进行清理,例如取消定时器或取消订阅平台服务。 dispose 的实现通常以调用 super.dispose 结束。

更多信息请参阅 State。

Key

#

使用 key 可控制 widget 重建时框架将哪些 widget 相互匹配。默认情况下,框架根据 runtimeType 及出现顺序匹配当前构建与先前构建中的 widget。有了 key,框架还要求两个 widget 具有相同的 key 以及相同的 runtimeType。

key 在构建大量同类型 widget 实例时最有用。例如 ShoppingList widget 会构建刚好填满可见区域的 ShoppingListItem 实例:

  • 没有 key 时,当前构建中的第一项总会与先前构建中的第一项同步,即使从语义上讲列表第一项已滚出屏幕、在视口中不再可见。

  • 为列表中每项分配「语义」key 后,无限列表可以更高效,因为框架会同步具有匹配语义 key 的项,从而保持相似(或相同)的视觉外观。此外,按语义同步项意味着有状态子 widget 中保留的状态会附着在相同语义项上,而不是视口中相同数值位置的项上。

更多信息请参阅 Key API。

Global keys

#

Global key

#

使用 global key 可唯一标识子 widget。 global key 必须在整个 widget 层次结构中全局唯一,而 local key 只需在兄弟节点间唯一。由于全局唯一,global key 可用于获取与 widget 关联的 state。

更多信息请参阅 GlobalKey API。