Flutter- 组件框架之旅

image

以下内容基本翻译自A Tour of the Flutter Widget Framework,翻译的可能并不完全!作为自己学习的笔记,加入了自己的理解,可能有疏漏错误,欢迎指正!
PS:Widget可能会翻译为小部件、组件、控件等等,都是一个东西,不要太在意细节

引言

Flutter组件使用现代的响应式框架(react-style framework)建立,灵感来源React。核心思想,通过组件(widget)构建UI。通过给组件(Widgets)设置它们当前的配置(configuration )和状态(state)来描述它们(Widgets)的长相。当组件的状态发生改变,组件重建它的描述,为了确定过度到下一个状态所需最小改变,该描述是框架对比之前的描述等到的差异。

原文:When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.

英语太次,翻译不准确,个人理解大意:就是framework取得了前后两个状态的最小改变,没有变的属性不操作,改变了的算差值进行改变,而不是清空一个状态,再设置另一个状态。
tips:如果想通过深入代码更好了解Flutter,查看Building Layouts in FlutterAdding Interactivity to Your Flutter App.

Hello World

最小的FlutterApp仅仅通过组件调用runApp函数。

import 'package:flutter/material.dart';

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

runApp方法获取到给它的组件( Widget)并把组件作为它的组件树(widget tree)的根。
此例中,组件树持有两个组件, Center(继承Align) 和它的子组件- Text 。框架强制根组件(The root of the widget tree)铺满(cover)屏幕,这意味着Hello Worldtext最终位于屏幕中心。此例中的text的方向需要指定。
The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later.
当写app时,你通常会创建 StatelessWidgetStatefulWidget的子类作为组件,继承那个类取决与你的组件是否管理状态state。一个组件的主要工作时实现build 方法,这个方法描述了这个组件与其他组件或子组件的条约(terms)。框架framework会依次创建这些组件,直到超出组件的底部,这代表计算和描述组件的几何形状的底层渲染对象RenderObject.
The framework will build those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject, which computes and describes the geometry of the widget.

Basic Widgets

Main article: Widgets Overview - Layout Models
Flutter自带一套强大的基础组件(Basic widgets),以下是其中一些常用的:

  • Text:Text用于创建带样式的文本
  • Row, Column: 这些灵活(flex)地组件可以让你在水平(Row)和垂直(Column)方向创建灵活的布局。它的设计是基于Web的flexbox布局模型
  • Stack: Stack组件可以让你的组件在绘制顺序上层积(stack)在彼此顶部,而不是线性(水平或垂直)的。可以在子Stack上使用 Positioned组件来放置他们到这个Stack的top,right,bottom或left边界。Stack设计是基于Web的absolute positioning layout model(绝对位置布局模型)。
  • Container: Container组件帮你创建一个矩形元素。一个Container可以被一个BoxDecoration修饰,如背景(background)、边线(border)、阴影(shadow)。一个Container也可以有margin,padding和固定大小定义。另外,Container可以用矩阵(matrix)在三维控件改变。
    以下是一些组合这些组件的例子:
import 'package:flutter/material.dart';

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

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

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

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

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

确保在pubspec.yaml文件的 flutter 一节下有 uses-material-design: true 设置,它允许使用预定义的Material icons集合。

name: my_app
flutter:
  uses-material-design: true
运行效果

许多组件需要在 MaterialApp内部才正确显示,继承他们的主题数据(Theme data),因此我们运行一个MaterialApp程序。
MyAppBar组件创建一个高56dip( device-independent pixels)及内部padding 8px,从左到右的Container组件。在Container中,MyAppBar使用Row布局(layout)来管理它的子控件。中间儿子,title组件,标记为Expanded,意为它可以扩展填充任何剩余的、未被其他子控件占用的空间。你可能有多重Expanded子控件,使用flex来确定他们各自占用可用空间的比例(You can have multiple Expanded children and determine the ratio in which they consume the available space using the flex argument to Expanded)。
MyScaffold组件在垂直列方向(vertical column)管理它的子控件。在列顶,它放了一个MyAppBar实例,传递一个Text组件做app bar的title。传递组件(Passing widgets)作为另一个组建的参数是一个强大的技术,它允许你创建的常用组件多样重用。最后,居中显示信心的MyScaffold使用Expanded来填充剩余的空间。

