[Flutter]flutter基础之组件基础(一)

一、从 main 函数开始

在前面的文章中,我们了解了 Dart 的基础语法部分,而 Flutter 就是 Dart 语言的移动应用框架。Flutter 应用程序使用 Dart 语言编写代码,而 Dart 语言中函数的执行入口就是 main 函数。而通过上一篇文章我们创建的默认 Flutter 工程我们知道,在 lib 文件夹下有一个名为 main.dart 的文件,在此文件中就有一个 main 函数的存在,如下:

void main() => runApp(MyApp());

也可以写为如下形式:

void main() {
  runApp(MyApp());
}

这个函数只有一行代码,执行了一个 runApp() 函数,Mac 系统,可以按住 command 键点击鼠标左键,可以进入函数的定义查看该函数,函数有一个类型为 Widget 的参数,Widget 可以理解为 "小部件" 的意思,以下统称 Widget 。在 Flutter 中,runApp() 函数就是应用的入口函数,调用此函数传入 Widget 参数才能正常执行一个应用程序,否则只是一个控制台程序,所有的 Flutter 项目都从 runApp() 函数开始执行。此函数的原型为 void runApp(Widget app) ,这个函数会将给定 Widget 作为 Widget 树的根节点。如果是布局 Widget ,将会填充整个屏幕。

通过 main 函数我们知道,运行一个 Flutter 应用程序,只需要在 runApp() 函数中传入一个 Widget 即可。那么 Widget 又表示什么?

二、Widgets

Widget 是用于在 Flutter 中构建用户界面的可重用的构建基块,是一个 Dart 类。在 Flutter 中,几乎所有东西都是 Widget ,从界面布局到图像、图标、文本等都是 Widget ,包括从界面看到的和看不到的。Flutter 应用程序中的 Widget 以树状层次结构分布,称为 "小部件树"。 Widget 类中所有的字段都是最终字段( final 修饰 ),通过构造函数进行设置。

在我们创建的默认工程的 runApp() 函数中,传入的是名为 MyAppWidget ,代码如下:

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

可以看到 MyApp 是一个继承自 StatelessWidget 的类,类中重写了父类的 build 方法,build 方法接收一个 BuildContext 类型参数,返回值为一个 Widget 类型。点击进入 StatelessWidget 类定义可以看到其为一个抽象类,前面基础部分说过,对于继承的抽象类,需要实现其中的全部抽象方法,在 StatelessWidget 抽象类中,build 方法的声明为:Widget build(BuildContext context); 是一个抽象方法,所以继承自 StatelessWidget 的类必须实现 build 方法。Widget build(BuildContext context) 方法用来实现定义的 Widget 的用户界面部分,当创建的 Widget 添加到现有的小部件树,并且此小部件的依赖项发生变化时会调用此方法。现在只要知道,对于创建的继承自 StatelessWidget 类,需要实现重写build 方法,要创建的界面部分在 build 中实现即可。

build 方法中,返回了一个 MaterialApp 类型的 Widget ,而 MaterialApp 的定义在 src/material/app.dart 中,所以在 main.dart 的第一行便是包的导入代码 import 'package:flutter/material.dart'; ,这个包是 Flutter 实现 Material Design 设计风格的基础包,里面包含了布局,文本,图片等小部件。Material Design 为谷歌推出的一套视觉设计语言,开发中一般使用此包即可。说白了,MaterialApp 也是一个类,代码中对其属性进行了设置,其中 home 属性,使用到了下面代码定义的类 MyHomePage , 继续看默认工程的下两个函数,如下:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

MyHomePage 类继承自 StatefulWidget ,其中重写了 createState() 方法,它也是一个抽象方法,为必须实现的方法,声明为:State createState(); 。在上述代码中,createState() 方法调用了 _MyHomePageState() 构造方法,该构造方法的类定义也在上述代码中,_MyHomePageState 类为一个包内可见类,继承自 State 。而 State 也是一个抽象类,其中也有一个未实现的 Widget build(BuildContext context); 方法,所以 _MyHomePageState 类需要实现 build 方法,我们知道 build 主要用来实现用户界面,这里主要用来实现点击按钮,按钮有一个回调函数为 _incrementCounter , 点击按钮时调用此方法并执行 setState 方法实现 _counter++ 。其中 setState() 方法用来实现通知框架触发更新操作。

三、StatelessWidget 与 StatefulWidget

通过上面的默认工程的例子可以看到,创建的小部件类继承自两个类: StatelessWidgetStatefulWidget 。在 Flutter 中,StatelessWidget 称为无状态 Widget,StatefulWidget 称为有状态 Widget,在开发中,自定义的小部件99%的情况都会继承自这两个中的一种。因为对于开发者来说,创建的都是小部件,所以创建 Widget 的过程首先就是继承自系统提供的 Widget 来实现自己的功能,从这两个类的名字便可以看出,他们都是 Widget 。那么什么是有状态和无状态?首先来看下这两个类的继承关系:

