Flutter学习笔记(一)Widget介绍

在昨天已经完成了在macOS上安装配置Flutter开发环境并真机调试成功第一个iOS App based on Flutter,也在Windows上搭建了Flutter环境。对这个开发方式感觉还是有点意思的,今天继续学一下Flutter的知识。
一开始根据官方教程建App时,就指出了在Flutter中一切都以Widget的形式存在的。Flutter的Widget是受React影响以现代框架进行创建的,它们描述自己在当前配置与状态下的样式。当状态改变时它就改变描述,框架根据与之前描述的差异以最小的变动展现状态变化带来的样式变化。

Hello World

最小的一个Flutter App如下,仅单纯地调用一个runApp()(入口是void main()):

import 'package:flutter/material.dart';

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

runApp()中,使用给出的widget作为整个widget tree的根。上面示例中widget tree包含两个widget,一个是Center一个是它的child成员Text。框架会强制使用widget tree的根覆盖屏幕,这样即意味着“Hello, world!”会在整个屏幕的正中央。
当写一个App时,通常建议写一个StatelessWidgetStatefulWidget的子类。Widget的主要任务是实现一个build函数来就更低级别的其他widget描述自己。框架逐个构建widget直到遇到由用来计算和描述widget几何形状的RenderObject构建的widget。

基本widget(小部件)

Flutter提供了强大的基础widget组,下面是常用的一些:

  • Text:Text小部件无需多言,可以在你的应用中创建一串指定样式的文字。
  • Row,Column:这些伸缩小部件让你可以在应用中横向、纵向创建灵活布局。这一点的设计可参考WEB中的flexbox布局。
  • Stack:相较于线性排列,Stack小部件让你可以将小部件们在绘制时相互堆叠。之后可以使用Positioned小部件作用于Stack小部件的子部件们,指定他们在Stack中相对于上下左右边缘的位置。这一点的设计可参考WEB中的绝对位置。
  • Container:Container小部件可创建矩形区域,它可以被BoxDecoration修饰,如背景、边缘、阴影等。它的尺寸还可以被设定margins,padding,constraints。Containter小部件可以用矩阵转换到三维空间。
    下面是演示了这些以及其他一些小组件:
import 'package:flutter/material.dart';

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

  // Widget子类的成员永远被标记为final.

  final Widget title;

  
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // 单位为逻辑像素
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row 为水平的线性布局.
      child: Row(
        // <Widget> 为List中元素类型
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // 禁用按钮
          ),
          // Expanded 将它的child填满整个可用空间
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // Material 是显示UI的概念上的一张纸.
    return Material(
      // Column 是垂直的线性布局.
      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', // 此标题被系统的任务管理器使用
    home: MyScaffold(),
  ));
}

需要确保在pubspec.yaml文件的flutter一节设有uses-material-design: true来让你有预设的图标集Material icons可用。

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

许多小部件需要在MaterialApp内部以继承主题,因此我们带MaterialApp运行应用。
上面的代码中,MyAppBar创建了56设备无关像素高度的Container并在水平向(左右侧)各设置8像素的padding。在Containter的内部,使用Row布局组织子成员们。位于中间的title子成员被标记为Expanded从而令其填满其他子成员未占用的空间,使用多个Expanded时可用flex参数来设置各自占用的比例。
MyScaffold小部件将其子成员以竖直方向一列组织,顶部放置MyAppBar实例,并传递给它一个Text小部件作为title。传递参数给widget是一个令你能以多种方式复用它的一个有力技术。最终MyScaffold使用Expanded以一个居中的消息填满其剩余空间。
关于Flutter布局的更多信息

使用Material组件

Flutter提供许多小部件帮你遵守Material Desgn规范。Material应用以MaterialApp小部件启动,其在你应用的根构建许多有用的小部件,包含一个Navigator。Navigator管理一叠以字符串标识的小部件,亦即“路由”,令你应用在屏幕之间平滑跳转。使用MaterialApp小部件并非强制,但是一个好的实践。

import 'package:flutter/material.dart';

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

