2. Flutter开发入门

Widget简介

Widget概念

        我们知道在Flutter中几乎所有的对象都是一个 widget 。与原生开发中“控件”不同的是,Flutter 中的 widget 的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector 、用于APP主题数据传递的 Theme 等等,而原生开发中的控件通常只是指UI元素。

        Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行事件处理的,所以记住,Flutter 中万物皆为Widget。

Widget接口

        在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget 类的声明:

@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key? key;

  @protected
  @factory
  Element createElement();

  @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;

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  ...
}
  • @immutable: 代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中如果属性发生变化则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。
  • widget:类继承自DiagnosticableTreeDiagnosticableTree即“诊断树”,主要作用是提供调试信息。
  • Key:这个key属性类似于 React/Vue 中的key,主要的作用是决定是否在下一次build时复用旧的 widget ,决定的条件在canUpdate()方法中。
  • createElement():正如前文所述“一个 widget 可以对应多个Element”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(...):复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(...):是一个静态方法,它主要用于在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用new widget去更新Element对象的配置,否则就会创建新的Element

        另外Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在 Flutter 开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承widget类来实现。StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 widget 模型。
 

Flutter中的四棵树

        既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的处理流程是这样的:

  1. 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  2. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  3. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

        真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。我们通过一个例子来说明,假设有如下 Widget 树:

Container( // 一个容器 widget
  color: Colors.blue, // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 显示图片的 widget
      const Text('A'),
    ],
  ),
);

        注意,如果 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:

if (color != null)
  current = ColoredBox(color: color!, child: current);

        而 Image 内部会通过 RawImage 来渲染图片、Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、Element 树、渲染树结构如下图:

 

        这里需要注意:

  1.  三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如 StatelessWidget 和 StatefulWidget 都没有对应的 RenderObject。
  2. 渲染树在上屏前会生成一棵 Layer 树。

StatelessWidget

        StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法:

@override
StatelessElement createElement() => StatelessElement(this);

        StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget 。我们看一个简单的例子:


class Echo extends StatelessWidget{

  final String text;
  final Color bgColor;

  const Echo({super.key, required this.text, this.bgColor = Colors.grey});

  @override
  Widget build(BuildContext context) {

    return 
    Center(
      child: Container(
        color: bgColor,
        child: Text(text),

      ),
    );
  }

}

        上面的代码,实现了一个回显字符串的Echo widget 。 然后我们可以通过如下方式使用它:

 Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

         运行后效果如下所示:

Context 

        build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context是当前 widget 在 widget 树中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。下面是在子树中获取父级 widget 的一个示例:

import 'package:flutter/material.dart';

class ContextRoute extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Context测试'),
      ),
      body: Container(
        child: Builder(builder: (context) {
               Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>()!;
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title!;
        })
        ),
      );
  }


}

        运行后效果如下:

StatefulWidget

        和StatelessWidget一样,StatefulWidget也是继承自widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()

        下面我们看看StatefulWidget的类定义:

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => StatefulElement(this);
    
  @protected
  State createState();
}
  • StatefulElement:  间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。
  • createState():用于创建和 StatefulWidget 相关的状态,它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

State

        一个StatefulWidget类会对应一个State类,State类表示与其对应的StatefulWidget要维护的状态,State中保存的状态信息可以:

  1. 在widget构建时可以被同步读取;
  2. 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter框架状态发生改变,Flutter框架在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。

State中有两个常用属性:

  • widget:它表示与该State实例关联的widget实例,由Flutter框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置State. widget 为新的 widget 实例。
  • context:StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。

State生命周期

        理解State的生命周期对Flutter开发非常重要,为了加深印象,我们通过一个实例来演示一个State的生命周期。我们以计数器功能为例,实现一个计数器CounterWidget组件,点击它可以使计数器加1,由于要保存计数器的数值状态,所以我们应继承StatefulWidget,代码如下

class CounterWidget extends StatefulWidget{

  final int initValue;

  const CounterWidget({super.key, this.initValue = 0});

  @override
  State<StatefulWidget> createState() {
   return _CounterwidgetState();
  }

}

        CounterWidget接收一个initValue整型参数,它表示计数器的初始值。下面我们看一下State的代码:

class _CounterwidgetState extends State<CounterWidget>{

  int _counter = 0;

  @override
  Widget build(BuildContext context) {
     print("build");
    return Scaffold(
      body: Center(
        child: TextButton(onPressed: () =>{
          setState(() {
            ++_counter;
          })
        }, child: Text('$_counter')),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    _counter = widget.initValue;
    print("initState");
  }

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

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

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

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

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

}

        接下来,我们创建一个新路由,在新路由中,我们只显示一个CounterWidget

class StateLifecycleTest extends StatelessWidget {
  const StateLifecycleTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CounterWidget();
  }
}

