跨平台

Flutter了解之入门篇1(Widget)

2022-09-18  本文已影响0人  平安喜乐698
目录
  1. 模版示例(计数器)
  2. Widget
  3. 内置组件库、自定义组件

1. 模版示例(计数器)

AndroidStudio创建的Flutter应用 默认是一个计数器示例(每点击一次右下角的加号按钮,屏幕中央的数字就会加1)。


  1. 程序入口(lib目录下的main.dart文件)
// import:用来导入文件。
import 'package:flutter/material.dart'; // 导入Material UI组件库。
// 应用入口,启动Flutter应用后会调用
void main() {
  // runApp方法接受一个根Widget参数(Flutter框架会强制根widget覆盖整个屏幕)。
  runApp(MyApp());
}
// 根widget
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // MaterialApp:Material库提供的App框架组件,可用来设置应用的名称、主题、语言、首页及路由列表等,创建了一些有用的widget(如:Navigator)。
    // 是否使用MaterialApp是可选的,但推荐使用。使用了MaterialApp则必须使用Scaffold。
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // 设置主题色为蓝色(会影响:导航栏颜色、底部Tab的选中色)
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      // 首页
      home: MyHomePage(title: 'Hello Home Page'),
    );
  }
}
// 首页,继承自StatefulWidget类(有状态的组件)
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  // _MyHomePageState类是MyHomePage类对应的状态类。
  // 和StatelessWidget类不同, StatefulWidget类中并没有build方法(被挪到了State中)。
  _MyHomePageState createState() => _MyHomePageState();
}
// State状态类
class _MyHomePageState extends State<MyHomePage> {
  // 该组件的状态
  // 定义一个_counter状态用于记录按钮的点击次数
  int _counter = 0;
  // 点击按钮后调用,更新状态继而更新UI
  void _incrementCounter() {
    // setState方法的作用是通知Flutter框架,有状态发生了改变,Flutter框架收到通知后,会执行build方法来根据新的状态重新构建界面。
    setState(() {
      _counter++;
    });
  }
  // 组件的主要工作:提供一个build()方法返回构建的UI界面(通常通过组合其它基础widget来构建UI)。
  // 1. 当MyHomePage第一次创建时,_MyHomePageState类会被创建,当初始化完成后,Flutter框架会调用Widget的build()方法来构建widget树,最终会将widget树渲染到设备屏幕上。
  // 2. 调用setState后会重新调用。
  @override
  Widget build(BuildContext context) {
    // Scaffold 是Material库中提供的页面脚手架(提供了导航栏、body、悬浮按钮等属性)。
    return Scaffold(
      appBar: AppBar(  // 导航栏
        title: Text(widget.title),
      ),
      // Center组件会将其子组件树对齐到屏幕中心。Column组件会将其子组件沿屏幕垂直方向依次排列。
      // Center子组件是一个Column组件,Column子组件是两个Text组件。
      body: Center(  
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      // 页面右下角的带“+”的悬浮按钮,onPressed属性接受一个点击后的回调处理函数。
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // 后面的逗号便于后期自动格式化排版。
    );
  }
}
  1. 程序配置(根目录下的pubspec.yaml文件)
#项目名
name: flutter_textfield_app
#项目描述
description: A new Flutter project.
#避免通过pub publish发布到pub.dev上。表明这是个私有项目。
publish_to: 'none' 

#版本名和版本号通过+分开
#通过指定 --build-name 和 --build-number 来设置版本名、版本号
#在Android中,build name用作versionName,而build number用作versionCode。
#在iOS中,内部版本名用作CFBundleShortVersionString,而内部版本号用作CfBundLeverVersion。
version: 1.0.0+1

#FlutterSDK版本
environment:
  sdk: ">=2.7.0 <3.0.0"

#依赖包(生产环境)
dependencies:
  flutter:
    sdk: flutter

#依赖包(开发环境)
dev_dependencies:
  flutter_test:
    sdk: flutter

#指定图片、字体等资源
flutter:
  #包含了Material Icons
  uses-material-design: true

2. Widget

Flutter中万物皆为Widget:几乎所有的对象(UI组件、功能组件、布局组件)都是一个Widget。

1. widget的主要工作是实现build方法(构建自身UI)。
2. 一个widget通常由一些较低级别(层级)widget组成,Flutter框架将依次构建这些widget,直到构建到最底层的widget(RenderObject)。
3. Widget树实际上是一个配置树,Element树才是真正的UI渲染树(二者存在对应关系)。
  Widget的功能是“描述一个UI元素的配置数据”,即Widget并不是表示最终绘制在设备屏幕上的显示元素(Element才是),它只是描述显示元素的配置数据。
4.有状态组件(Stateful widget) 和无状态组件(Stateless widget)的区别:
  1. Stateful widget可以拥有状态,这些状态在widget生命周期中是可以变的,而Stateless widget是不可变的。
  2. Stateful widget至少由两个类组成:
    一个StatefulWidget类;一个 State类。
    StatefulWidget类本身是不可变的,但是State类中持有的状态在widget生命周期中可能会发生变化。
/*
build方法放在State类中,而不是放在StatefulWidget中的原因。
如果将build()方法放在StatefulWidget中:
  1. 状态访问不便。
    build方法构建UI时读取state(State的state就需要对外公开,不方便、不安全)。
    state改变时需要调用build方法更新UI。
  2. 继承StatefulWidget不便。例如:
    Flutter中有一个动画widget的基类AnimatedWidget,它继承自StatefulWidget类。AnimatedWidget中有一个抽象方法build(BuildContext context),继承自AnimatedWidget的动画widget都要实现这个build方法。
    现在设想一下,如果StatefulWidget 类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个state对象,这就意味着AnimatedWidget必须将自己的State对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build方法中调用父类的build方法。
    这样很显然是不合理的,因为:AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部;如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
*/
Flutter组件库中的很多基础组件(Text 、Column、Align等,好比积木)是通过自定义RenderObject来实现的,而不是通过StatelessWidget 和 StatefulWidget(组合其他组件,本身没有对应的RenderObject,负责塔积木)来实现的。
  1. Widget 抽象类,继承自DiagnosticableTree(诊断树,主要作用是提供调试信息)

不会直接继承Widget类来实现新组件,而是通过继承StatelessWidget或StatefulWidget(继承自Widget类)。

Widget定义如下:

// @immutable:限制Widget没有可变状态,所有属性都必须是final。这是因为,Flutter中如果属性发生变化则会重新构建Widget树,即重新创建新的Widget实例来替换旧的Widget实例,所以允许Widget的属性变化是没有意义的,因为一旦Widget的属性变了Widget自己就会被替换。
@immutable
abstract class Widget extends DiagnosticableTree {
  // key:决定是否在下一次build时复用旧的Element,决定的条件在canUpdate()方法中
  const Widget({ this.key });
  final Key key;

