Flutter小部件介绍

271 阅读14分钟

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

一、Hello World

哈哈,学习一门语言当然要从Hello World学起了

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

import 'package:flutter/cupertino.dart';

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

Screenshot_20230112-234847.png

该`runApp()`函数给定`Widget`并使其成为小部件树的根。在此示例中,小部件树由两个小部件组成,`Center`部件及其子`Text`部件。该框架强制根部件覆盖屏幕,这意味着文本“Hello,world”最终在屏幕上居中。在这种情况下需要制定文本防线;当使用该`MaterialApp`小部件时,它会为您处理好,如后文所示。

在编写应用程序时,您通常会编写新的小部件,这些小部件是StatelessWidgetStatefulWidget的子类,具体取决于您的小部件是否管理任何状态。小部件的主要工作是实现一个build()功能,该功能根据其他较低级别的小部件来描述小部件。该框架依次构建这些小部件,直到该过程在代表底层RenderObject的小部件中触底,计算和描述小部件的集合形状。

二、基本部件

Flutter自带了一套强大的基础 widgets,其中常用的有:

Text:该Text小部件可让您的应用程序中创建一些列带样式的文本。

Row,Column:这些flex小部件可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。这些对象的设计基于网络的flexbox布局模型。

Stack:小部件不是线性定向(水平或垂直),而是让您可以将widget按绘制顺序放置在彼此之上。然后,您可以在Stack的子项上使用Positioned小部件,以相对应于堆栈顶部、右侧、底部或左侧边缘定位它们。堆栈基于网格的绝对定位布局模型。

Container:该Container小部件可让您创建矩形视觉元素。容器可以用装饰BoxDecoration,例如背景、边框或阴影。Container还可以对其大小应用边距、填充和约束。此外,Container可以使用矩阵在三维空间中进行变换。

import 'package:flutter/material.dart';

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

class MyScaffold extends StatelessWidget {
  const MyScaffold({Key? key}) : super(key: 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.headline6,
            ),
          ),
          const Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

class MyAppBar extends StatelessWidget {
  // Fields in a Widget subclass are always marked "final".
  final Widget title;

  const MyAppBar({Key? key, required this.title}) : super(key: key);

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

Screenshot_20230113-011146.png

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

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

许多Material Design小部件需要MaterialApp内部才能正确显示,以便集成主题数据。因此,使用MaterialApp运行程序。

MyAppBar小部件创建一个容器,其高度为56个设备独立像素,内部填充为8个像素,左侧和右侧均如此。在容器内部,MyAppBar使用Row布局来组织其子项中间的孩子,标题小部件,被标记为Expanded,这意味着它会扩展以填充任何剩余的可用空间。您可以拥有多个Expanded子项,并使用Expandedflex参数确定它们占有可用空间的比率。

MyScaffold小部件在垂直列中组织其子项。在列表的顶部,它放置了一个MyAppBar的实例。向app bar传递一个文本小部件以用作其标题。将小部件作为参数传递给其他小部件是一种强大的技术,它使用您可以创建可以以多种方式重用的通用小部件。最后,MyScaffold使用Expanded来用它的主体填充剩余空间,它由居中的消息组成。

三、使用Material组件

Flutter提供了许多小部件,可帮助您构建遵循Material Design的应用程序。Material应用程序从MaterialApp小部件开始,它在你的应用程序的根部构建了许多有用的小部件,包括一个Navigator,它管理一堆由字符串标识的小部件,也称为“路由”。Navigator使您可以在应用程序的屏幕之间平滑过渡。使用MaterialApp小部件完全是可选的,但却是一种很好的做法。

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Flutter Tutorial', // used by the OS task switcher
      home: TutorialHome(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    // Material 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(
            onPressed: null, 
            tooltip: 'Search',
            icon: Icon(Icons.search),
          ),
        ],
      ),
      // body is the majority of the screen
      body: const Center(
        child: Text('Hello, world'),
      ),
      floatingActionButton: const FloatingActionButton(
        tooltip: 'Add',// used by assistive technologies
          child: Icon(Icons.add),
          onPressed: null,
      ),
    );
  }
}

Screenshot_20230113-044457.png

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

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

四、处理手势

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

import 'package:flutter/material.dart';

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

class MyButton extends StatelessWidget {
   const MyButton({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50.0,
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        padding: const EdgeInsets.all(8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: const Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

Screenshot_20230115-205452.png

GestureDetector小部件没有视觉表示,而是监测用户做出的手势。当用户点击Container时,GestureDetector调用其onTap()回调,在本示例中将消息打印到控制台。您可以使用GestureDetector监测各种手势,包括点击、拖动和缩放。

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

五、更改小部件以相应输入

到目前为止,该页面仅使用了无状态小部件。无状态小部件从它们的父小部件接收参数,这些参数存储在最终成员变量中。当一个小部件被要求build()时,它使用这些存储的值来为它创建的小部件派生新的参数。

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

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      )
    ),
  );
}
class Counter extends StatefulWidget {
  // 这个类是状态配置
  // 它保存提供的值(在本例没有)
  // 由父级并由build方法使用状态
  // Widget子类中的字段总被标记为"final"
  const Counter({Key? key}) : super(key: key);

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

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

