Skip to main content

使用Flutter构建用户界面

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

Hello world

#

最小的Flutter应用程序只需使用一个部件调用runApp()函数:

import 'package:flutter/material.dart';

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

runApp()函数接收给定的Widget并将其设置为部件树的根。在这个例子中,部件树由两个部件组成,Center部件及其子部件Text部件。框架强制根部件覆盖屏幕,这意味着文本“Hello, world”最终会居中显示在屏幕上。在这个例子中需要指定文本方向;当使用MaterialApp部件时,这将自动处理,如下所示。

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

基本部件

#

Flutter带有一套强大的基本部件,其中以下部件常用:

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

下面是一些结合了这些部件和其他部件的简单部件:

import 'package:flutter/material.dart';

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

  // Widget子类中的字段始终标记为“final”。

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56, // 以逻辑像素为单位
      padding: const EdgeInsets.symmetric(horizontal: 8),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row是一个水平线性布局。
      child: Row(
        children: [
          const IconButton(
            icon: Icon(Icons.menu),
            tooltip: '导航菜单',
            onPressed: null, // null禁用按钮
          ),
          // Expanded扩展其子元素
          // 以填充可用空间。
          Expanded(
            child: title,
          ),
          const IconButton(
            icon: Icon(Icons.search),
            tooltip: '搜索',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    // Material是UI显示的
    // 概念上的纸张。
    return Material(
      // Column是一个垂直线性布局。
      child: Column(
        children: [
          MyAppBar(
            title: Text(
              '示例标题',
              style: Theme.of(context) //
                  .primaryTextTheme
                  .titleLarge,
            ),
          ),
          const Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: '我的应用程序', // 由操作系统任务切换器使用
      home: SafeArea(
        child: MyScaffold(),
      ),
    ),
  );
}

确保在您的pubspec.yaml文件的flutter部分中有一个uses-material-design: true条目。它允许您使用预定义的Material图标。如果您使用的是Material库,通常最好包含这一行。

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

许多Material Design部件需要位于MaterialApp内部才能正确显示,以便继承主题数据。因此,请使用MaterialApp运行应用程序。

MyAppBar部件创建一个高度为56个设备无关像素、内部左右填充为8像素的Container。在容器内部,MyAppBar使用Row布局来组织其子部件。中间的子部件,即title部件,被标记为Expanded,这意味着它会扩展以填充其他子部件未使用的任何剩余可用空间。您可以有多个Expanded子部件,并使用Expandedflex参数确定它们消耗可用空间的比率。

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

更多信息,请查看布局

使用Material组件

#

Flutter提供许多部件来帮助您构建遵循Material Design的应用程序。Material应用程序以MaterialApp部件开头,该部件在应用程序的根部构建许多有用的部件,包括一个Navigator,它管理一个由字符串标识的部件堆栈,也称为“路由”。Navigator允许您在应用程序的屏幕之间平滑过渡。使用MaterialApp部件完全是可选的,但这是一个好习惯。

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    // Scaffold是主要Material组件的布局。
    return Scaffold(
      appBar: AppBar(
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: '导航菜单',
          onPressed: null,
        ),
        title: const Text('示例标题'),
        actions: const [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: '搜索',
            onPressed: null,
          ),
        ],
      ),
      // body是屏幕的大部分区域。
      body: const Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: const FloatingActionButton(
        tooltip: '添加', // 由辅助技术使用
        onPressed: null,
        child: Icon(Icons.add),
      ),
    );
  }
}

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

请注意,部件作为参数传递给其他部件。Scaffold部件将许多不同的部件作为命名参数接收,每个部件都放置在Scaffold布局中的适当位置。类似地,AppBar部件允许您为leading部件和actions部件的title部件传递部件。这种模式贯穿整个框架,您在设计自己的部件时可能会考虑这一点。

更多信息,请查看Material组件部件

处理手势

#

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

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton被点击了!');
      },
      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部件没有可视化表示,而是检测用户做出的手势。当用户点击Container时,GestureDetector会调用其onTap()回调,在本例中是向控制台打印一条消息。您可以使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放。

许多部件使用GestureDetector为其他部件提供可选回调。例如,IconButtonElevatedButtonFloatingActionButton部件具有onPressed()回调,当用户点击部件时会触发这些回调。

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

响应输入更改部件

#

到目前为止,本页只使用了无状态部件。无状态部件从其父部件接收参数,并将其存储在final成员变量中。当请求部件build()时,它使用这些存储的值为其创建的部件派生新参数。

为了构建更复杂的体验——例如,以更有趣的方式响应用户输入——应用程序通常会携带一些状态。Flutter使用StatefulWidgets来捕捉这个概念。StatefulWidgets是特殊的部件,它们知道如何生成State对象,然后使用这些对象来保存状态。考虑这个基本的例子,使用前面提到的ElevatedButton

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // 此类是状态的配置。
  // 它保存由父级提供的(在本例中什么也没有)
  // 并由State的build方法使用的值。Widget子类中的字段始终标记为
  // "final"。

  const Counter({super.key});

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

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

  void _increment() {
    setState(() {
      // 此setState调用告诉Flutter框架
      // 此State中有一些内容已更改,这
      // 会导致它重新运行下面的build方法,以便
      // 显示可以反映更新的值。如果您
      // 在不调用setState()的情况下更改_counter,那么
      // build方法将不会再次被调用,因此
      // 看起来什么也不会发生。
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 每次调用setState时都会重新运行此方法,
    // 例如,如上面的_increment方法所示。
    // Flutter框架已被优化,可以
    // 快速重新运行build方法,以便您可以只
    // 重新构建需要更新的任何内容,而不是
    // 必须单独更改部件的实例。
    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()方法中直接使用结果。在更复杂的应用程序中,部件层次结构的不同部分可能负责不同的关注点;例如,一个部件可能呈现一个复杂的UI,其目标是收集特定信息,例如日期或位置,而另一个部件可能使用该信息来更改整体呈现。

在Flutter中,更改通知通过回调“向上”流动到部件层次结构,而当前状态“向下”流动到执行呈现的无状态部件。重定向此流的公共父级是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) {
    // 主题取决于BuildContext,因为树的不同
    // 部分可以有不同的主题。
    // BuildContext指示构建发生的位置,因此指示使用哪个主题。

    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) {
    // 主题取决于BuildContext,因为树的不同
    // 部分可以有不同的主题。
    // BuildContext指示构建发生的位置,因此指示使用哪个主题。

    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;

  // 框架在部件第一次出现在树中给定位置时调用createState。
  // 如果父级重新构建并使用相同类型的
  // 部件(具有相同的键),框架会重用
  // State对象,而不是创建新的State对象。

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

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

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // 当用户更改购物车中的内容时,您需要
      // 在setState调用中更改_shoppingCart来
      // 触发重新构建。
      // 然后,框架调用build,如下所示,
      // 这会更新应用程序的视觉外观。

      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属性更改时收到通知,请重写didUpdateWidget()函数,该函数会传递一个oldWidget,让您可以将旧部件与当前部件进行比较。

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

响应部件生命周期事件

#

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。