  // Flutter框架在构建UI树时,会先调用此方法(开发过程中基本不会主动调用)生成对应节点的Element对象。
  @protected
  Element createElement();

  // Widget的字符串描述
  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }
  // 覆写父类的方法,主要是设置诊断树的一些特性
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;
  @override
  @nonVirtual
  int get hashCode => super.hashCode;

  // 是否用新Widget对象去更新旧UI树上所对应的Element对象的配置;只要newWidget与oldWidget的runtimeType和key同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element。key都为null也代表key相等。
  // 多数情况下都返回true。
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  // 返回1是StatefulWidget,返回2是StatelessWidget,返回0是其他widget
  static int _debugConcreteSubtype(Widget widget) {
    return widget is StatefulWidget ? 1 :
           widget is StatelessWidget ? 2 :
           0;
    }
}
  1. StatelessWidget 抽象类,继承自Widget(重写了createElement方法)无状态组件

用于不需要维护状态的场景。无状态组件不会发生状态改变(即没有可变状态),所有属性都是final类型(通常在父组件中调用无状态组件的构造函数时赋值)。

StatelessWidget定义如下:

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key key }) : super(key: key);
  // 一般子类不重写该方法。StatelessElement 间接继承自Element。
  @override
  StatelessElement createElement() => StatelessElement(this);

  // context(BuildContext类的实例,实际就是element对象)表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。
  // 当widget插入到树中时会调用,调用setState后会调用build来重新构建UI。
  // 通常在build方法中通过嵌套其它Widget来构建UI。
  @protected
  Widget build(BuildContext context);
}