        我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:

I/flutter ( 7865): initState
I/flutter ( 7865): didChangeDependencies
I/flutter ( 7865): build

        可以看到,在 StatefulWidget插入到widget树时首先initState方法会被调用。

        然后我们点击⚡️按钮热重载,控制台输出日志如下:

I/flutter ( 7865): reassemble
I/flutter ( 7865): didUpdateWidget
I/flutter ( 7865): build

        可以看到此时initState 和didChangeDependencies都没有被调用,而此时didUpdateWidget被调用。 接下来,我们在 widget 树中移除CounterWidget,将 StateLifecycleTest 的 build方法改为:

 @override
  Widget build(BuildContext context) {
    // return CounterWidget();
      return Text("hello world");
  }

        然后热重载,日志如下:

I/flutter ( 7865): reassemble
I/flutter ( 7865): deactive
I/flutter ( 7865): dispose

        我们可以看到,在CounterWidget从 widget 树中移除时,deactivedispose会依次被调用。

        下面我们来看看各个回调函数:

  • initState():当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget),原因是在初始化完成后, widget 树中的InheritFrom widget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
  • didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget ,然后在之后的build() 中Inherited widget发生了变化,那么此时InheritedWidget的子 widget 的didChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies也会被调用。
  • build():它主要是用于构建 widget 子树的,会在如下场景被调用:在调用initState()之后。
  1. 在调用initState()之后。
  2. 在调用didUpdateWidget()之后。
  3. 在调用setState()之后。
  4. 在调用didChangeDependencies()之后。
  5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  • reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • didUpdateWidget():在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调。widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用。
  • deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose():当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

        StatefulWidget生命周期如下图所示:

在widget树中获取State对象 

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

通过Context获取

        context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打开 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget{

  @override
  State<StatefulWidget> createState() {
    return _GetStateObjectRouteState();
  }

}

class _GetStateObjectRouteState extends State<GetStateObjectRoute>{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('子树中获取State对象'),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context){
              return  ElevatedButton(onPressed: (){
              ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
              _state.openDrawer();
            }, child: Text('打开抽屉菜单1'));
            })
           

          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

         一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:

 Builder(builder: (context){
              return  ElevatedButton(onPressed: (){
              ScaffoldState _state = Scaffold.of(context);
              _state.openDrawer();
            }, child: Text('打开抽屉菜单2'));
            }),

         又比如我们想显示 snack bar 的话可以通过下面代码调用:

Builder(builder: (context){
              return  ElevatedButton(onPressed: (){
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('我是SnackBar'))
                );
            }, child: Text('显示SnackBar'));
            }),

        上面示例运行后,点击”显示SnackBar“,效果如下图所示:

通过GlobalKey

        Flutter还有一种通用的获取State对象的方法——通过GlobalKey来获取! 步骤分两步:

  1. 给目标StatefulWidget添加GlobalKey

        

//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)

     2. 通过GlobalKey来获取State对象

_globalKey.currentState?.openDrawer();

        GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey,那么我们便可以通过globalKey.currentWidget获得该 widget 对象、globalKey.currentElement来获得 widget 对应的element对象,如果当前 widget 是StatefulWidget,则可以通过globalKey.currentState来获得该 widget 对应的state对象。

【注意】:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。

通过RenderObject自定义Widget

        StatelessWidget 和 StatefulWidget 都是用于组合其他组件的,它们本身没有对应的 RenderObject。Flutter 组件库中的很多基础组件都不是通过StatelessWidget 和 StatefulWidget 来实现的,比如 Text 、Column、Align等,就好比搭积木,StatelessWidget 和 StatefulWidget 可以将积木搭成不同的样子,但前提是得有积木,而这些积木都是通过自定义 RenderObject 来实现的。实际上Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。下面我们简单演示一下通过RenderObject定义组件的方式:

class CustomWidget extends LeafRenderObjectWidget{

  @override
  RenderObject createRenderObject(BuildContext context) {
    // TODO: implement createRenderObject
    return RenderCustomObject();
  }

  @override
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
    // TODO: implement updateRenderObject
    super.updateRenderObject(context, renderObject);
  }

}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 实现布局逻辑
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

        如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget ,我们可以看一下它的实现:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const LeafRenderObjectWidget({super.key});

  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

        很简单,就是帮 widget 实现了createElement 方法,它会为组件创建一个 类型为 LeafRenderObjectElement 的 Element对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。

        然后我们重写了 createRenderObject 方法,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用(构建渲染树时)用于生成渲染对象。我们的主要任务就是来实现 createRenderObject 返回的渲染对象类,本例中是 RenderCustomObject 。updateRenderObject 方法是用于在组件树状态发生变化但不需要重新创建 RenderObject 时用于更新组件渲染对象的回调。

        RenderCustomObject 类是继承自 RenderBox,而 RenderBox 继承自 RenderObject,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑。

