Flutter StatelessWidget和 StatefulWidget的区别 以及不要滥用StatefulWidget

1,743 阅读6分钟

接触过flutter的人,对StatefulWidget和StatelessWidget应该都很熟悉了. 大家都知道在flutter中几乎所有的对象都是一个Widget。与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等,而原生开发中的控件通常只是指UI元素。而StatefulWidget和StatelessWidget就是继承自widget类.

  • 一 . StatelessWidget和StatefulWidget的区别和概念
  1. StatelessWidget用来描述widget的类型为无状态的, 用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget.在整个生命周期中是不会发生改变的.
  2. StatefulWidget用来描述widget的类型为有状态的的,描述起来相对复杂 我们先来看一下StatefulWidget的定义
class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

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

  @protected
  State createState();
}

在StatefulWidget类中添加了一个新的接口createState(), StatefulElement()方法执行过程中可能会多次调用createState()来创建状态(State)对象.createState()用于创建和Statefulwidget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例.说到State类给大家说明一下,实现一个StatefulWidget类必须同时实现State类, 一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态.StatefulWidget类本身是不变的,但是State类在Widget生命周期中始终存在,如果想要页面发生改变,实际上就是调用State类中的setState()方法,在调用之后就会重新执行build()方法,达到刷新UI的目的.大家看一下State的生命周期,能更好的理解StatefulWidget是怎么实现刷新widget的

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

这就是一个State生命周期,其他的方法就不在这里说明了,大家有兴趣或者还是不太懂的话可以专门搜一下关于State的文章学习一下,能帮助你更好的理解这部分内容.

在这里多插一个知识点,大家可能注意到每个build方法里都有一个context参数, 它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象,ontext是当前widget在widget树中位置中执行”相关操作“的一个句柄它提供了从当前widget开始向上遍历widget,树以及按照widget类型查找父级widget的方法.

class ContextRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget树中向上查找最近的父级`Scaffold` widget
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

简单的概括就是说在子类中可以通过context参数获取到父级中的一些信息.大家大致有一个概念就可以,这里就不详细介绍了, 以后可能会专门写一篇来详细说明Context的一些用法,以及BuildContext的更多内容介绍.

总结: 因为我想尽可能的和大家在专业的角度去阐述他们的概念和区别,可能有些刚接触flutter的朋友对我说的东西没有太理解,或者是没耐心看我上面的长篇大论.那么大家可以只看这一段我的概括.这里我比较笼统的概括一下他们的区别以及使用场景.StatelessWidget就是描述一个无状态的widget,初始化之后就无法改变了,可以使用在一些静态页面上,比如显示一段文字或者是一张图片等.StatefulWidget就是描述一个有状态的widget,他可以对widget进行状态的改变,比如网络请求刷新一个列表,点击一个button改变text的内容等.

  • 二 . 不要滥用StatefulWidget

    有些初学者会有这样一个问题, 既然StatefulWidget功能这么强大,那就干脆都只用StatefulWidget好了,为什么还要使用鸡肋的StatelessWidget.这就涉及到本文的第二个知识点了,就是StatefulWidget并不是万金油的存在,它看似强大,但是如果滥用的话也会对内存有很大的消耗.

    上面文章提到了想改变widget就要执行setState()方法, 调用之后会出发State.build.但是这也会间接触发每个子控件的构造方法以及build方法如果你的根布局是一个StatefulWidget,那么每在根State中调用一次setState(),都将是一次整页所有Widget的rebuild,通俗的说就接近于你重新创建一遍控件,但是这么说只是让大家方便理解,大家不要就认为他是重新创建,因为他和重新创建还是有一些差别.下面给大家举个例子.

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

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,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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(
              '1111111',
            ),
            Text(
              '222222',
            ),
            Text(
              '333333---' + _counter.toString(),
            ),
            TestCirculating()
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class TestCirculating extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    for (int i = 0; i < 200; i++) {
      print("模拟耗时操作 :" + i.toString());
    }
    return Container(
      child: Text("4444444"),
    );
  }
}

利用创建flutter工程系统自带的demo,写了一个小例子,然后为了大家直观的看到滥用又StatefulWidget的后果,模拟了一下耗时操作.下面是界面效果图,点击+号按钮, 第三行文字后面的0每点击一次都会+1

现在手机性能都很好,我模拟的一个for循环对于手机资源暂用可以说是微乎其微,只是让大家明白这个道理,真正项目有可能会遇到很大量的数据,如果整个页面都重新执行的话那对手机资源占用是很大的.为了更好的理解,大家可以把根widget想象成是一颗大树,里面的子控件想象成是树上的一条条树枝.你如果想去让其中一条树枝晃动,有两种方法,一种是晃动一颗大树,另一种是单独晃动你想晃动的树枝.我上面介绍的方法就是第一种晃动大树的方法,这种方法显然是不可取的,会对你体能造成很大的消耗.所以我们要做的就是单独晃动一条树枝,可以把控件单独抽出来,父级继承还是继承自StatelessWidget, 需要作出变化的控件单独继承StatefulWidget类,然后单独对一个控件进行刷新就可以了.