示例(一个灰色背景字符串的widget)

class HelloWidget extends StatelessWidget {
  final String text;
  final Color backgroundColor;
   // widget的构造函数参数应使用命名参数(规范-语义更清晰),命名参数中的必要参数要添加@required标注(这样有利于静态代码分析器进行检查)。
  // 第一个参数通常应该是Key。如果需要接收子Widget,那么child或children参数通常应被放在参数列表的最后。
  // Widget的属性应声明为final,防止被意外改变。
  const HelloWidget({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}
在其他widget的build中使用:
Widget build(BuildContext context) {
  return HelloWidget(text: "hello world");
}

示例2(在子树中获取父级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>();
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}
  1. StatefulWidget 抽象类(继承自Widget,重写了createElement方法,添加了createState方法)有状态组件
StatefulWidget定义如下:

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
  // 一般子类不重写该方法,StatefulElement间接继承自Element类。
  @override
  StatefulElement createElement() => StatefulElement(this);

  // 子类需要重写该方法,返回关联的state状态。
  @protected
  State createState();
}

1. 在继承StatefulWidget重写其方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中先调用父类方法。
2. 在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的。即子widget到父widget是通过事件通信,父向子共享状态。

State类

一个StatefulWidget类对应一个State类,State表示与其对应的StatefulWidget要维护的状态。

State中的状态信息:
    1. 在widget构建时可以被读取。
    2. 可以改变,改变后可以手动调用其setState()方法通知Flutter框架 状态发生改变,Flutter框架会重新调用其build方法重新构建widget树来更新UI。

State中两个常用属性:
    1. widget(与该State关联的widget,由Flutter框架动态设置)。
      State实例只会在第一次插入到树中时被创建。
      在重新构建时,如果widget被修改,Flutter框架会动态设置State.widget为新的widget实例。
    2. context。
      StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。
/*
调用setState刷新StatefulWidget的官方使用建议:

  1. 尽可能将组件树的状态维护往下推到叶子节点。
    状态维护层级越高,意味着重建的组件树越大。
  2. 最小化StatefulWidget的State中build方法构建组件的数量。
  3. 如果子组件树在整个生命周期都不改变的话,那么应该考虑将该子组件树抽离缓存起来重复利用。
  4. 尽可能地使用const修饰子组件构造方法。
  5. 尽可能避免更改子组件树的层级或子组件树中的组件类型。任何更改子组件树深度的操作都会需要重新构建、重新布局、重新绘制整个子组件树。IgnorePointer包裹。
  6. 假设不得不更改子组件树的层级,那么应该考虑将子组件树中不变的部分使用 GlobalKey 使得这部分在整个StatefulWidget的生命周期都保持一致。如果不方便使用GlobalKey的话,那么可以考虑使用KeyedSubtree组件来应用GlobalKey。
  7. 如果StatefulWidget中有些属性是不变的话,那么 这些属性的定义应该优先放在 `Widget` 的定义中,并声明为final,而不是State中,这样可以减少State需要维护的数据。
*/
StatefulWidget的生命周期

示例(了解StatefulWidget的生命周期)

实现一个计数器widget,点击它可以使计数器加1,由于要保存计数器的数值状态,所以应继承StatefulWidget
class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });
  final int initValue;
  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;
  // Widget第一次插入到Widget树时会被调用。
  // 对于每一个State对象,Flutter框架只会调用一次该回调。通常在该方法中做一些一次性操作(如:请求网路、状态初始化、订阅子树的事件通知等)。
  // 不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在Widget树上获取离当前widget最近的一个父级InheritFromWidget),原因是在初始化完成后,Widget树中的InheritFromWidget可能会发生变化,所以正确的做法应该在build方法或didChangeDependencies方法中调用它。
  @override
  void initState() {
    super.initState();
    _counter=widget.initValue;  // 初始化状态  
    print("initState");
  }
