Skip to content

Latest commit

 

History

History
518 lines (411 loc) · 18.1 KB

04. Flutter用户交互 - Widgets.md

File metadata and controls

518 lines (411 loc) · 18.1 KB

User interface

Introduction to Widgets

widgets是一套响应式框架, 灵感来自React. 使用Widgets构建UI. Widgets描述view长什么样, 当前结构和状态. 当widget的状态改变, flutter框架会比较之前的描述信息, 以最小的改变, 重新构建widget

Hello World

最简单的Flutter app, runApp()中传入一个widget

import 'package:flutter/material.dart';

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

runApp()函数需要传入一个widget, 作为widget树的根. 上面例子中widget树有两个widget, Centerwidget和它的子widgetText. flutter框架强制根widget满屏.

app开发中, 通过创建一个新的widget继承自StatelessWidget(无状态) 或 StatefulWidget(有状态). 取决于是不是需要管理widget的状态. widget主要工作是实现build函数, 以此为基础来描述其他更低级别的widget. flutter框架依次构建那些widget直到最底部的RenderObject, 用来计算并描述widget的几何形状.

基础widgets Basic widgets

Text: 文本

Row, Column: 水平 (Row行) 和 垂直 (Column列) .

Stack: 非线性布局, Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置

Container: 创建矩形框, 可以由BoxDecoration进行装饰, 如背景, 边框和阴影. Container有边距(margins)、填充(padding)和约束(constraints)来确定尺寸. 可以通过矩阵将Container转成三维.
import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Widget子类中的字段都必须标记为final
  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @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(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null可以禁用按钮 null disables the button
          ),
          // Expanded 填充余下的可用空间
          // expands its child to fill the available space.
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material 一张纸
    // Material is a conceptual piece of paper on which the UI appears.
    return Material(
      // Column 垂直的线性布局
      // Column is a vertical, linear layout.
      child: Column(
        children: <Widget>[
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

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

在pubspec.yaml文件中, 将uses-material-design: true, 来使用Material icons.

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

许多widget需要放在MaterialApp中才能合理的显示, 继承主题数据. 因此在runApp中使用MaterialApp.

使用Material组件 Using Material Components

Flutter提供一些遵循Material设计的widgets. 一个Material app, 以MaterialApp widget开始, 构建一些基础的widget, 包括Navigator, 用于管理一堆以字符串为标识的widgets(也称routes). 使用MaterialApp widget不是必要的, 但是一个不错的练习.

import 'package:flutter/material.dart';

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

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

material.dart库中的AppBar和Scaffold widgets.

处理手势 Handling gestures

用户交互第一步是监听手势的输入.

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

GestureDetector用于监听用户的手势, 没有任何视觉效果. 当用户点击Container, GestureDetector调用onTap回调. GestureDetector可以监听很多输入手势, 如点击, 拖动, 拉伸.

许多widget通过GestureDetector提供一个可选的回调. 如 IconButton, RaisedButton, and FloatingActionButton widgets 的 onPressed 回调.

Changing widgets in response to input

StatelessWidget(无状态的widget), 接收父widget的参数, 并存储在final成员变量中. 当一个widget被要求构建时,它使用这些存储的值作为参数来构建widget

StatefulWidget(有状态的widget), 特殊的widget,它知道如何生成State对象来保持状态。

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".
  
  // 状态的结构
  // 保留父widget提供的数值, 来构建widget

  @override
  _CounterState 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 we changed _counter without calling
      // setState(), then the build method would not be called again,
      // and so nothing would appear to happen.
      
      // setState高度Flutter框架, 有状态改变
      // widget会重新运行下面的build方法, 来展示更新后的值
      // 如果只改变_counter, 没有调用setState(), 则build放发不会被调用, 不会显示新的状态
      
      _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 change
    // instances of widgets.
    
    // 每次setState调用时, 会重新运行该方法, 如调用上面的_increment方法
    // Flutter框架对重新运行build方法做了最佳优化, 因此在每次需要更新的时候都可以直接rebuild
    
    return Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

StatefulWidget和State是独立的对象, 有不同的生命周期.

  • StatefulWidget是临时的对象, 用来显示应用程序的当前状态
  • State对象在多次调用build()中依旧保持不变,使他们能够保存信息

上面的例子接受用户点击,并在点击时使_counter自增,然后直接在其build方法中使用_counter值。在更复杂的应用程序中,widget结构层次的不同部分可能有不同的职责; 例如,一个widget可能呈现一个复杂的用户界面,其目标是收集特定信息(如日期或位置),而另一个widget可能会使用该信息来更改整体的显示。

在Flutter中,状态改变的通知, 通过回调的形式“向上”流动,而当前状态“向下”流动到stateless widget来做展示.重定向这一流程的共同父元素是State。

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

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

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

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

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

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

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

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}

通过两个无状态widget分离 展示(CounterDisplay) 和 计算(CounterIncrementor) 的逻辑. 尽管最终效果与前一个示例相同,但责任分离允许将复杂性逻辑封装在各个widget中,同时保持父项的简单性。

Bringing it all together

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({Product product, this.inCart, this.onCartChanged})
      : product = product,
        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 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)),
    );
  }
}

