赵走x博客
首页
书籍
软件
工具
古诗词
搜索
登录
Flutter入门:3、数据传递/状态管理
Flutter入门:2、界面开发基础
mac修改默认浏览器
Flutter入门:1、基础知识
Flutter入门:2、界面开发基础
资源编号:75702
Flutter入门
精品推荐
热度:33
这边文章,我们对StateFulWidget有了升入的认识。 * 认识了通用的控件 * 了解了StatefulWidget的生命周期 * 对BuildContext 了解。 * 对Key的场景进行了了解。得到了使用GlobalKey来跨子组件传递状态的方式。
>上一篇文章:[Flutter入门:1、基础知识](http://www.handsomemark.com/articles/2019/07/30/1564455089039.html) >下一篇文章: 上一节我们熟悉了初始化后的flutter的界面。这一节,我们就来重点了解一下这部分的内容。 ### StatelessWidgets and StatefulWidgets * Flutter中的Widget都必须从Flutter库中继承。 你将使用的两个几乎总是`StatelessWidget`和`StatefulWidget`。顾名思义,我们只要如果是不需要根据状态变化的组件,我们可以直接继承`StatelessWidget`.如果和状态有关系的组件就必须继承`StatefulWidget`。 * Flutter中的`Widget`都是不可变的状态。 但是实际上,总要根据对应的状态,视图发生变化,所以就有了`state`。用它来保持我们的状态。 这样,一个Stateful Widget,实际上是两个类:状态对象`state`和`Widget`组成的。 如下代码 ``` class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => new _MyHomePageState(); } class _MyHomePageState extends State
{ int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children:
[ new Text( 'You have pushed the button this many times:', ), new Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: new FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ``` * `setState` & `build` `_MyHomePageState`继承于`State`.一方面需要管理自己的状态`_counter`,一方面需要`build`来构造组件。 改变状态后,需要通过`setState`来重新构建`widget`,就是会重新调用`build`方法,来得到状态同步。 ### 最常见的Widget 接着先看看一些常用的组件,这些是随时可用的小部件,开箱即用,你会非常满意: * `Text` - 用于简单地在屏幕上显示文本的小部件。 * `Image` - 用于显示图像。 * `Icon` - 用于显示Flutter的内置Material和Cupertino图标。 * `Container` - 在Flutter中,相当于`div`。允许在其中进行添加填充,对齐,背景,力大小以及其他东西的加载。空的时候也会占用0px的空间,这很方便。 * TextInput - 处理用户反馈。 * `Row`, `Column`- 这些小部件显示水平或垂直方向的子项列表。 * `Stack` - 堆栈显示一个孩子的列表。这个功能很像CSS中的'position'属性。 * `Scaffold` - 为应用提供基本的布局结构。它可以轻松实现底部导航,appBars,后退按钮等。 更多的可以看[目录](https://link.juejin.im?target=https%3A%2F%2Fflutter.io%2Fwidgets%2F)。 **注意:**如果您熟悉基于组件的框架(如React或Vue),则可能不需要阅读此内容。`Widget`就是组件。 #### 封装组件 这样的话,实际开发中,也是通过不断对组件的封装,来提高工作效率。 比如简单的封装一个原型的图片组件(实际上,应该这个width和height都可以封装进去的。) ``` class CircleImage extends StatelessWidget { final String renderUrl; CircleImage(this.renderUrl); @override Widget build(BuildContext context) { return Container( width: 100.0, height: 100.0, decoration: BoxDecoration( shape: BoxShape.circle, image: new DecorationImage( fit: BoxFit.cover, image: NetworkImage(renderUrl ?? ''), ), ), ); } } //直接使用 new CircleImage('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533638174553&di=6913961a358faf638b6233e5d3dcc2b2&imgtype=0&src=http%3A%2F%2Fimage.9game.cn%2F2015%2F3%2F5%2F10301938.png') ``` 运行效果(中间皮卡丘) ![image.png](https://user-gold-cdn.xitu.io/2018/8/7/1651477cba82c35a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) ### Stateful Widget 的生命周期 现在让我们深入一点, 先来思考一下 **- 为什么Stateful Widget会将`State`和`Widget`分开呢?** * 答案就只有一个:性能。 * `State`管理着状态,它是常驻的。然而,`Widget`是不可变的,当配置发生变化,它会立马发生重建。所以这样的重建的成本是极低的。 因为`State`在每次重建时都没有抛弃,所以可以维护它并且不必每次重建某些东西时都要进行昂贵的计算以获得状态属性。 * 此外,这是允许Flutter动画存在的原因。因为`State`没有丢弃,它可以不断重建它的Widget以响应数据变化。 #### 1. createState() 当创建一个`StatefulWidget`时。立即调用。通常都是如下,这样简单的操作。 ``` class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => new _MyHomePageState(); } ``` #### 2. mounted is true 当这个`Widget`调用`createState` 后, 会将`buildContext` 传入。 `BuildContext` 内有自己在`widget tree`上相关的信息。 所有的`widgets` 都有 `bool this.mounted` 这个属性. 当`BuildContext`传入时,它将会被标记成 true。如果这个属性不是true的话,调用`setState`会报错。 注意:你可以在调用`setState`前,检查一下这个变量。 ``` if (mounted) {...` to make sure the State exists before calling `setState() ``` #### 3 . initState() 这个方法只会调用一次,在这个`Widget`被创建之后。它必须调用`super.initState()`. 在这里可以做: 1. 初始化根据对应`BuildContext`的状态 2. 初始化根据在树上的父节点的属性确定的值 3. 注册`Streams` `ChangeNotifiers`或者其他会改变的数据的监听。 ``` @override initState() { super.initState(); // Add listeners to this class cartItemStream.listen((data) { _updateWidget(data); }); } ``` #### 4. didChangeDependencies() 它是在`initState` 方法后,就会调用。 当`Widget`依赖的一些数据(比如说是`InheritedWidget`,后面会介绍)更新时,它会立即被调用。 同时`build`方法,会自动调用。 需要注意的是,你需要通过调用`BuildContext.inheritFromWidgetOfExactType`,手动去注册`InheritedWidget`的监听后,这个方法才会起作用。 **文档还建议,当InheritedWidget更新时,如果需要进行网络调用(或任何其他昂贵的操作),它可能会很有用。** #### 5.build() 这个方法会经常被调用。 #### 6. didUpdateWidget(Widget oldWidget) 如果父组件发生变化,而且必须去重建widget时,而且被相同的`runtimeType`重建时,这个方法会被调用。 因为Flutter是复用`state`的。所以,你可能需要重新初始化状态。 如果你的`Widget`是需要根据监听的数据,发生变化的,那么你就需要从旧的对象中反注册,然后注册新的对象。 注意:如果您希望重建与此状态关联的Widget,则此方法基本上是'initState'的替代! 这个方法,会自动调用`build`,所以不需要去调用`setState` ``` @override void didUpdateWidget(Widget oldWidget) { if (oldWidget.importantProperty != widget.importantProperty) { _init(); } } ``` #### 7. setState() 这个方法会被`framework`和开发者不断调用。用来通知组件刷新。 这个方法的不能有异步的回调。其他,就可以随便使用。 ``` void updateProfile(String name) { setState(() => this.name = name); } ``` #### 8. deactivate() (这个状态暂时不是很理解) `State`从树中删除时会调用`Deactivate`,*但可能会*在当前帧更改完成之前*重新插入*。此方法的存在主要是因为`State`对象可以从树中的一个点移动到另一个点。 这很少使用。 #### 9. dispose() State删除对象时调用Dispose ,这是永久性的。 **在此方法取消订阅并取消所有动画,流等** #### 10. mounted is false `state`对象被移除了,如果调用`setState`,会抛出的错误。 ### 一些疑问 #### BuildContext ##### - 1. 每个`widget`都有自己的`context`。这个context是父组件通过build方法给他返回的。 首先,先看下面代码。我们将在四个地方打印context的hashCode,来看看有什么不同 ``` //... _MyHomePageState() { //1. constructor print('constructor context hashcode = ${context.hashCode}'); } void _incrementCounter() { //2. member method print('_incrementCounter context hashcode = ${context.hashCode}'); setState(() { _counter++; }); } @override void initState() { super.initState(); //3. initState print('initState context hashcode = ${context.hashCode}'); } @override Widget build(BuildContext context) { return new Scaffold( //... floatingActionButton: new FloatingActionButton( onPressed: () { //4.floattingbutton print( 'FloatingActionButton onPressed context hashcode = ${context.hashCode}'); _incrementCounter(); }, tooltip: 'Increment', child: new Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ``` ![image.png](https://user-gold-cdn.xitu.io/2018/8/7/1651477cbabbdd97?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 很明显可以看到,我们在`initState`方法时,已经分配拿到了父组件的BuildContext.接下来的直接使用context,也都是同一个。 我们知道可以通过Scaffold的context来弹出一个`SnackBar`。这里想通过点击弹出这个。 修改代码如下: ``` //... floatingActionButton: new FloatingActionButton( onPressed: () { print( 'FloatingActionButton onPressed context hashcode = ${context.hashCode}'); Scaffold.of(context).showSnackBar(SnackBar( content: Text('I am context from Scaffold'), )); }, tooltip: 'Increment', child: new Icon(Icons.add), ), ``` 运行,但是运行报错信息如下: ![image.png](https://user-gold-cdn.xitu.io/2018/8/7/1651477cbad9e8cd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 很明显。通过上面的测试,我们知道这里的context,确实不是Scaffold。那我们要如何在这里拿到Scaffold的context呢? ##### 2. 通过builder方法 修改代码如下,通过Builder方法,得到这个`context`. ``` //... floatingActionButton: new Builder( builder: (context) { return new FloatingActionButton( onPressed: () { print( 'FloatingActionButton onPressed context hashcode = ${context.hashCode}'); Scaffold.of(context).showSnackBar(SnackBar( content: Text('I am context from Scaffold'), )); _incrementCounter(); }, tooltip: 'Increment', child: new Icon(Icons.add), ); }, ) ``` 运行结果 ![image.png](https://user-gold-cdn.xitu.io/2018/8/7/1651477cbaf12a7c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 我们可以看到,我们确实拿到了`Scaffold`分配的`Context`,而且弹出了`SnackBar`. 后续过程中,一定要注意这个Context的使用。 *注意:这里其实还有另外一个方法,来得到这个`BuildContext`。就是将FloatingActionButton分离出来,写成另外一个组件,就能通过`build`方法拿到了。* 方法如下: * 添加类 ``` class ScaffoldButton extends StatelessWidget { ScaffoldButton({this.onPressedButton}); final VoidCallback onPressedButton; @override Widget build(BuildContext context) { return FloatingActionButton( onPressed: () { print( 'FloatingActionButton onPressed context hashcode = ${context.hashCode}'); Scaffold.of(context).showSnackBar( SnackBar(content: Text('I am context from Scaffold'))); onPressedButton(); }, tooltip: 'Increment', child: new Icon(Icons.add), ); } } ``` 再将`floatingActionButton`修改成这个类 ``` //... floatingActionButton: ScaffoldButton( onPressedButton: () { _incrementCounter(); }, )); ``` #### 不知所云的构造参数 Key 随意点开一个`Widget`,就会发现,可以传递一个参数`Key`.那这个Key到底是干啥子,有什么用呢? ![image.png](https://user-gold-cdn.xitu.io/2018/8/7/1651477cbb988599?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) > Flutter是受React启发的,所以Virtual Dom的diff算法也参考过来了(应该是略有修改),在diff的过程中如果节点有Key来比较的话,能够最大程度重用已有的节点(特别在列表的场景),除了这一点这个Key也用在很多其他的地方这个以后会总结一下。总之,这里我们可以知道key能够提高性能,所以每个Widget都会构建方法都会有一个key的参数可选,贯穿着整个框架。 通常情况下,我们不需要去传递这个`Key`。因为`framework`会在内部自处理它,来区分不同的`widgets` 下面有几种情况,我们可以使用它 ##### - 使用`ObjectKey`和`ValueKey`来对组件进行区分。 可以看[PageStorageKey](https://link.juejin.im?target=https%3A%2F%2Fdocs.flutter.io%2Fflutter%2Fwidgets%2FPageStorageKey-class.html), 和另外一个例子,这个例子是deletion: [flutter.io/cookbook/ge…](https://link.juejin.im?target=https%3A%2F%2Fflutter.io%2Fcookbook%2Fgestures%2Fdismissible%2F). 简单的来说,当我们使用`Row`或者`Column`时,想要执行一个`remove`的动画 ``` new AnimatedList( children: [ new Card(child: new Text("foo")), new Card(child: new Text("bar")), new Card(child: new Text("42")), ] ) ``` 当我们移除"bar"后 ``` new AnimatedList( children: [ new Card(child: new Text("foo")), new Card(child: new Text("42")), ] ) ``` 因为我们没有定义Key,所以可能flutter并不知道,我们那个item发生了改变,所以可能发生在位置1上的动画,可能发生在其他位置。 正确的修改如下: ``` new AnimatedList( children: [ new Card(key: new ObjectKey("foo"), child: new Text("foo")), new Card(key: new ObjectKey("bar"), child: new Text("bar")), new Card(key: new ObjectKey("42"), child: new Text("42")), ] ) ``` 这样当我们移除"bar"的时候,flutter就能准确的区别到正确的位置上。 `Key`虽然不是`Index`,但是对于每一个元素来说,是独一无二的。 ##### - 使用`GlobalKey` 1. 使用`GlobalKey`的场景是,从父控件和跨子`Widget`来传递状态时。 需要注意的是:不要滥用GlobalKey,如果有更好的方式的,请使用其他方式来传递状态。 这里有一个例子是 通过给Scaffold添加GolbalKey。然后通widget.GolbalKey.state来调用showSnackBar ``` class _MyHomePageState extends State
{ final globalKey = new GlobalKey
(); void _incrementCounter() { globalKey.currentState .showSnackBar(SnackBar(content: Text('I am context from Scaffold'))); } @override Widget build(BuildContext context) { return new Scaffold( key: globalKey, //... ) } } ``` 这样就可以直接从父控件调用子`Widget`的状态。 2. 还有一个场景是,过渡动画,当两个页面都是相同的Widget时,也可以使用GlobalKey。 ### 总结 这边文章,我们对StateFulWidget有了升入的认识。 * 认识了通用的控件 * 了解了StatefulWidget的生命周期 * 对BuildContext 了解。 * 对Key的场景进行了了解。得到了使用GlobalKey来跨子组件传递状态的方式。