/*
用于构建Widget子树的,会在如下场景被调用:
    在调用initState()之后。
    在调用didUpdateWidget()之后。
    在调用setState()之后。
    在调用didChangeDependencies()之后。
    在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
*/
  // 构建Widget
  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          onPressed:()=>setState(()=> ++_counter, // 点击后计数器自增
          ),
        ),
      ),
    );
  }
  // widget发生改变需要重新构建时,Flutter框架会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点来决定是否需要更新,如果返回true(会在新旧widget的key和runtimeType同时相等时返回true)则会调用该方法。
  // 该方法调用之后会调用build方法。
  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }
  // (State对象)从树中被移除时会调用。有时Flutter框架会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。
  // 如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }
  // (State对象)从树中被永久移除时调用。
  // 通常在该方法中释放资源(定时器、未完成的动画)。
  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }
  // 专门为了开发调试而提供,在热重载时会被调用。该方法调用之后会调用didUpdateWidget方法。
  // 在Release模式下永远不会被调用。
  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }
  // initState方法调用后会调用该方法。该方法调用之后会调用build方法。
  // 会在依赖(是否使用了父辈widget中InheritedWidget的数据)改变后调用该方法。比如当主题、locale(语言)改变时,依赖其的子widget会调用该方法。
  //  子widget一般很少重写该方法,因为在依赖改变后Flutter Framework也会调用build()方法来更新UI。但如果需要在依赖改变后执行一些昂贵的操作(网络请求),则最好在该方法中执行,可以避免每次build()都执行这些昂贵操作。
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}
// 在其他组件的build方法中使用自定义的CounterWidget
Widget build(BuildContext context) {
  return CounterWidget();
}
终端输出
flutter: initState
flutter: didChangeDependencies
flutter: build
保存,终端输出
flutter: reassemble
flutter: didUpdateWidget
flutter: build
将return CounterWidget();改为return Text("xxx");并保存,终端输出
flutter: reassemble
flutter: deactive
flutter: dispose

在Widget树中获取State对象

由于StatefulWidget的的具体逻辑都在其State中,所以很多时候,需要获取StatefulWidget对应的State对象来调用一些方法。比如Scaffold组件对应的状态类ScaffoldState中就定义了打开SnackBar(路由页底部提示条)的方法。

在子widget树中获取父辈级StatefulWidget的State对象:
  方法1. 通过Context获取
    context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。
  方法2. 通过GlobalKey获取
    第一步:给目标StatefulWidget添加GlobalKey
    // 定义一个globalKey, 由于GlobalKey要保持全局唯一性,使用使用静态变量存储
    static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
    Scaffold(
        key: _globalKey , //设置key
    )
    第二步:通过GlobalKey来获取State对象
    _globalKey.currentState.openDrawer()
/*
如果一个widget设置了GlobalKey,那么便可以通过
  globalKey.currentWidget 获得该widget对象
  globalKey.currentElement 获得该widget对象对应的element对象
  globalKey.currentState 获得该widget对象对应的state对象(如果当前widget是StatefulWidget)。

使用GlobalKey开销较大,如果有其他可选方案,应尽量避免使用它。
*/
示例(通过Context获取State对象)

Scaffold(
  appBar: AppBar(
    title: Text("子树中获取State对象"),
  ),
  body: Center(
    child: Builder(builder: (context) {
      return RaisedButton(
        onPressed: () {
          // 查找父级最近的Scaffold对应的ScaffoldState对象,调用state的showSnackBar方法来弹出SnackBar
          ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
          _state.showSnackBar(
            SnackBar(
              content: Text("我是SnackBar"),
            ),
          );
        },
        child: Text("显示SnackBar"),
      );
    }),
  ),
);
如果StatefulWidget的状态是私有的(不应该向外部暴露),那么代码中就不应该去直接获取其State对象;如果StatefulWidget的状态是希望暴露出的,则可以去直接获取其State对象。
但通过context.findAncestorStateOfType获取StatefulWidget的状态的方法是通用的(并不能看出StatefulWidget的状态是否私有),所以在Flutter开发中便有了一个默认的约定【如果StatefulWidget的状态是希望暴露出的,应提供一个of静态方法来获取其State对象;如果State不希望暴露,则不提供of方法】。
上面示例中的Scaffold就提供了一个of静态方法来直接获取ScaffoldState:
ScaffoldState _state=Scaffold.of(context);   
_state.showSnackBar(
  SnackBar(
    content: Text("我是SnackBar"),
  ),
);