ShoppingListItem widget后跟着一些常见的无状态stateless widget. 通过构造函数, 将传入的值保存到final成员变量中. 这些变量在build函数中用到, 如inCart布尔值, 控制两种显示样式的切换.

当用户点击 list item, widget不会直接修改inCart的值. widget调用从父widget传过来的onCartChanged函数. 这种模式让你可以将状态保存在更高层级, 允许状态保持更长的时间. 极端情况下, widget的状态传到runApp(), 在app的生命周期中都会保存.

但父widget收到onCartChanged回调之后, 父widget会更新它的内部状态, 触发rebuild, 并创建一个新的ShoppingListItem, 传入一个新的inCart值.

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: 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.
  
  // widget第一次出现在给定位置时, 框架会调用createState.
  // 如果父widget重新build, 并且使用了同一个类型的widget(key值一样)
  // 框架不会创建一个新的对象, 而是会重用同一个State对象

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

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, we 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.
		
		// 当用户改变了cart中的内容, 需要改变_shoppingCart
		// setState中会触发rebuild. framework会触发build方法更新UI
		
      if (inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

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

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

ShoppingList类继承自StatefulWidget, 意味着该widget中保存着可变的状态state. 当ShoppingList widget 第一次插入widget树中, 框架会调用createState函数来创建一个新的_ShoppingListState实例来关联树的位置.(通常命名State子类时带一个下划线,表示其是私有的). 当这个widget的父widget重新build, 父widget会创建一个新的ShoppingList实例, 但是框架会重用存在树上的_ShoppingListState实例, 而不是调用createState重新创建.

_ShoppingListState可以使用它的widget属性来访问当前的ShoppingList. 如果父级widget重建一个新的ShoppingList. _ShoppingListState的widget值也会重建. 如果希望监听widget属性的改变, 可以重写didUpdateWidget函数, 该函数传了一个oldWidget值让你与当前widget进行比较.

处理onCartChanged回调时, _ShoppingListState的内部状态随着_shoppingCart中的产品增加/减少改变. 将这些改变写在setState函数中来通知框架. 调用setState标记这些widget为脏widget, 并且计划在下次应用程序需要更新屏幕时重建. 当修改内部状态时, 忘记调用setState, 框架不知道你的widget已经脏了, 可能就不会调用widget的build函数, 意味着用户页面不会反应更新后的状态.

通过以这种方式管理状态,不需要分别写创建和更新子widget的代码。相反,只需实现可以处理这两种情况的build函数即可。

响应widget生命周期事件 Responding to widget lifecycle events

当StatefulWidget调用createState后, 框架将新的state对象插入树种, 然后调用state对象的initState. 如果想处理只需执行一次的事情, 可以在State的子类中重写initState方法. 例如, 配置动画或者订阅平台服务. initState实现中需要调用super.initState.

当一个状态对象不再需要时,框架调用状态对象的dispose。 您可以覆盖该dispose方法来执行清理工作。例如,您可以覆盖dispose取消定时器或取消订阅platform services。 dispose典型的实现是直接调用super.dispose。

Keys

可以使用key来控制框架将在widget重建时与哪些其他widget匹配。默认情况下,框架根据它们的runtimeType和它们的显示顺序来匹配, 有了key,框架要求两个widget具有相同的key和runtimeType。

Key在构建相同类型widget的多个实例时很有用。例如,ShoppingList构建足够的ShoppingListItem实例以填充其可见区域:

  • 如果没有key,当前构建中的第一个条目将始终与前一个构建中的第一个条目同步,即使在语义上,列表中的第一个条目如果滚动出屏幕,那么它将不会再在窗口中可见。

  • 通过给列表中的每个条目分配为“语义” key,无限列表可以更高效,因为框架将同步条目与匹配的语义key并因此具有相似(或相同)的可视外观。 此外,语义上同步条目意味着在有状态子widget中,保留的状态将附加到相同的语义条目上,而不是附加到相同数字位置上的条目。

Global Keys

您可以使用全局key来唯一标识子widget。全局key在整个widget层次结构中必须是全局唯一的,这与局部key不同,后者只需要在同级中唯一。由于它们是全局唯一的,因此可以使用全局key来检索与widget关联的状态。

Building layouts

Layouts in Flutter
Tutorial
Box constraints

Adding interactivity

Assets and images

Navigation & routing

Animations

Introduction
Overview
Tutorial
Hero animations
Staggered animations

Advanced UI

Slivers
Gestures
Widget catalog