Flutter学习(一) 有状态的StatefulWidget

有状态的StatefulWidget

原创地址coderwhy公众号文章 - 有状态的StatefulWidget

一. StatefulWidget

在开发中,某些Widget情况下我们展示的数据并不是一层不变的:
比如Flutter默认程序中的计数器案例,点击了+号按钮后,显示的数字需要+1;
比如在开发中,我们会进行下拉刷新、上拉加载更多,这时数据也会发生变化;
而StatelessWidget通常用来展示哪些数据固定不变的,如果数据会发生改变,我们使用StatefulWidget;

1.1 认识 StatefulWidget

1.1.1. StatefulWidget介绍

如果你有阅读过默认我们创建Flutter的示例程序,那么你会发现它创建的是一个StatefulWidget。
为什么选择StatefulWidget呢?

  • 因为在示例代码中,当我们点击按钮时,界面上显示的数据会发生改变;

  • 这时,我们需要一个变量来记录当前的状态,再把这个变量显示到某个Text Widget上;

  • 并且每次变量发生改变时,我们对应的Text上显示的内容也要发生改变;

但是有一个问题,我之前说过定义到Widget中的数据都是不可变的,必须定义为final,为什么呢?

  • 这是因为Flutter在设计的时候就决定了一旦Widget中展示的数据发生变化,就重新构建整个Widget;

  • 下一个章节我会讲解Flutter的渲染原理,Flutter通过一些机制来限定定义到Widget中的成员变量必须是final的;

Flutter如何做到我们在开发中定义到Widget中的数据一定是final的呢?

我们来看一下Widget的源码:

@immutable
abstract class Widget extends DiagnosticableTree {
    // ...省略代码
}

这里有一个很关键的东西 @immutable

  • 我们似乎在Dart中没有见过这种语法,这实际上是一个 注解,这涉及到Dart的元编程,我们这里不展开讲;
  • 这里我就说明一下这个@immutable是干什么的;
  • 实际上官方有对@immutable进行说明:
  • 来源: https://api.flutter.dev/flutter/meta/immutable-constant.html URL
  • 说明: 被 @immutable 注解标明的类或者子类都必须是不可变的

1.1.2. 如何存储Widget状态?

既然Widget是不可变,那么StatefulWidget如何来存储可变的状态呢?

  • StatelessWidget无所谓,因为它里面的数据通常是直接定义完后就不修改的。
  • 但StatefulWidget需要有状态(可以理解成变量)的改变,这如何做到呢?

Flutter将StatefulWidget设计成了两个类:
也就是你创建StatefulWidget时必须创建两个类:

  • 一个类继承自 StatefulWidget ,作为Widget树的一部分;
  • 一个类继承自 State ,用于记录StatefulWidget会变化的状态,并且根据状态的变化,构建出新的Widget;

创建一个StatefulWidget,我们通常会按照如下格式来做:

  • 当Flutter在构建Widget Tree时,会获取 State的实例 ,并且它调用 build 方法 去获取StatefulWidget希望构建的Widget;
  • 那么,我们就可以将需要保存的状态保存在 MyState 中,因为它是可变的;

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

思考:为什么Flutter要这样设计呢?

这是因为在Flutter中,只要数据改变了Widget就需要重新构建(rebuild

1.2. StatefulWidget案例

1.2.1. 案例效果和分析

我们通过一个案例来练习一下StatefulWidget,还是之前的计数器案例,但是我们按照自己的方式进行一些改进。

案例效果以及布局如下:

  • 在这个案例中,有很多布局对于我们来说有些复杂,我们后面会详细学习,建议大家根据我的代码一步步写出来来熟悉Flutter开发模式;
  • 1.2.2. 创建StatefulWidget
  • Column小部件:之前我们已经用过,当有垂直方向布局时,我们就使用它;
  • Row小部件:之前也用过,当时水平方向布局时,我们就使用它;
  • RaiseButton小部件:可以创建一个按钮,并且其中有一个onPress属性是传入一个回调函数,当按钮点击时被回调;

1.2.2. 创建StatefulWidget

下面我们来看看代码实现:

  • 因为当点击按钮时,数字会发生变化,所以我们需要使用一个StatefulWidget,所以我们需要创建两个类;

  • MyStatefulWidget继承自StatefulWidget,里面需要实现createState方法;

  • _MyStatefulWidgetState 继承自State,里面实现build方法,并且可以定义一些成员变量;

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text("当前计数:$counter",style: TextStyle(fontSize: 30),),
    );
  }
}

1.2.3. 实现按钮的布局

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  color: Colors.redAccent,
                  child: Text("+1",style: TextStyle(fontSize: 18,color: Colors.white),),
                  onPressed: (){

                  },
                ),
                RaisedButton(
                  color: Colors.orangeAccent,
                  child: Text("-1",style: TextStyle(fontSize: 18,color: Colors.white),),
                  onPressed: (){

                  },
                )

              ],
            ),
            Text("当前计数:$counter",style: TextStyle(fontSize: 30),),
          ],

        ),

      ),
    );
  }

1.2.4. 按钮点击状态改变

我们现在要监听状态的改变,当状态改变时要修改counter变量:

  • 但是,直接修改变量可以改变界面吗?不可以。
  • 这是因为Flutter并不知道我们的数据发生了改变,需要来重新构建我们界面中的Widget;

如何可以让Flutter知道我们的状态发生改变了,重新构建我们的Widget呢?

  • 我们需要调用一个State中默认给我们提供的setState方法;
  • 可以在其中的回调函数中修改我们的变量;
onPressed: () {
  setState(() {
    counter++;
  });
},
Snip20200914_1.png

这样就可以实现想要的效果了