setState方法(避免使用该方法来更新UI,除非组件树中的组件很少)

setState方法定义在State<T extends StatefulWidget> with Diagnosticable这个类中。
  首先使用assert断言进行了如下限制:
    1. 传给setState的回调方法不能为空。
    2. 不能在dispose后调用setState(已经从组件树中移除)。
      可能发生在定时器、动画或异步回调的过程中,会导致内存泄露。
    3. 不能在created阶段和!mounted未装载阶段调用setState(即不能在构造函数里调用 setState)。
      通常应在 initState 之后调用 setState。
    4. setState的回调方法不能返回Future对象(即不能在setState中执行异步操作)。
  最后调用了_element!.markNeedsBuild(); 

继续查看源码发现:
  获取BuildContext的方法,返回的也是_element(StatefulElement类型)。
  StatefulElement继承自ComponentElement,ComponentElement继承自Element,Element继承自DiagnosticableTree并实现了BuildContext接口。markNeedsBuild方法是在Element类中定义的。
Element类详情如下:

element元素的生命周期
Element中关键的属性和方法:
  1. _parent属性
    父元素(可为空)。
  2. _depth属性
    元素在组件树中的层级,根节点的该值必须大于0。
  3. _widget属性
    元素对应的widget。
  4. _owner属性
    管理元素生命周期的对象。
  5. _lifecycleState属性
    生命周期状态(默认是initial状态)。
  6. 获取renderObject的get方法
    会递归调用返回元素及其子元素中需要渲染的对象(子元素是RenderObjectElement对象)。
  7. _sort方法
    比较两个Element元素的层级,层级值(_depth)越大,层级越深,显示的层也就越靠前。
  8. reassemble方法
    debug热重载时会调用。该方法处理将元素自身标记为需要build外(调用 markNeedsBuild 方法),还会递归遍历全部子节点,调用子节点的 reassemble 方法。
  9. markNeedsBuild方法
    _dirty = true;  // 标记元素为dirty状态
    owner!.scheduleBuildFor(this);  // 会调用rebuild方法。
  10. updateChild
    渲染过程的核心方法,通过新的组件配置来更新子元素。存在4种情况:
      1. 如果child为空而newWidget不为空,则创建一个新元素来渲染。
      2. 如果child不为空而newWidget为空,则表明组件配置中已经没有child这个元素了,因此需要移除它。
      3. 如果二者都不为空,则需要根据child的当前是否可以更新(Widget.canUpdate)来处理,如果可以更新,那么使用新的组件配置更新元素;否则需要移除旧的元素,并使用新的组件配置创建一个新的元素。
      4. 如果二者都为空,那就什么都不做。
   返回的结果也分为3种情况:
      1. 如果创建了一个新的元素,则返回新构建的子元素。
      2. 如果旧的元素被更新,返回更新后的子元素。
      3. 如果子元素被移除,而没有新的替换的话,返回null。
  11. mount方法
    在新元素首次被创建时调用,按照给定的插入位置将元素插入给定的父节点。
    调用该方法后,元素的状态会从 initial 改为 active。
    会将子元素的层级(_depth)设置为父元素的层级+1。
  12. unmount方法
    状态从inactive改为defunct(不再存在)时调用,此时元素将脱离渲染树,并且再也不会在渲染树中存在。
  13. update方法
    当父节点使用新的配置组件更新元素时调用(要求新的配置类型和旧的保持一致)。
  14. detachRenderObject和 attachRenderObject方法
    分别对应从组件树移除和添加RenderObject。
  15. deactivateChild方法
    将子元素加入到不活跃的元素列表,之后再从渲染树中移除。
  16. activate方法
    状态从inactive切换到active时调用。
    注意组件第一次挂载的时候不会调用这个方法而是mount方法。
  17. deactivate方法
    状态从active切换到inactive时调用,也就是元素被移入到不活跃列表的时候会被调用。
  18. didChangeDependencies方法
    当元素的依赖发生改变时调用,该方法也会调用 markNeedBuild 方法。
  19. rebuild方法
    当元素的BuildOwner对象调用scheduleBuildFor方法时会调用本方法。
    调用performRebuild方法来重建元素。
    // 首次装载时是在mount方法中触发,配置组件更改时会在build方法触发。
  20. performRebuild方法
    在Element中,该方法是个空方法,需要子类去实现(即每个元素具体怎么重建由子类来决定)。
