跳至主要内容

使用 Flutter 构建用户界面

Flutter Widget 使用现代框架构建,该框架借鉴了 React。核心思想是使用 Widget 构建 UI。Widget 描述了在给定其当前配置和状态的情况下其视图应该是什么样子。当 Widget 的状态发生变化时,Widget 会重建其描述,框架会将该描述与之前的描述进行比较,以确定在底层渲染树中从一种状态过渡到另一种状态所需的最小更改。

Hello world

#

最小的 Flutter 应用只需使用 Widget 调用 runApp() 函数

import 'package:flutter/material.dart';

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

runApp() 函数获取给定的 Widget 并将其设为 Widget 树的根。在本例中,Widget 树由两个 Widget 组成,即 Center Widget 及其子 Widget,即 Text Widget。框架强制根 Widget 覆盖屏幕,这意味着文本“Hello, world”最终会居中显示在屏幕上。在此实例中需要指定文本方向;稍后将演示,当使用 MaterialApp Widget 时,它会自动处理此问题。

在编写应用时,您通常会创建新的 Widget,这些 Widget 是 StatelessWidgetStatefulWidget 的子类,具体取决于您的 Widget 是否管理任何状态。Widget 的主要工作是实现 build() 函数,该函数使用其他更低级别的 Widget 来描述 Widget。框架依次构建这些 Widget,直到该过程最终以表示底层 RenderObject 的 Widget 结束,后者计算并描述 Widget 的几何形状。

基本 Widget

#

Flutter 附带了一套功能强大的基本 Widget,其中以下 Widget 常用

文本
Text Widget 允许您在应用中创建一段带样式的文本。
RowColumn
这些弹性 Widget 允许您在水平 (Row) 和垂直 (Column) 方向上创建灵活的布局。这些对象的的设计基于 Web 的 flexbox 布局模型。
Stack
Stack Widget 不会线性排列(水平或垂直),而是允许您按绘制顺序将 Widget 彼此叠加。然后,您可以在 Stack 的子 Widget 上使用 Positioned Widget,以便相对于堆栈的顶部、右侧、底部或左侧边缘定位它们。堆栈基于 Web 的绝对定位布局模型。
Container
Container Widget 允许您创建一个矩形可视元素。容器可以使用 BoxDecoration 进行装饰,例如背景、边框或阴影。Container 还可以应用边距、填充和约束来限制其大小。此外,Container 可以使用矩阵在三维空间中进行变换。

以下是一些结合了这些 Widget 和其他 Widget 的简单 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 个设备独立像素且左右内边距均为 8 像素的 Container。在容器内部,MyAppBar 使用 Row 布局来组织其子 Widget。中间的子 Widget,即 title Widget,被标记为 Expanded,这意味着它会扩展以填充尚未被其他子 Widget 使用的任何剩余可用空间。您可以有多个 Expanded 子 Widget,并使用 flex 参数传递给 Expanded 来确定它们消耗可用空间的比例。

MyScaffold Widget 在垂直列中组织其子 Widget。在列的顶部,它放置了一个 MyAppBar 的实例,并向应用栏传递了一个 Text Widget 作为其标题。将 Widget 作为参数传递给其他 Widget 是一种强大的技术,它允许您创建可以在各种情况下重复使用的通用 Widget。最后,MyScaffold 使用 Expanded 用其主体填充剩余空间,主体包含一个居中的消息。

有关更多信息,请查看 布局

使用 Material 组件

#

Flutter 提供了许多 Widget,可以帮助您构建符合 Material Design 的应用。Material 应用以 MaterialApp Widget 开头,该 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),
      ),
    );
  }
}

现在代码已从 MyAppBarMyScaffold 切换到 AppBarScaffold Widget,以及从 material.dart 切换,应用开始看起来更像 Material。例如,应用栏有阴影,标题文本会自动继承正确的样式。还添加了一个浮动操作按钮。

请注意,Widget 作为参数传递给其他 Widget。Scaffold Widget 将许多不同的 Widget 作为命名参数获取,每个 Widget 都放置在 Scaffold 布局的适当位置。类似地,AppBar Widget 允许您为 leading Widget 和 actions Widget 的 title Widget 传递 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 提供可选回调。例如,IconButtonElevatedButtonFloatingActionButton Widget 具有 onPressed() 回调,当用户点击 Widget 时会触发这些回调。

有关更多信息,请查看 Flutter 中的手势

响应输入更改 Widget

#

到目前为止,此页面仅使用了无状态 Widget。无状态 Widget 从其父 Widget 接收参数,并将这些参数存储在 final 成员变量中。当请求 Widget 进行 build() 时,它会使用这些存储的值为其创建的 Widget 推导出新参数。

为了构建更复杂的体验(例如,以更有趣的方式响应用户输入),应用通常会携带一些状态。Flutter 使用 StatefulWidgets 来捕获此概念。StatefulWidgets 是特殊的 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(),
        ),
      ),
    ),
  );
}

您可能想知道为什么 StatefulWidgetState 是单独的对象。在 Flutter 中,这两种类型的对象具有不同的生命周期。Widgets 是临时对象,用于构建应用在其当前状态下的呈现。另一方面,State 对象在对 build() 的调用之间是持久存在的,允许它们记住信息。

上面的示例接受用户输入,并在其 build() 方法中直接使用结果。在更复杂的应用中,Widget 层次结构的不同部分可能负责不同的关注点;例如,一个 Widget 可能会呈现一个复杂的 UI,其目标是收集特定信息(例如日期或位置),而另一个 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(),
        ),
      ),
    ),
  );
}