1.3. StatefulWidget生命周期

1.3.1. 生命周期的理解

什么是生命周期呢?

  • 客户端开发:iOS开发中我们需要知道UIViewController从创建到销毁的整个过程,Android开发中我们需要知道Activity从创建到销毁的整个过程。以便在不同的生命周期方法中完成不同的操作;
  • 前端开发中:Vue、React开发中组件也都有自己的生命周期,在不同的生命周期中我们可以做不同的操作;

Flutter小部件的生命周期:

  • StatelessWidget可以由父Widget直接传入值,调用build方法来构建,整个过程非常简单;
  • 而StatefulWidget需要通过State来管理其数据,并且还要监控状态的改变决定是否重新build整个Widget;
  • 所以,我们主要讨论StatefulWidget的生命周期,也就是它从创建到销毁的整个过程;

1.3.2. 生命周期的简单版

那么StatefulWidget有哪些生命周期的回调呢?它们分别在什么情况下执行呢?

  • 在下图中,灰色部分的内容是Flutter内部操作的,我们并不需要手动去设置它们;
  • 白色部分表示我们可以去监听到或者可以手动调用的方法;

我们知道StatefulWidget本身由两个类组成的:StatefulWidget和State,我们分开进行分析

Snip20200914_2.png

首先,执行StatefulWidget中相关的方法:

  1. 执行StatefulWidget的构造函数(Constructor)来创建出StatefulWidget;
  2. 执行StatefulWidget的createState方法,来创建一个维护StatefulWidget的State对象;

其次,调用createState创建State对象时,执行State类的相关方法:

1、执行State类的构造方法(Constructor)来创建State对象;
2、执行initState,我们通常会在这个方法中执行一些数据初始化的操作,或者也可能会发送网络请求;

  @protected
  @mustCallSuper
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
  }
  • 注意:这个方法是重写父类的方法,必须调用super,因为父类中会进行一些其他操作;
  • 并且如果你阅读源码,你会发现这里有一个注解(annotation):@mustCallSuper

3、执行didChangeDependencies方法,这个方法在两种情况下会调用

  • 情况一:调用initState会调用;
  • 情况二:从其他对象中依赖一些数据发生改变时,比如前面我们提到的InheritedWidget(这个后面会讲到);

4、Flutter执行build方法,来看一下我们当前的Widget需要渲染哪些Widget;

5、当前的Widget不再使用时,会调用dispose进行销毁;

6、手动调用setState方法,会根据最新的状态(数据)来重新调用build方法,构建对应的Widgets;

7、执行didUpdateWidget方法是在当父Widget触发重建(rebuild)时,系统会调用didUpdateWidget方法;

我们来通过代码进行演示:

import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("HelloWorld"),
        ),
        body: HomeBody(),
      ),
    );
  }
}


class HomeBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("HomeBody build");
    return MyCounterWidget();
  }
}


class MyCounterWidget extends StatefulWidget {

  MyCounterWidget() {
    print("执行了MyCounterWidget的构造方法");
  }

  @override
  State<StatefulWidget> createState() {
    print("执行了MyCounterWidget的createState方法");
    // 将创建的State返回
    return MyCounterState();
  }
}

class MyCounterState extends State<MyCounterWidget> {
  int counter = 0;

  MyCounterState() {
    print("执行MyCounterState的构造方法");
  }

  @override
  void initState() {
    super.initState();
    print("执行MyCounterState的init方法");
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print("执行MyCounterState的didChangeDependencies方法");
  }

  @override
  Widget build(BuildContext context) {
    print("执行执行MyCounterState的build方法");
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              RaisedButton(
                color: Colors.redAccent,
                child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),),
                onPressed: () {
                  setState(() {
                    counter++;
                  });
                },
              ),
              RaisedButton(
                color: Colors.orangeAccent,
                child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),),
                onPressed: () {
                  setState(() {
                    counter--;
                  });
                },
              )
            ],
          ),
          Text("当前计数:$counter", style: TextStyle(fontSize: 30),)
        ],
      ),
    );
  }

  @override
  void didUpdateWidget(MyCounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("执行MyCounterState的didUpdateWidget方法");
  }

  @override
  void dispose() {
    super.dispose();
    print("执行MyCounterState的dispose方法");
  }
}

打印结果如下:

I/flutter ( 8065): HomeBody build
I/flutter ( 8065): 执行了MyCounterWidget的构造方法
I/flutter ( 8065): 执行了MyCounterWidget的createState方法
I/flutter ( 8065): 执行MyCounterState的构造方法
I/flutter ( 8065): 执行MyCounterState的init方法
I/flutter ( 8065): 执行MyCounterState的didChangeDependencies方法
I/flutter ( 8065): 执行执行MyCounterState的build方法
Reloaded 1 of 513 libraries in 3,586ms.

当我们改变状态,手动执行setState方法后会打印如下结果:

I/flutter ( 8065): 执行执行MyCounterState的build方法

1.3.3. 生命周期的复杂版(选读)

我们来学习几个前面生命周期图中提到的属性,但是没有详细讲解的
1、mounted 是State内部设置的一个属性,事实上我们不了解它也可以,但是如果你想深入了解它,会对State的机制理解更加清晰;
很多资料没有提到这个属性,但是我这里把它列出来,是内部设置的,不需要我们手动进行修改;

2、dirty state 的含义是 脏的State

  • 它实际是通过一个Element的东西(我们还没有讲到Flutter绘制原理)的属性来标记的;
  • 将它标记为dirty会等待下一次的重绘检查,强制调用build方法来构建我们的Widget;

3、clean state 的含义是干净的State

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

推荐阅读更多精彩内容