/*
先来看StatefulElement的performRebuild实现。
@override
void performRebuild() {
  if (_didChangeDependencies) {
    state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}
继续看StatefulElement的父类ComponentElement的performRebuild实现。
@override
void performRebuild() {
  // 省略调试的代码
  Widget? built;
  try {
    // ...
    built = build();  // 所以build方法中不能调用setState方法,会导致循环调用。
    // ...
  } catch (e, stack) {
    // ...
  } finally {
    _dirty = false;
    // ...
  }
  try {
    _child = updateChild(_child, built, slot);
    assert(_child != null);
  } catch (e, stack) {...//}
}
通过 built = build() 获取了最新的Widget,再调用updateChild方法更新子元素。
*/

State状态管理(响应式编程的永恒主题)

无论是在React/Vue(支持响应式编程的Web开发框架)还是Flutter中,讨论的问题和解决的思想都是一致的。

管理状态的方法:
  1. Widget管理自身状态。
  2. Widget管理子Widget状态。
  3. 混合管理(父子Widget都管理状态)。
  4. 全局状态管理(跨组件状态共享)。
    例:如果有一个设置页(可设置应用的语言),改变语言后,希望APP中依赖应用语言的组件都能重新build一下,但这些依赖应用语言的组件和设置页并没有关联。
    需要通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
      1. 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。
      2. 使用InheritedWidget组件或Provider、Redux等状态管理库(详情见功能组件篇)。

如何决定使用哪种管理方法:
  1. 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
  2. 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
  3. 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
  4. 在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活。
  5. 组件之间相距较远或没有关联时使用全局状态管理。

示例(管理自身状态)

