Flutter底层学习:State

jkouu 164 0

1.StatelessWidget和StatefulWidget

在介绍State之前,我们要先说一下StatelessWidget和StatefulWidget。

如果我们以前有过Android开发经验,在编写Flutter的过程中,我们很容易有这么一种想法:Flutter搭建UI和Android中写xml布局非常相似。实际上确实是这样的,无论是Flutter还是Android,它们描述UI都是通过树的方式实现的,所以无论是Flutter的Widget树还是Android的xml树,实际上都是通过控件之间的层次关系来描述UI的布局的。但是,Flutter和Android又不完全一样,比如点击事件我们也要通过控件封装GestureDetector后拥有的onPress等属性来注册,这又给我们一种在使用DataBinding的感觉。

当然,Flutter并没有实现数据绑定,Widget树也并不真的和xml树一样。但是我们将Flutter的UI布局理解成Android的xml布局,总归是不会有太多偏离的。如果从这一点出发,我们很容易可以发现,Flutter的两大类控件StatelessWidget和StatefulWidget起始都对应着Android中的View。

那么,为什么Flutter要把View拆分成StatelessWidget和StatefulWidget呢?这其实是因为Flutter的底层实现与Android不同。

在Android中,View一旦被创建,除非强制使其销毁,否则View会一直和它的页面一起存在。一旦View的属性需要发生变化(比如加载新的数据、改变View本身的位置、大小、颜色、透明度等属性),我们只能通过相应的set函数来通知Android进行UI的重绘。这种方式被称作命令式编程。

在Flutter中,Widget是可以在页面不销毁的情况下销毁并重建的。因此,如果我们想改变Widget,也不需要通过set函数来通知系统刷新UI了,我们只要告诉系统销毁旧的控件,用最新的数据和属性创建一个新的控件来代替旧控件就好了。这种方式叫做声明式编程,之所以叫声明式,是因为我们要想显示我们想要的控件,就必须全方位地告诉系统我们想要的控件的属性都是什么,就好比我们自定义View的时候要事先声明好所有继承自上一层View但是又需要自定义属性一样。

在声明式编程下,Flutter的Widget都是没有记忆的——它们只通过构造函数获得我们声明的属性,然后通知Flutter根据这些属性来绘制自己,我们不能通过set函数来动态地修改这些属性。对于静态页面来说,Widget已经可以满足要求了。但是对于动态页面来说,Widget还远远不够——它没有记忆,每一次被创建时都是初始的样子。

为了解决这个问题,Flutter才将Widget进一步细分为StatefulWidget。对于静态场景,比如单纯的标题显示,没有记忆的StatelessWidget足够了;对于动态场景,比如开关按钮、判断一个控件是否被点击过,就需要使用StatefulWidget了。

首先需要知道的是,StatelessWidget和StatefulWidget一样,都是静态的。因为它们都是声明式编程下的控件,而且我们都没有办法通过set函数来改变属性。那么怎么用静态控件实现动态效果呢?我们只要让Widget的构造函数的参数依赖于一个第三方类的属性,我们就可以通过set第三方类的属性来动态地改变Widget了。这个第三方类就是State。

StatefulWidget和StatelessWidget的唯一区别,在于它创建的时候会与唯一一个State类的实例关联,每一次调用State类的setState函数,都可以令StatefuleWidget根据State的相关属性来重新创建。

如果你还是不理解,可以把State看作是一个轻量的ViewModel,它可以成为一个或者一组StatefulWidget的数据源,承担着一个或一组StatefulWidget展示所需要的所有数据提供和逻辑处理。(当然,这么讲只是便于理解。如果我们写过demo,就会发现StatefulWidget实际上返回的是State类。从这一点出发,State类更像是为了被当作Widget参与UI展示,特地披了一层StatefulWidget的外衣)

当然,方法都有好与坏。虽然Flutter通过重新创建Widget的方式也可以实现UI刷新,但是代价也是很大的——一个StatefulWidget被重新创造,会将其拥有的所有子Widget也一同重新创造,这样的时间开销会很大。所以,即使我们需要使用StatefulWidget,考虑它在Widget树上的位置,位置越靠近叶子节点,每次重新创建的开销就越小。

2.State的生命周期

在前面我们提到,State像一个小型的ViewModel,那么它的生命周期就是我们要关心的重点了。

Flutter底层学习:State

上面是State的生命周期示意图。重要的几个生命周期回调就是图中英文表示的函数。

首先是initState函数。从图中可以看到,它的地位大致和Android中Activity的onCreate相同,在整个生命周期中只被调用一次,一般数据初始化的操作都会放在这里。