Using Material Components

Main article: Widgets Overview - Material Components
Flutter提供了许多遵循Material Design的组件帮助你创建app。一个Material app始于MaterialApp组件,MaterialApp组件作为你app的根(root)创建许多有用的组件,包括 管理一堆使用strings区分的组件、亦被称为routesNavigatorNavigator 让你平滑在app的screens间切换。使用MaterialApp不是必须的,但是是一个很好的惯例。

import 'package:flutter/material.dart';

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

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

现在我们替换MyAppBar and MyScaffold为来自material.dartAppBarScaffold ,我们的app看起来更加Material。例如,app bar有阴影了,title文本自动继承了正确的样式。我们也添加了一个合适的浮动按钮(FloatingActionButton)。

注意,我们再次把组件作为参数传递给另一个组件。Scaffold需要许多不同的组件作为参数,他们将被放在Scaffold的适当位置。类似的,AppBar组件让我们传递组件作为 title组件的leadingactions 。这种模式遍布整个框架,你在设计自己的组件时也需要考虑。

Handling gestures 处理手势

Main article: Gestures in Flutter
大部分app包含一些用户与系统交互的表单。第一步就是要创建一个可交互的app去检测输入的手势getsures。创建一个实例按钮来看看这是如何工作的:

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

GestureDetector 组件不可见,但可以检测用户手势gestures。当用户轻击ContainerGestureDetector将回调它的onTap方法,这个例子中,在控制台console打印了一条消息。你可以使用GestureDetector来检测一系列输入手势,包括点击tap,拖动drag和缩放scale。
许多组件使用GestureDetector来用于其他组件的可选回调。例如,IconButton,RaisedButton, FloatingActionButton 有一个 onPressed 回调方法,当用户点击这些组件时触发。

Changing widgets in response to input

Main articles: StatefulWidget, State.setState
目前,我们仅使用了stateless组件。Stateless widgets从他们的父类接受参数,他们保存了 final 成员变量。当一个组件调用build时,它使用这些已存的值提取新参数(derive new arguments )来创建组件。
为了创建更加复杂的体验,例如,以更有趣的方式响应用户的输入,app通常带有某些状态。Flutter使用StatefulWidgets组件获取这些idea.StatefulWidgets是知道如何创建State对象的特殊组件,State持有state。在此例中,使用RaisedButton mentioned earlier:

class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this nothing) provided by the parent and used by the build
  // method of the State. Fields in a Widget subclass are always marked "final".

  @override
  _CounterState createState() => new _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.
      _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.
    return new Row(
      children: <Widget>[
        new RaisedButton(
          onPressed: _increment,
          child: new Text('Increment'),
        ),
        new Text('Count: $_counter'),
      ],
    );
  }
}
运行效果

你可能好奇,为什么StatefulWidgetState是分离的对象。在Flutter中,这两类对象有不同的生命周期。Widgets是临时对象,用于构建app当前状态的表达(presentation)。另一方面,State是在build()之间是持续的,允许他们记忆信息。
上例中,接收用户输入和也接受它的build方法中的结果。在更复杂的app中,不同层级的组件可能负责不同关注点。例如,一个组件可能呈现复杂的用户界面,其目的是收集特定信息,如信息或位置,而另一个组件可能使用这些信息更改总体表现。
在Flutter中,更改信息流依赖回调组件层次结构,当当前状态流向stateless组件。State就是父类重定向这些信息流。我们来看看实际中是如何工作的,这是一个稍微复杂的例子:

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

  final int count;

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

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

  final VoidCallback onPressed;

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

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

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

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

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

请注意我们如何创建2个stateless组件,清晰分离了关注点:显示displaying计数器(CounterDisplay)和改变changing计数器(CounterIncrementor)。虽然最终结果和之前一样,但费力责任允许在单个组件中加入更多复杂性,同时保持父级的简单性。

Bringing it all together

我们来思考一个更复杂的例子,把以上观点都汇聚在一起。我们来假设一个购物app,展示各种待售产品,并维护一个购物车用于购买。我们先来定义 presentation class-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: new 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 new TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return new ListTile(
      onTap: () {
        onCartChanged(product, !inCart);
      },
      leading: new CircleAvatar(
        backgroundColor: _getColor(context),
        child: new Text(product.name[0]),
      ),
      title: new Text(product.name, style: _getTextStyle(context)),
    );
  }
}