请注意创建了两个新的无状态小部件,清晰地分离了显示计数器 (CounterDisplay) 和更改计数器 (CounterIncrementor) 的关注点。尽管最终结果与之前的示例相同,但责任的分离允许在各个小部件中封装更大的复杂性,同时保持父级小部件的简单性。

更多信息,请查看

整合所有内容

#

接下来是一个更完整的示例,它将这些概念结合在一起:一个假设的购物应用程序显示各种待售产品,并维护一个用于预期购买的购物车。首先定义表示类 ShoppingListItem

import 'package:flutter/material.dart';

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

  final String name;
}

typedef CartChangedCallback = 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 小部件遵循无状态小部件的常见模式。它将其构造函数中接收到的值存储在final 成员变量中,然后在build() 函数期间使用这些值。例如,inCart 布尔值在两种视觉外观之间切换:一种使用当前主题的主要颜色,另一种使用灰色。

当用户点击列表项时,小部件不会直接修改其 inCart 值。相反,小部件会调用其父小部件接收到的 onCartChanged 函数。此模式允许您在小部件层次结构中更高的地方存储状态,这会导致状态持续更长时间。在极端情况下,传递给runApp() 的小部件上存储的状态会持续应用程序的整个生命周期。

当父级接收到 onCartChanged 回调时,父级会更新其内部状态,这会触发父级重建并创建一个具有新 inCart 值的 ShoppingListItem 的新实例。尽管父级在重建时会创建一个新的 ShoppingListItem 实例,但此操作成本低廉,因为框架会将新构建的小部件与先前构建的小部件进行比较,并且仅将差异应用于底层的RenderObject

这是一个存储可变状态的父级小部件示例

import 'package:flutter/material.dart';

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

  final String name;
}

typedef CartChangedCallback = 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,这意味着此小部件存储可变状态。当 ShoppingList 小部件首次插入树中时,框架会调用createState() 函数来创建一个新的 _ShoppingListState 实例,并将其与树中的该位置关联。(请注意,State 的子类通常以下划线开头命名,以指示它们是私有的实现细节。)当此小部件的父级重建时,父级会创建一个新的 ShoppingList 实例,但框架会重用树中已存在的 _ShoppingListState 实例,而不是再次调用 createState

要访问当前 ShoppingList 的属性,_ShoppingListState 可以使用其widget 属性。如果父级重建并创建了一个新的 ShoppingList,则 _ShoppingListState 会使用新的 widget 值重建。如果您希望在 widget 属性更改时收到通知,请覆盖didUpdateWidget() 函数,该函数会传递一个 oldWidget 以便您将旧 widget 与当前 widget 进行比较。

在处理 onCartChanged 回调时,_ShoppingListState 通过将产品添加到 _shoppingCart 或从中删除产品来更改其内部状态。为了向框架发出其内部状态已更改的信号,它将这些调用包装在一个setState() 调用中。调用 setState 会将此小部件标记为脏状态,并将其安排在下一次应用程序需要更新屏幕时重建。如果您在修改小部件的内部状态时忘记调用 setState,则框架将不知道您的 widget 是脏状态,并且可能不会调用 widget 的build() 函数,这意味着用户界面可能不会更新以反映更改后的状态。通过以这种方式管理状态,您无需为创建和更新子小部件编写单独的代码。相反,您只需实现 build 函数,该函数处理这两种情况。

响应 Widget 生命周期事件

#

StatefulWidget 上调用createState() 后,框架会将新的状态对象插入树中,然后在状态对象上调用initState()State 的子类可以覆盖 initState 以执行只需要发生一次的工作。例如,覆盖 initState 以配置动画或订阅平台服务。initState 的实现需要先调用 super.initState

当不再需要状态对象时,框架会在状态对象上调用dispose()。覆盖 dispose 函数以执行清理工作。例如,覆盖 dispose 以取消计时器或取消订阅平台服务。dispose 的实现通常以调用 super.dispose 结束。

更多信息,请查看State

#

使用键来控制框架在小部件重建时将哪些小部件与其他小部件匹配。默认情况下,框架会根据小部件的runtimeType 和它们出现的顺序来匹配当前构建和先前构建中的小部件。使用键,框架要求这两个小部件具有相同的key 以及相同的 runtimeType

键在构建许多相同类型的小部件实例的小部件中最有用。例如,ShoppingList 小部件,它构建了足够多的 ShoppingListItem 实例以填充其可见区域

  • 如果没有键,当前构建中的第一个条目将始终与先前构建中的第一个条目同步,即使从语义上讲,列表中的第一个条目刚刚滚动出屏幕,并且在视口中不再可见。

  • 通过为列表中的每个条目分配一个“语义”键,无限列表可以更高效,因为框架会将具有匹配语义键且因此具有相似(或相同)视觉外观的条目同步。此外,以语义方式同步条目意味着保留在有状态子小部件中的状态会保持附加到相同的语义条目,而不是视口中相同数字位置的条目。

更多信息,请查看Key API。

全局键

#

使用全局键来唯一标识子小部件。全局键必须在整个小部件层次结构中全局唯一,这与只需要在同级之间唯一的本地键不同。由于它们是全局唯一的,因此全局键可用于检索与小部件关联的状态。

更多信息,请查看GlobalKey API。