Flutter SDK内置组件库介绍

        Flutter 提供了一套丰富、强大的基础组件,在基础组件库之上 Flutter 又提供了一套 Material 风格( Android 默认的视觉风格)和一套 Cupertino 风格(iOS视觉风格)的组件库。要使用基础组件库,需要先导入:

import 'package:flutter/widgets.dart';

基础组件

  • Text:该组件可让您创建一个带格式的文本。
  • Row、Column:这些具有弹性空间的布局类 widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。
  • Stack:取代线性布局 (译者语:和 Android 中的FrameLayout相似),[Stack](https://docs.flutter.dev/flutter/ widgets/Stack-class.html)允许子 widget 堆叠, 你可以使用 Positioned (opens new window)来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位(absolute positioning )布局模型设计的。
  • Container:可让您创建矩形视觉元素。Container 可以装饰一个BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。 Container (opens new window)也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container (opens new window)可以使用矩阵在三维空间中对其进行变换。

Material组件

        Flutter 提供了一套丰富 的Material 组件,它可以帮助我们构建遵循 Material Design 设计规范的应用程序。Material 应用程序以MaterialApp (opens new window) 组件开始, 该组件在应用程序的根部创建了一些必要的组件,比如Theme组件,它用于配置应用的主题。 是否使用MaterialApp (opens new window)完全是可选的,但是使用它是一个很好的做法。在之前的示例中,我们已经使用过多个 Material 组件了,如:ScaffoldAppBarTextButton等。要使用 Material 组件,需要先引入它:

import 'package:flutter/material.dart';

Cupertino组件

        Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富,但是它仍在不断的完善中。值得一提的是在 Material 组件库中有一些组件可以根据实际运行平台来切换表现风格,比如MaterialPageRoute,在路由切换时,如果是 Android 系统,它将会使用 Android 系统默认的页面切换动画(从底向上);如果是 iOS 系统,它会使用 iOS 系统默认的页面切换动画(从右向左)。由于在前面的示例中还没有Cupertino组件的示例,下面我们实现一个简单的 Cupertino 组件风格的页面:

import 'package:flutter/cupertino.dart';

class CupertinoTestRoute extends StatelessWidget   {

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
            color: CupertinoColors.activeBlue,
            child: const Text("Press"),
            onPressed: () {}
        ),
      ),
    );
  }
  
} 

状态管理 

        响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在 React/Vue(两者都是支持响应式编程的 Web 开发框架)还是 Flutter 中,他们讨论的问题和解决的思想都是一致的。StatefulWidget的状态应该被谁管理?Widget本身?父 Widget ?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:

  • Widget 管理自己的状态。
  • Widget 管理子 Widget 状态。
  • 混合管理(父 Widget 和子 Widget 都管理状态)。

        如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
  • 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。

        在 Widget 内部管理状态封装性会好一些,而在父 Widget 中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理(灵活会显得更重要一些)。

        接下来,我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的 ——创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active确定颜色:绿色为true ,灰色为false

Widget管理自身状态

        我们实现一个TapboxA,在它对应的状态管理类_TapboxAState,管理TapboxA的状态,定义一个_active属性,确定盒子的当前颜色状态,定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。具体实现代码如下:

class TapBoxA extends StatefulWidget{

  const TapBoxA({super.key});


  @override
  State<StatefulWidget> createState() {
    return _TapBoxAState();
  }

}

class _TapBoxAState extends State<TapBoxA>{
  bool _active = false;

  void _handleTap(){
    setState(() {
      _active = !_active;
    });
  }

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

}

        运行效果如下,可以看出通过_active控制盒子的背景状态变化。

父Widget管理子Widget的状态

        对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。在以下示例中,TapboxB通过回调将其状态导出到其父组件,状态由父组件管理,因此它的父组件为StatefulWidget。但是由于TapboxB不管理任何状态,所以TapboxBStatelessWidget

ParentWidgetState 类:

  • 为TapboxB 管理_active状态。
  • 实现_handleTapboxChanged(),当盒子被点击时调用的方法。
  • 当状态改变时,调用setState()更新UI。
class ParentWidget extends StatefulWidget{

  @override
  State<StatefulWidget> createState() {
    return _ParentWidgetState();
  }

}

class _ParentWidgetState extends State<ParentWidget>{
  bool _active = false;

  _handleTapboxChanged(bool newValue){
    setState(() {
      _active = newValue;
    });

  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
         SizedBox(
          height:50,
        ),
        TapBoxB(
        active: _active,
        onChanged: _handleTapboxChanged),
        SizedBox(
          height: 10,
        ),
        TapBoxB(
        active: _active,
        onChanged: _handleTapboxChanged)
      ],
    )
    ;
  }

}