StatelessWidget < Widget < DiagnosticableTree < Diagnosticable < Object
StatefulWidget  < Widget < DiagnosticableTree < Diagnosticable < Object

从上述的继承链可以看出,他们有着同样的继承关系,他们的父类都是 Widget 。而上面也说过,Widget 类中的字段都是最终字段(使用 final 修饰),为何如此?先来看下 Widget 的定义如下:

@immutable
abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({ this.key });

  final Key key;

  @protected
  Element createElement();

  /// A short, textual description of this widget.
  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

可见,Widget 是一个抽象类,并且使用了 @immutable 注解做了注释,@immutable 的作用是表示此标注标注的类和其子类的类型必须为不可变的(不可变的意思就是该类的所有实例字段,无论是直接定义还是继承的,都是最终字段,则该类不可变),而且 Widget 类的构造函数也是常量构造函数(关于常量构造函数的使用规则可以查看 Dart 基础部分文章),所以继承自 Widget 的类中的实例属性也必须为不可变的。这也就是上面说的无状态,无状态指的就是 Widget 中的内容定义以后不可以改变。既然如此,我们会发现,无论是 StatelessWidget 或者 StatefulWidget 都是继承自 Widget ,那么也就意味着,他们的实例属性也都是不可变的。既然如此,又如何有有状态与无状态之分呢?再来看下 StatelessWidgetStatefulWidget 的源码如下:

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);
  
  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);
}
abstract class StatefulWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();
}

这两个类的源码都比较简单,这里我们只需要关注这两个类中的最后一个方法,即使用 @protected 注解注释的方法。@protected 也就是受保护的意思,指示被标注的方法只能在其子类中访问,其他类不可访问。在 StatelessWidget 类中,方法为 Widget build(BuildContext context); ,为继承类必须实现的方法,方法返回的是 Widget ,也就是要直接实现定义的 Widget ,即用户界面,所以继承自 StatelessWidget 类中的实例属性一旦定义便无法进行更改。而 StatefulWidget 类中的方法为 State createState(); ,它并非直接返回一个 Widget ,而是返回一个状态 State 类型。以下为 State 类的定义:

@optionalTypeArgs
abstract class State<T extends StatefulWidget> extends Diagnosticable {

  T get widget => _widget;
  T _widget;

  _StateLifecycle _debugLifecycleState = _StateLifecycle.created;

  bool _debugTypesAreRight(Widget widget) => widget is T;

  BuildContext get context => _element;
  StatefulElement _element;

  bool get mounted => _element != null;

  @protected
  @mustCallSuper
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
  }

  @mustCallSuper
  @protected
  void didUpdateWidget(covariant T oldWidget) { }

  @protected
  @mustCallSuper
  void reassemble() { }

  @protected
  void setState(VoidCallback fn) {
    assert(fn != null);
    assert(() {
      if (_debugLifecycleState == _StateLifecycle.defunct) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called after dispose(): $this'),
          ErrorDescription(
            'This error happens if you call setState() on a State object for a widget that '
            'no longer appears in the widget tree (e.g., whose parent widget no longer '
            'includes the widget in its build). This error can occur when code calls '
            'setState() from a timer or an animation callback.'
          ),
          ErrorHint(
            'The preferred solution is '
            'to cancel the timer or stop listening to the animation in the dispose() '
            'callback. Another solution is to check the "mounted" property of this '
            'object before calling setState() to ensure the object is still in the '
            'tree.'
          ),
          ErrorHint(
            'This error might indicate a memory leak if setState() is being called '
            'because another object is retaining a reference to this State object '
            'after it has been removed from the tree. To avoid memory leaks, '
            'consider breaking the reference to this object during dispose().'
          ),
        ]);
      }
      if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called in constructor: $this'),
          ErrorHint(
            'This happens when you call setState() on a State object for a widget that '
            'hasn\'t been inserted into the widget tree yet. It is not necessary to call '
            'setState() in the constructor, since the state is already assumed to be dirty '
            'when it is initially created.'
          ),
        ]);
      }
      return true;
    }());
    final dynamic result = fn() as dynamic;
    assert(() {
      if (result is Future) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() callback argument returned a Future.'),
          ErrorDescription(
            'The setState() method on $this was called with a closure or method that '
            'returned a Future. Maybe it is marked as "async".'
          ),
          ErrorHint(
            'Instead of performing asynchronous work inside a call to setState(), first '
            'execute the work (without updating the widget state), and then synchronously '
           'update the state inside a call to setState().'
          ),
        ]);
      }
      // We ignore other types of return values so that you can do things like:
      //   setState(() => x = 3);
      return true;
    }());
    _element.markNeedsBuild();
  }

  @protected
  @mustCallSuper
  void deactivate() { }

  @protected
  @mustCallSuper
  void dispose() {
    assert(_debugLifecycleState == _StateLifecycle.ready);
    assert(() {
      _debugLifecycleState = _StateLifecycle.defunct;
      return true;
    }());
  }

  @protected
  Widget build(BuildContext context);

  @protected
  @mustCallSuper
  void didChangeDependencies() { }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    assert(() {
      properties.add(EnumProperty<_StateLifecycle>('lifecycle state', _debugLifecycleState, defaultValue: _StateLifecycle.ready));
      return true;
    }());
    properties.add(ObjectFlagProperty<T>('_widget', _widget, ifNull: 'no widget'));
    properties.add(ObjectFlagProperty<StatefulElement>('_element', _element, ifNull: 'not mounted'));
  }