然后是didChangeDependencies函数。我们看到,这个函数实际上是build函数的一个前置工作,其实setState函数、didUpdateWidget函数和deactivate函数调用它都是为了间接调用build函数。当然,调用这个函数的情况并不止上述三种,还有一种常见的情况是,当系统语言 Locale 或应用主题改变时,系统会通知 State 执行 didChangeDependencies 回调方法,这也并不难理解。

setState函数是我们经常打交道的函数,当State类的数据发生变化时,我们需要通过调用它来间接调用build函数,从而刷新UI。

didUpdateWidget函数顾名思义,当Widget 的配置发生变化时,或热重载时,系统会回调该方法。这也不难理解。

build函数是整个生命周期中最核心的一个函数,但其实它也是最简单的一个函数。说它简单,并不是因为它实现简单,而是因为它完成的功能简单。从我们的视角来看,它就是根据父 Widget 传递过来的初始化配置数据及 State 的当前状态,创建一个 Widget 然后返回。在build函数执行完后,新的StatefulWidget就被创建完并开始渲染了。渲染的部分我们另外再用一篇文章讲。

deactivate方法在State对象从树中被移除时调用。Flutter的文档里讲了两种常用情况,一种是State对象从树中移除——通常是它的可见性发生变化的时候;另外一种是Flutter需要改变State在树中的位置,这是需要先将State从树中移除,然后再将其插入新的位置。deactivate方法类似于Activity中的onStop方法,在这个阶段,控件还有重新返回前台的机会,因此大多用来做一些轻量级的资源释放工作。

dispose函数类似于Activity的onDestory函数,在这个状态下,控件马上就要被销毁了,所以要做最后的资源释放工作。

3.State的管理

关于State的管理,其实有很多好用的第三方库。但是这篇文章并不是对第三方库的一个介绍,所以这里就不多说了。在这部分,我们只探究管理State的三种思路。我想只要理解了这些思路,我们也能更快地去理解第三方库的实现。

3.1.自己控制

第一种方式是控件自己控制自己的状态,这也是我们最常用的一种方式。下面是一个例子:

class BoxPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TapBox(),
      ),
    );
  }

}

class TapBox extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => TapBoxState();

}

class TapBoxState extends State<TapBox>{
  bool _isActivate = false;
  String _text;

  void _updateActivate(){
    setState(() {
      _isActivate = !_isActivate;
    });
  }

  @override
  Widget build(BuildContext context) {
    _text = _isActivate ? 'activate' : 'inActivate';
    return GestureDetector(
      onTap: _updateActivate,
      child: Container(
        width: 300,
        height: 300,
        color: Colors.blue,
        child: Center(
          child: Text(_text, style: TextStyle(color: Colors.white),),
        ),
      ),
    );
  }

}

上面的代码实现的效果是,在屏幕中央绘制一个300x300的蓝色矩形,矩形的正中央是一个用白色显示的文字。文字最初是显示inActivate‘,当我们点击蓝色区域后,它就会变成'activate',如此反复。

我们看到,作为页面的BoxPage是一个StatelessWidget,它只负责展示自己的child,并不关心child的状态。动态变化的TapBox是一个封装好的StatefulWidget,因为被点击的是它,动态改变的也是它,在自己控制的管理模式先,它通过TapBoxState来维护自己的状态。

3.2.父控件控制

第二种方式是通过父控件来管理状态,利用StatefulWidget一旦build便会重新创建所有子Widget的特性,通过构造函数传参来刷新UI。

class BoxPage extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => BoxPageState();

}

class BoxPageState extends State<BoxPage>{
  bool _activate = false;
  String _text;

  void _updateActivate(){
    setState(() {
      _activate = !_activate;
    });
  }

  @override
  Widget build(BuildContext context) {
    _text = _activate ? 'activate' : 'inActivate';

    return Scaffold(
      body: Center(
        child: TapBox(_text, (){_updateActivate();}),
      ),
    );
  }

}

class TapBox extends StatelessWidget{
  String _showText;
  Function _tapCallBack;


  TapBox(this._showText, this._tapCallBack);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _tapCallBack,
      child: Container(
        width: 300,
        height: 300,
        color: Colors.blue,
        child: Center(
          child: Text(_showText, style: TextStyle(color: Colors.white),),
        ),
      ),
    );
  }

}

上面的代码和前面的代码实现的效果是相同的。我们看到,在这种控制模式下,动态变化的TapBox成了一个StatelessWidget,它的状态由父控件BoxPage管理,这样BoxPage就成为了一个StatefulWidget。每次点击后,TapBox通过构造函数给定的BoxPageState的回调来改变State中管理的自己的状态,之后再通过通知BoxPageState重新build,从而间接让自己以最新的参数被重新绘制,最终实现更新UI的效果。

3.3.混合管理

这种管理模式融合了上面两种管理模式,动态控件自己管理一部分状态,父控件也管理自己的一部分状态。显然,这种方式是最灵活的,但是开销也是最大的。

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享