  void _increment() {
    setState(() {
      // 这个"setState"的调用告诉Flutter框架
      // 一些变量发生变化,使它重新运行下面的build放啊
      // 如果不在不调用"setState()"的情况下更改_counter
      // 然后build方法不会被再次调用,似乎什么也不会发生变化。
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 每次调用setState时都会重新运行此方法
    // 例如,正如上面_increment方法所做的那样。
    // Flutter框架经过优化,使快速重新运行个构建
    // 方法,这样搞你就可以重建任何需要更改小部件的实例。
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16,),
        Text('Counter: $_counter'),
      ],
    );
  }
}

ezgif.com-gif-maker.gif

你可能想知道为什么StatefulWidgetState是独立的对象。在Flutter中,这两类对象有着不同的生命周期。小部件是临时对象,用于构建应用程序当前状态的表示。另一方面,状态对象在build()的调用之间是持久的,允许它们记住信息。

上面的示例接受用户输入并直接在其build()方法中使用结果。在更复杂的应用程序中,小部件层次结构的不同部分可能负责不同额度关注点;例如,一个小部件可能会呈现一个复杂的用户界面,目的是收集特定信息,例如日期或位置,而另一个小部件可能会使用该信息来改变整体呈现。

在Flutter中,更改通知通过回调的小部件层次结构中“up”流动,而当前状态“down”流动到进行展示的无状态小部件。重定向此流的公共父级是State。下面稍微复杂一点的例子展示了它在实践中试是如何工作的:

import 'package:flutter/material.dart';

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

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

  @override
  _CounterState 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),
      ],
    );
  }
}

class CounterIncrementor extends StatelessWidget {
  final VoidCallback onPressed;

  const CounterIncrementor({Key? key, required this.onPressed})
      : super(key: key);

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

class CounterDisplay extends StatelessWidget {
  final int count;

  const CounterDisplay({Key? key, required this.count}) : super(key: key);

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

ezgif.com-gif-maker.gif

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

有关详细信息,请参阅:

六、完整的例子,汇集上述概念

下面是一个更完整的例子,它汇集了这些概念:一个假设的购物程序显示各种待售产品,并维护一个购物车以供预期购买。首先定义表示类ShoppingLiistItem:

import 'package:flutter/material.dart';

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

class Product {
  final String name;

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

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

class ShoppingListItem extends StatelessWidget {
  final Product product;
  final bool inCart;
  final CartChangedCallback onChangedCallback;
  const ShoppingListItem(
      {
    Key? key,
    required this.product,
    required this.inCart,
    required this.onChangedCallback,
  }) : super(key: key);

  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: () {
        onChangedCallback(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context),),
    );
  }
}

Screenshot_20230116-012050.png

ShoppintListItem小部件遵循无状态小部件的通用模式。它将在其构造函数中接收到的存储在final成员变量中,然后在其build()函数中使用这些值。例如,inChart布尔值在两种视觉外观之间切换:一种使用当前主题的原色,另一种使用灰色。

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

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

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

import 'package:flutter/material.dart';

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

class Product {
  final String name;

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

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

class ShoppingListItem extends StatelessWidget {
  final Product product;
  final bool inCart;
  final CartChangedCallback onChangedCallback;

  const ShoppingListItem({
    Key? key,
    required this.product,
    required this.inCart,
    required this.onChangedCallback,
  }) : super(key: key);

  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: () {
        onChangedCallback(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(
        product.name,
        style: _getTextStyle(context),
      ),
    );
  }
}

class ShoppingList extends StatefulWidget {
  final List<Product> products;

  const ShoppingList({Key? key, required this.products}) : super(key: key);

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

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

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

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // 当用户更改购物车中的商品时,您需要在setState
      // 调用中将_shoppingCart更改为触发重建。
      // 然后框架调用构建,如下所示,更新应用程序的视觉外观。
      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.0),
        children: widget.products.map((product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onChangedCallback: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

ezgif.com-gif-maker (1).gif

ShoppingList类扩展了StatefulWidget,这意味着此小部件存储可变状态。当ShoppingList小部件首次插入到树中时,框架调用createState()函数来创建_ShoppingListState的新实例以与树中的该位置相关联。(请注意,State的子类通常以前导下划线命名,以表明它们是私有实现细节。)当此小部件的父级重建时,父级创建一个新的ShoppingList示例,但框架会重用树中已有的_ShoppingListState实例而不是再次调用createState

要访问当前ShoppingList的属性,_ShoppingListState可以使用其小部件属性。如果父级重建并创建一个新的ShoppingList,则ShoppingListState将使用新的小部件值重建。如果您希望在小部件属性更改时收到通知,请覆盖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结束。

7.1、key

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

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

  • 如果没有keys,当前构建中的第一条将始终与前一个构建中的第一个条目同步,即使从语义上讲,列表中的第一个条目只是滚出屏幕并且在屏幕中不可见。
  • 通过为列表中的每个条目分配你一个semantic键,无限列表可以更有效,因为框架将条目与匹配的语义键同步,因此具有相似(或相同)的视觉外观。此外,在语义上同步条目意味着保留在有状态子部件中的状态仍然附加到相同的语义条目,而不是屏幕中相同数字位置的条目。

7.2、GlobalKey

使用global keys来唯一标识子部件。全局键在整个小部件层次结构中必须是全局唯一的,不像local key只需要在兄弟之间是唯一的。因为它们所示全局唯一的,搜索哦咦可以使用全局键来检索与小部件关联的状态。