TapboxB 类:

  • 继承StatelessWidget类,因为所有状态都由其父组件处理。
  • 当检测到点击时,它会通知父组件。
class TapBoxB extends StatelessWidget{
  final bool active;
  final ValueChanged<bool> onChanged;
  const TapBoxB({super.key, this.active=false,required this.onChanged});

  void _handleTap(){
    onChanged(!active);
  }


  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Center(
        child: Container(
          child: Text(
            active?'Active':'Inactive',
            style: TextStyle(fontSize: 32,color: Colors.white),
            ),
          width:200.0,
          height: 200.0,
          decoration: BoxDecoration(
            color: active?Colors.lightGreen[700] : Colors.grey[600]
            ),

        ),
         
      ),
     

    );
  }
}

        运行效果如下,单点击其中任意一个盒子,两个盒子的背景色跟着改变。

 

混合状态管理

        对于一些组件来说,混合管理的方式会非常有用。在这种情况下,组件自身管理一些内部状态,而父组件管理一些其他外部状态。

        下面 TapboxC 示例中,手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。 TapboxC 将其_active状态导出到其父组件中,但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState_TapboxCState

_ParentWidgetStateC类:

  • 管理_active 状态。
  • 实现 _handleTapboxChanged() ,当盒子被点击时调用。
  • 当点击盒子并且_active状态改变时调用setState()更新UI。
class ParentWidgetC extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return _ParentWidgetCState();
  }
}

class _ParentWidgetCState extends State<ParentWidgetC>{

  bool _active = false;

  void _handleTapboxChanged(bool newValue){
    setState(() {
      _active = newValue;
    });
  }


  @override
  Widget build(BuildContext context) {
   return TapBoxC(
    active: _active,
    onChanged: _handleTapboxChanged,
   );
  }

}

_TapboxCState 对象:

  • 管理_highlight 状态。
  • GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
  • 当按下、抬起、或者取消点击时更新_highlight状态,调用setState()更新UI。
  • 当点击时,将状态的改变传递给父组件。
class TapBoxC extends StatefulWidget{
  final bool active;
  final ValueChanged<bool> onChanged;
  const TapBoxC({super.key,this.active = false,required this.onChanged});


  @override
  State<StatefulWidget> createState() {
    return _TapBoxCState();
  }
  
}

class _TapBoxCState extends State<TapBoxC>{

  bool _higlight = false;

  void _handleTapDown(TapDownDetails details){
    setState(() {
      _higlight = true;
    });

  }

  void _handleTapUp(TapUpDetails details){
      setState(() {
      _higlight = false;
    });
  }

  void _handleTap(){
    widget.onChanged(!widget.active);
  }

  void _handleTapCancel(){
      setState(() {
      _higlight = false;
    });
  }
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: _handleTapDown,
      onTapUp: _handleTapUp,
      onTapCancel: _handleTapCancel,
      onTap: _handleTap,
      child: Center(
        child: Container(
          child: Text(
            widget.active ? 'Active':'Inactive',
            style: TextStyle(fontSize: 32,color: Colors.white),
          ),
          width: 200,
          height: 200,
          decoration: BoxDecoration(
            color: widget.active?Colors.lightGreen[700] : Colors.grey[600],
            border: _higlight?Border.all(
              color: Colors.teal[700]!,
              width: 10
            ):null
          ),
        ),
      ),
    );
  }
}

        运行效果如下所示: 

 

全局状态管理

        当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,App中依赖应用语言的组件能够重新 build 一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:

  1. 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。
  2. 使用一些专门用于状态管理的包,如 Provider、Redux。

路由管理

        路由(Route)在移动开发中通常指页面(Page),这跟 Web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android中 通常指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter 中的路由管理和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

        我们创建一个页面,命名为"Page1",继承自StatelessWidget,界面很简单,在页面添加一个Text和TextButton,具体代码实现如下:

class Page1 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
        children: [
        SizedBox(
          height: 100,
        ),
        Text('Page 1',style: TextStyle(fontSize: 32),),
        TextButton(
          onPressed: (){
            Navigator.push(
              context, 
              MaterialPageRoute(builder:(context){
                return Page2();
              })
            );
          }, 
          child: Text('Next'))

      ],
    ),
      )
    );
  
  }
}

        再新建一个页面,命名为"Page2", 也继承自StatelessWidget,在页面中间显示一句"Page 2",具体代码如下:

class Page2 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Text('Page 2',
          style: TextStyle(fontSize: 32),),
          
        )
      ),
    )
    ;
  }
  
}

        运行之后的效果如下:

 

MaterialPageRoute

Navigator

路由传值

命名路由

路由生成钩子

总结

包管理

资源管理

调试Flutter应用

Flutter异常捕获

引用声明:简介 | 《Flutter实战·第二版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值