class TutorialHome extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // Scaffold 是主要Material组件的一个布局.
    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 为屏幕的主要部分.
      body: Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add', // 为辅助功能使用
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

现在我们从MyAppBarMyScaffold切换到material.dart中的AppBarScaffold,从而上一节中Demo的样子更符合Mertial一些。比如,app bar有阴影并且标题文本自动继承正确样式。另外我们还添加了一个浮动按钮。
注意到我们又将一个小部件当做参数传递给其他小部件。Scaffold小部件通过接收许多小部件作为参数,将其放到Scaffold布局合适的位置。类似的,AppBar小部件也是由我们传递小部件设置title小部件的leadingaction。该模式遍及于框架中,在你设计自己的小部件时也应该如此考虑。
关于Material组件的更多信息

处理手势

大多数应用含有一些用户与系统间的交互。构建一个交互应用的第一步即是用户手势操作的探测。我们下面通过创建一个简单的按钮来看看它如何运作:

class MyButton extends StatelessWidget {
  
  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小部件探测用户的手势操作而非可见小部件。当用户触击Containter小部件时,GestureDetector调用onTap的回调函数(此例中将在控制台打印一条消息)。你可以使用GestureDetector来探测多种输入手势,如触击、拖拽、缩放等。
许多小部件使用GestureDetector来为其他小部件提供可选的回调。例如IconButtonRaisedButton以及FloatingActionButton有在用户点击控件时触发的onPressed回调。
关于Flutter中触控的更多信息

改变小部件以响应输入

到目前为止,我们都只使用了无状态小部件。无状态的小部件从它的父小部件处接收参数,存入它final的成员变量中。当小部件被构建(build)时,使用存储的值来衍生出它创建的小部件的新变量。
为了创建更复杂的体验——例如,对用户的输入以更有趣的方式响应——应用一般会携带一些状态。Flutter使用有状态小部件(StatefulWidget)来实现在这一点。有状态小部件是知道如何生成用来保持状态的“状态(State)”对象的小部件。以前面提到的RaisedButton为例:

class Counter extends StatefulWidget {
  // 这个类为State的配置
  // 它持有由其父提供的被State构造方法使用的值(本例中为空)
  // Widget的子类中的域都被标记为“final”

  
  _CounterState createState() => _CounterState();
}

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