这个类的代码比较多,但并不复杂,可以发现 State 为一个抽象类,我们知道抽象类不能直接被实例化,所以需要开发者自定义一个类来继承 State 类,类中也只有一个抽象方法,是我们熟知的 Widget build(BuildContext context); ,所以开发者只需要定义一个类并继承 State 类,实现 build 方法,然后在 createState() 方法中实例化自定义类即可。类似如下方式:

class MyState extends State {
  @override
  Widget build(BuildContext context) {
    //省略代码
    return widget;
  }
}

然后在自定义的 Widget 中通过 createState() 方法调用即可,如下:

class MyCustomWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyState();   //构造返回
  }
}

一般情况下,如此定义即可,但是我们发现在 State 类定义中,有一个泛型的约束条件 <T extends StatefulWidget> , 在类中也有一个 widget 实例属性 ,其具体类型是由约束条件限定的,也就是传递的是哪个 Widget ,类中就会是对应的 Widget ,且这个 Widget 必须是继承自 StatefulWidget 的。当我们不设置此泛型参数时,在通过 createState() 参数调用自定义的类 MyState 后,Flutter 会自动将 MyCustomWidgetMyState 做关联,也就是说此时 MyState 类中的 widget 就是 MyCustomWidget 的实例。但是如果在MyCustomWidget 中有属性需要在 MyState 中调用时,就需要显式指明是哪个 Widget ,否则无法在对应的 State 类中调用属性。如下:

class MyCustomWidget extends StatefulWidget {
  final String name = "custom";

  @override
  State<StatefulWidget> createState() {
    return MyState();
  }
}

class MyState extends State<MyCustomWidget> {
  MyState() {
    widget.name;   //如不指明具体Widget类型,无法调用属性
  }

  @override
  Widget build(BuildContext context) {
    //省略代码
    return widget;
  }
}

说到这里,大家应该发现,虽然继承自 StatelessWidgetStatefulWidget 的类中必须使用 final 修饰实例属性,但是继承自 StatefulWidget 的类中 createState() 方法返回的 State 类却并非要定义成 final 实例属性。所以如果有需要变换的量可以在自定义的继承自 State 的类中实现。

在默认工程的例子中,实现点击数量增加是通过 _counter 属性实现的,可以发现在点击的回调方法中,实现如下:

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

setState() 用来在 Widget 状态发生变化时通知框架,以实现界面数据的更新,如果不调用此方法,界面数据不会更新。所以当 State 对象内部状态发生改变时,应在 setState() 内进行更改操作。

摘录以上例子,并非要对源码做分析,而是说明有状态 Widget 与 无状态 Widget 的区别。可见,当要实现有状态 Widget 时,需要实现两个类,一个继承自 StatefulWidget 和一个继承自 State ,他们是互相关联的。

根据官方文档的说明,使用 StatelessWidget 的情况如下:

  1. 当 Widget 第一次被插入到小部件树时;
  2. 小部件的父类更改其配置时;
  3. InheritedWidget 依赖于改动时。

总结就是:StatelessWidget 用于在特定配置和环境状态下始终以相同的方式构建 Widget ,也就是定义后数据不做改动时。StatefulWidget 在其生命周期内可以多次构建的 Widget 。

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

推荐阅读更多精彩内容

  • 首先,在Flutter中几乎所有的对象都是一个Widget。跟原生开发中的“控件”不同,Flutter中的Widg...
    沉江小鱼阅读 1,557评论 0 2
  • 概念 在前面的介绍中,我们知道在Flutter中几乎所有的对象都是一个Widget。与原生开发中“控件”不同的是,...
    小小的开发人员阅读 633评论 0 2
  • 原文在此,此处只为学习 Widget与ElementWidget主要接口Stateless WidgetState...
    lltree阅读 4,501评论 0 1
  • 前言 上一篇我们简单地了解了 Dart 语言,接着我们就开始学习 Flutter 的基础 Widget 吧。 1....
    南小夕阅读 2,556评论 0 8
  • 爱上一个诗人 文/宋雨霜 没有任何理由,爱上一个诗人。爱他清贫的身影,爱他飘逸的长发,爱他挺拔的文字。 爱上一个诗...
    土家霜妹阅读 439评论 4 3