Flutter入门三部曲(2) - 界面开发基础

image.png

上一节我们熟悉了初始化后的flutter的界面。这一节,我们就来重点了解一下这部分的内容。

StatelessWidgets and StatefulWidgets

  • Flutter中的Widget都必须从Flutter库中继承。
    你将使用的两个几乎总是StatelessWidgetStatefulWidget。顾名思义,我们只要如果是不需要根据状态变化的组件,我们可以直接继承StatelessWidget.如果和状态有关系的组件就必须继承StatefulWidget
  • Flutter中的Widget都是不可变的状态。
    但是实际上,总要根据对应的状态,视图发生变化,所以就有了state。用它来保持我们的状态。
    这样,一个Stateful Widget,实际上是两个类:状态对象stateWidget组成的。
    如下代码
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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: <Widget>[
            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,后退按钮等。

更多的可以看目录

注意:如果您熟悉基于组件的框架(如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

Stateful Widget 的生命周期

现在让我们深入一点,
先来思考一下
- 为什么Stateful Widget会将StateWidget分开呢?

  • 答案就只有一个:性能。
  • 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,会抛出的错误。

summary with image

来自网络盗图.png

一些疑问

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

很明显可以看到,我们在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

很明显。通过上面的测试,我们知道这里的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

我们可以看到,我们确实拿到了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

Flutter是受React启发的,所以Virtual Dom的diff算法也参考过来了(应该是略有修改),在diff的过程中如果节点有Key来比较的话,能够最大程度重用已有的节点(特别在列表的场景),除了这一点这个Key也用在很多其他的地方这个以后会总结一下。总之,这里我们可以知道key能够提高性能,所以每个Widget都会构建方法都会有一个key的参数可选,贯穿着整个框架。

通常情况下,我们不需要去传递这个Key。因为framework会在内部自处理它,来区分不同的widgets
下面有几种情况,我们可以使用它

- 使用ObjectKeyValueKey来对组件进行区分。

可以看PageStorageKey, 和另外一个例子,这个例子是deletion: https://flutter.io/cookbook/gestures/dismissible/.

简单的来说,当我们使用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<MyHomePage> {
  final globalKey =
      new GlobalKey<ScaffoldState>();

  void _incrementCounter() {
    globalKey.currentState
        .showSnackBar(SnackBar(content: Text('I am context from Scaffold')));
  }

 @override
  Widget build(BuildContext context) {
    return new Scaffold(
        key: globalKey,
      //...
      )
}
}

这样就可以直接从父控件调用子Widget的状态。

推荐视频 When to Use Keys - Flutter Widgets 101 Ep. 4
使用LocalKey,只能在同一层级上找相同的key,如果包裹了一层的话,就不行了。
这个时候,可以用globalkey来完成需求。
valuekey适合于当个变量,objectKey应用于Object。GlobalKey是全局的

  1. 还有一个场景是,过渡动画,当两个页面都是相同的Widget时,也可以使用GlobalKey。

总结

这边文章,我们对StateFulWidget有了升入的认识。

  • 认识了通用的控件
  • 了解了StatefulWidget的生命周期
  • 对BuildContext 了解。
  • 对Key的场景进行了了解。得到了使用GlobalKey来跨子组件传递状态的方式。

下一遍文章:我们将更加深入的对Flutter的界面开发的一些原理

参考文章

Flutter Widgets
Flutter中的Key,LocalKey,GlobalKey... And More
what-are-keys-used-for-in-flutter-framework

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

推荐阅读更多精彩内容