ShoppingListItem继承自一个stateless 组件的通用模式。它保存来自它的构造函数接受的值给它的 final 成员变量,这些值在它的build方法中使用。例如,inCart切换两种可视外观,一个使用当前主题的primary颜色,另一个使用灰色gray。
当用户点击条目,组件不直接改变它的inCart的值。而是调用父类的onCartChanged方法。这种模式让你保存状态state到更高的组件层级,这是状态持续更长时期的原因。这个例子中,保存在组件中的状态state通过runApp持续存在于app的生命周期中。
当父类收到onCartChanged回调,父类将更新它的内部状态state,这将引发父类重建并新建一个带有新inCart值的新的ShopingListItem实例。虽然父类重建时创建一个新的ShoppingListItem实例,但是这个操作是廉价的,因为框架对比了新的组件和旧的组件,只应用不同的RenderObject
来看一个保存可变状态的父类:

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 will re-use the State object
  // instead of creating a new State object.

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

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

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When user changes what is 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.

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

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

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

运行效果

ShoppingList类继承自 保存可变状态的 StatefulWidget。当ShoppingList组件首次插入树(tree)中,框架调用createState方法来创建一个最新的_ShoppingListState实例来连接树(tree)。(注意我们通常使用_开头来定义State的子类来表示他们私有实现详情(private implementation details)) 当组件的父类重建,父类会创建一个新的ShoppingList实例,但是框架会重用已经存在树(tree)中的_ShoppingListState实例,而不是再次调用createState
为了获取当前ShoppingList特性(properties ),_ShoppingListState会使用它自身widget特性。如果父类重建并创建一个新的ShoppingList,_ShoppingListState也会重新创建一个新的widget值。如果你希望当 widget 特性改变时得到通知,可以复写didUpdateWidget方法,它会传递oldWidget,你可以与当前widget进行对比。
当处理onCartChanged回调时,_ShoppingListState通过给_shoppingCart增加或删除一个product改变它内部状态。通过调用setState 通知框架(framework)它内部状态(state)的改变。调用setState标记widget为脏dirty并安排它在下次更新屏幕时重建。如果内部状态改变时你忘记调用setState,框架(Framework)不会知道你的widget是脏的(dirty),也就不会调用widgetbuild方法,这意味着,用户界面不会更新反应状态的改变。
通过这种方式管理state,你不必分别为创建和更新子widget写代码,只需简单的实现build方法,它会处理两种情况。

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

Main article: State
StatefulWidget调用 createState之后,框架(framework)插入一个新的state对象到树(tree)中,然后调用state对象的 initStateState的子类可以复写(override)initState来做只需执行一次的工作。例如,你可以复写(override)initState来配置动画或订阅平台服务(subscribe to platform services)。initState的实现要求以调用super.initState开始。
当一个state对象不在被需要,框架(framework)调用state对象的dispose 。你可以复写(override)dispose方法来清理工作。例如,通过复写dispose来取消计时器或解除平台服务订阅。dispose实现通常以调用super.dispose结束。

Keys

Main article: Key
你可以使用keys来控制当一个组件重建时,框架将匹配那些其他组件。默认的,框架会根据他们的 runtimeType和他们出现的顺序匹配当前和前一个组件。带有keys的,框架(framework)会要求2个组件有相同的key和相同的runtimeType
当创建很多相同类型组件的实例时Keys是非常有用的。例如,上例中ShoppingList组件,创建了足够多的ShoppingListItem实例来填充它的可视区域:

  • 没有keys,当前创建的第一个entry会一直与前一次创建的第一个entry同步,即使列表中的第一个entry滚出屏幕而且不再可见。
    通过指派列表中每一个entry一个semantic* key,无限列表可以更有效,因为框架会同步entries 匹配semantic keys和因此相同或近似的外观。此外,同步entries语义semantically意味着在stateful的子组件中的state会保持连接到相同semantic entry,而不是连接到在viewport中的相同数值位置的entry。

Global Keys 全局Keys

Main article: GlobalKey
你可以使用全局keys唯一的标识子组件。全局keys必须在整个组件层级是全局唯一的,不像本地local key只需在兄弟之间唯一。因为他们全局唯一,可以用全局key检索与组件连接状态。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容