//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);
  @override
  _TapboxAState createState() => new _TapboxAState();
}
/*
_TapboxAState 类:
    管理TapboxA的状态。
    定义_active:确定盒子的当前颜色的布尔值。
    定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。
    实现widget的所有交互式行为。
*/
class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: _handleTap,
      child: new Container(
        child: new Center(
          child: new Text(
            _active ? 'Active' : 'Inactive',
            style: new TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: new BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

示例(管理子组件TapboxB的状态)

//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => new _ParentWidgetState();
}
/*
ParentWidgetState 类:
    为TapboxB 管理_active状态。
    实现_handleTapboxChanged(),当盒子被点击时调用的方法。
    当状态改变时,调用setState()更新UI。
TapboxB 类:
    继承StatelessWidget类,因为所有状态都由其父组件处理。
    当检测到点击时,它会通知父组件。
*/
class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;
  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Container(
      child: new TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);
  final bool active;
  final ValueChanged<bool> onChanged;
  void _handleTap() {
    onChanged(!active);
  }
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: _handleTap,
      child: new Container(
        child: new Center(
          child: new Text(
            active ? 'Active' : 'Inactive',
            style: new TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: new BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

示例(混合管理)

/*
_ParentWidgetStateC对象:
    管理_active 状态。
    实现 _handleTapboxChanged方法(当盒子被点击时调用),改变_active状态并调用setState()更新UI。
_TapboxCState对象:
    管理_highlight 状态。
    GestureDetector监听tap事件。
*/
//---------------------------- ParentWidget ----------------------------
class ParentWidgetC extends StatefulWidget {
  @override
  _ParentWidgetCState createState() => new _ParentWidgetCState();
}
class _ParentWidgetCState extends State<ParentWidgetC> {
  bool _active = false;
  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Container(
      child: new TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
  TapboxC({Key key, this.active: false, @required this.onChanged})
      : super(key: key);
  final bool active;
  final ValueChanged<bool> onChanged;
  @override
  _TapboxCState createState() => new _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;
  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }
  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }
  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }
  void _handleTap() {
    widget.onChanged(!widget.active);
  }
  @override
  Widget build(BuildContext context) {
    // 在按下时添加绿色边框,当抬起时,取消高亮  
    return new GestureDetector(
      onTapDown: _handleTapDown, // 处理按下事件
      onTapUp: _handleTapUp, // 处理抬起事件
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: new Container(
        child: new Center(
          child: new Text(widget.active ? 'Active' : 'Inactive',
              style: new TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
        width: 200.0,
        height: 200.0,
        decoration: new BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? new Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

示例(优化:显示和逻辑分离)

========================原代码=============
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
  int _counter = 0;
  void _increment() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Row(
      children: <Widget>[
        new RaisedButton(
          onPressed: _increment,
          child: new Text('Increment'),
        ),
        new Text('Count: $_counter'),
      ],
    );
  }
}

========================优化后=============
将计数器显示和更改逻辑分离。
责任分离允许将复杂性逻辑封装在各个widget中,同时保持父项的简单性。

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});
  final int count;
  @override
  Widget build(BuildContext context) {
    return new Text('Count: $count');
  }
}
class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});
  final VoidCallback onPressed;
  @override
  Widget build(BuildContext context) {
    return new RaisedButton(
      onPressed: onPressed,
      child: new Text('Increment'),
    );
  }
}
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
  int _counter = 0;
  void _increment() {
    setState(() {
      ++_counter;
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Row(children: <Widget>[
      new CounterIncrementor(onPressed: _increment),
      new CounterDisplay(count: _counter),
    ]);
  }
}

示例

void main() {
  runApp(new MaterialApp(
    title: 'Shopping App',
    home: new ShoppingList(
      products: <Product>[
        new Product(name: 'Eggs'),
        new Product(name: 'Flour'),
        new Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}
class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);
  final List<Product> products;
  @override
  _ShoppingListState createState() => new _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = new Set<Product>();
  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      if (inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Shopping List'),
      ),
      body: new ListView(
        padding: new EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return new ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}
// 商品
class Product {
  const Product({this.name});
  final String name;
}
typedef void CartChangedCallback(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({Product product, this.inCart, this.onCartChanged})
      : product = product,
        super(key: new ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;
  Color _getColor(BuildContext context) {
    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }
  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;
    return new TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }
  @override
  Widget build(BuildContext context) {
    return new ListTile(
      onTap: () {
        onCartChanged(product, !inCart);
      },
      leading: new CircleAvatar(
        backgroundColor: _getColor(context),
        child: new Text(product.name[0]),
      ),
      title: new Text(product.name, style: _getTextStyle(context)),
    );
  }
}

3. 内置组件库、自定义组件

  1. 内置组件库

Flutter提供了一套丰富强大的基础组件库,在基础组件库之上又提供了一套Material风格(Android默认的视觉风格)和一套Cupertino风格(iOS视觉风格)的组件库。

  1. 基础组件库
需要导入:import 'package:flutter/widgets.dart';
如果引入了Material或Cupertino则不需要再引入,因为他们内部已经引入过了。
Text
  文本组件。
    
Row、 Column
  对子组件水平、垂直布局。
  其设计是基于Web开发中的Flexbox布局模型。
    
Stack
  允许子widget堆叠 (类似Android的FrameLayout), 可使用 Positioned来定位子组件相对于Stack的上下左右的位置。
  其设计是基于Web开发中的绝对定位布局模型。
    
Container
  矩形容器。
  可以装饰一个BoxDecoration(background、边框、阴影)。有边距、填充和约束。
  类似html的div。
  1. Material组件库
需要导入:import 'package:flutter/material.dart';

Flutter提供了一套丰富的Material组件,它可以构建遵循Material Design设计规范的应用程序。
Material应用程序以MaterialApp组件开始, 该组件在应用程序的根部创建了一些必要的组件,比如Theme组件,它用于配置应用的主题。 是否使用MaterialApp完全是可选的,但建议使用。
Material 组件库中有一些组件可以根据实际运行平台来切换表现风格,比如MaterialPageRoute,在路由切换时,如果是Android系统,它将会使用Android系统默认的页面切换动画(从底向上);如果是iOS系统,它会使用iOS系统默认的页面切换动画(从右向左)。
  1. Cupertino组件库
需要导入:import 'package:flutter/cupertino.dart';

Flutter提供了一套丰富的Cupertino风格的组件(目前没有Material组件丰富,仍在不断的完善中)。

示例(使用Cupertino组件)

import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
            color: CupertinoColors.activeBlue,
            child: Text("Press"),
            onPressed: () {}
        ),
      ),
    );
  }
}