  void _increment() {
    setState(() {
      // setState方法的调用告诉Flutter框架有事物发生变化
      // 从而导致其重新运行下面的build方法令显示可以反映更新后的值
      // 如果我们不调用setState()改变_counter
      // 那么build方法就不会再被调用
      // 并且没有事情显得发生过
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // 如本实例上面_increment方法做的
    // 该方法在每次setState被调用时执行
    // Flutter框架被优化为执行build方法非常快速
    // 因此你可以只重建需要更新的地方而非不得不改变小部件实例
    return Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

你可能会好奇为何State和StatefulWidget是分开的对象,在Flutter中,这两种对象有不同的生命周期。Widget是临时的对象,用以构建出应用在当前状态下的表现形态。而State则是持久的对象,用以在build()方法之间记住信息。
上面这个例子接收用户输入并直接在build方法中使用结果。在更复杂的应用中,不同层级的Widget可能负责不同事务,例如一个Widget可能负责收集指定的信息如日期、位置等,而另一个Widget利用这些信息改变整体的展现。
在Flutter中,变化通知由回调而根据Widget层级向上流动,当前状态则向下流动到用以展现的无状态小部件,重引导这流向的即为State。下面这个略微复杂一些的例子展示了这是如何发挥作用的:

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

  final int count;

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

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

  final VoidCallback onPressed;

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

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

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

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

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

注意到我们创建了两个StatelessWidget将计数器的_展示_(CounterDisplay)和_改变_(CounterIncrementor)清晰地分开。尽管最终结果与前面的方式完全一样,责任的分离允许单个Widget里囊括更大的复杂性,从而在父层级上更简洁。
更多的信息:关于StatefulWidgetsetState

将这些合在一起

下面是将上面介绍的概念合在一起的复杂的例子:一个假设的购物应用展示各种售卖的产品并设有购物车保存有意购买的产品。先定义一个展示用的类ShoppingListItem开始:

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,
    );
  }

  
  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小部件遵循无状态小部件的通用模式,以final的成员变量保存从构造函数接受的值来在build函数使用。例如inCart布尔值在两种视觉展现上切换:使用当前主题的主色以及使用灰色。
当用户点击列表项时,小部件不直接修改inCart值,而是调用从父小部件接收的onCartChanged。这样方式让你可以在更高小部件层级保存状态从而持续更长时间。极端的情况,状态保持在传递给runApp()的小部件就会持续整个应用的生命周期。
当其父接收到onCartChanged回调后会更新它内部状态,触发其重新构造并创建带有新inCart值的新ShoppingListItem。尽管其父在重新构造时创建了新实例,但由于框架会比较新构建的和之前的小部件并只应用根本的RenderObject的不同之处,所以操作开销很小。
这是个父小部件保存可变状态的示例:

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  // 框架于小部件在树种给定位置首次出现时调用createState
  // 如果其父重新构造并使用同样同一小部件类型(带同样key)
  // 框架会复用这个State对象而非创建新的State对象

  
  _ShoppingListState createState() => _ShoppingListState();
}

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

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // 当用户改变购物车中物品时
      // 我们需要在setState中改变_shoppingCart
      // 接着框架就会调用下面的build来更新应用的视觉表现

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

  
  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即可存储可变状态。当其第一次被插入树中时,框架调用createState函数来创建一个新的_ShoppingListState。(注意State子类的命名前有下划线表明其为私有化实现)。当父部件重新构造时,创建一个新的ShoppingList实例,但是框架将复用树中已有的_ShoppingListState实例而非再次调用createState
_ShoppingListState可以使用它小部件的属性,如果其父重新构造并创建新的ShoppingList_ShoppingListState使用新的小部件值重新构造。如果你希望在小部件属性改变时被通知,可以重写didUpdateWidget方法从而被传入旧的小部件以便和当前的进行比较。
当处理onCartChanged回调时,_ShoppingListState通过在_shoppingCart中添加或删除一个产品改变其内部状态。它将这些包在setState调用中以通知框架它内部状态发生了变化。setState的调用会将小部件标记为脏并计划在下次应用刷新屏幕时将它重建。如果你在修改内部状态时忘记调用setState,框架就不会知道小部件发生了变化也就不会调用build方法,这意味着用户界面不会更新以展示状态的改变。
使用这样方式管理状态,你不需要为创建和更新小部件分别编写代码,而是简单实现build方法来同时处理这两种情况。
#响应小部件生命周期事件
StatefulWidget调用createState后,框架将新状态对象插入树中并对其调用initStateState的子类可以重写该方法来做仅需进行一次的工作,例如你可以重写它来配置动画或是订阅平台服务。在实现时需先调用super.initState
当一个状态对象不再需要时,框架对其调用dispose方法。你可以重写其做一些清洁工作,如重弄它来取消计时器或是取消订阅平台服务。在实现时最后需调用super.dispose
关于State的更多信息

Keys

你可以使用键值来控制当一个小部件重建时框架如何将小部件们匹配关联。默认框架依照出现顺序及通过runtimeType将当前小部件与先前的构建匹配关联。带键值则框架要求两个小部件像有同样runtimeType一样需要有同样的key。
键值在需要创建很多同一类型小部件的小部件内很有用。如ShoppingList小部件创建许多ShoppingListItem小部件实例填充其可视区域:

  • 无键值则当前构建的第一个条目总是与前一构建的第一条目同步,即使它应当已经滑动出了屏幕可见区域。
  • 给列表中每个条目分配一个键值,无限列表将因为框架匹配项目键值而高效并有相似(相同)显示。此外同步条目意味着有状态子小部件中保持的状态会附加在同一条目而非视觉上同一位置。
    关于Key API的更多信息

全局键值(Global Keys)

你可以使用全局键值来辨认子小部件。全局键值必须在整个小部件层级中无重复,不像本地键值仅需在兄弟中无重复即可。因其全局独一无二,可以使用它来获取小部件的关联状态。
关于GlobalKey API的更多信息