示例(Material组件)

// (默认已经设置)确保在pubspec.yaml文件中,将flutter的值设置为:uses-material-design: true。
import 'package:flutter/material.dart';
void main() {
  runApp(new MaterialApp(    // 为了继承主题数据,widget需要位于MaterialApp内才能正常显示
    title: 'My app', 
    home: new MyScaffold(),
  ));
}
// MyScaffold 通过一个Column widget,在垂直方向排列其子项。在Column的顶部,放置了一个MyAppBar实例,将一个Text widget作为其标题传递给应用程序栏。最后,MyScaffold使用了一个Expanded来填充剩余的空间,正中间包含一条message。
class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material 是UI呈现的“一张纸”
    return new Material(
      // Column is 垂直方向的线性布局.
      child: new Column(
        children: <Widget>[
          new MyAppBar(
            title: new Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          new Expanded(
            child: new Center(
              child: new Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}
// 在MyAppBar中创建一个Container,高度为56像素(像素单位独立于设备,为逻辑像素),其左侧和右侧均有8像素的填充。在容器内部, MyAppBar使用Row 布局来排列其子项。 中间的title widget被标记为Expanded, ,这意味着它会填充尚未被其他子项占用的的剩余可用空间。Expanded可以拥有多个children, 然后使用flex参数来确定他们占用剩余空间的比例。
class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});
  final Widget title;
  @override
  Widget build(BuildContext context) {
    return new Container(
      height: 56.0, // 单位是逻辑上的像素(并非真实的像素,类似于浏览器中的像素)
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: new BoxDecoration(color: Colors.blue[500]),
      // Row 是水平方向的线性布局(linear layout)
      child: new Row(
        // 列表项的类型是 <Widget>
        children: <Widget>[
          new IconButton(
            icon: new Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null 会禁用 button
          ),
          // Expanded expands its child to fill the available space.
          new Expanded(
            child: title,
          ),
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}
  1. 自定义组件

当Flutter提供的现有组件无法满足需求,或者为了共享代码需要封装一些通用组件时,就需要自定义组件。

3种方式:
  1. 组合其它组件(通过组合其它组件来组成一个新组件)
  2. 自绘(通过CustomPaint和Canvas来实现)
  3. 实现RenderObject

详见篇11(自定义组件)

其他

  1. 解决Flutter嵌套过深问题
1. 使用类构建(建议)的好处
  1. 支持性能优化(如:使用const构造方法)
  2. 两个不同的布局切换时,能够正确地销毁对应的资源。
  3. 保证正确的方式进行热重载,而使用函数可能破坏热重载。
  4. 在 Widget Inspector 中可以查看得到,从而可以方便定位和调试问题。
  5. 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰的名词。
  6. 可以使用 key 提高性能。
  7. 可以使用 context 提供的方法(函数式组件需要显示地传递context)。

2. 使用函数构建(不建议)的好处
  1. 代码量少(通过快捷代码片段构建类时,相差不多)。
    functional_widget插件(通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)
上一篇 下一篇

猜你喜欢

热点阅读