Skip to main content

状态管理

Flutter 应用的_状态_ 指的是它用于显示 UI 或管理系统资源的所有对象。状态管理是我们将应用组织起来以最有效地访问这些对象并在不同的 Widget 之间共享它们的方式。

本页面探讨了状态管理的许多方面,包括:

  • 使用 StatefulWidget
  • 使用构造函数、InheritedWidget 和回调在 Widget 之间共享状态
  • 使用 Listenable 在某些内容更改时通知其他 Widget
  • 为您的应用程序架构使用模型-视图-视图模型 (MVVM)

有关状态管理的其他介绍,请查看以下资源:

教程:状态管理。这展示了如何将 ChangeNotiferprovider 包一起使用。

本指南不使用 provider 或 Riverpod 等第三方包。相反,它只使用 Flutter 框架中提供的基本元素。

使用 StatefulWidget

#

管理状态最简单的方法是使用 StatefulWidget,它在自身内部存储状态。例如,考虑以下 Widget:

dart
class MyCounter extends StatefulWidget {
  const MyCounter({super.key});

  @override
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        TextButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text('Increment'),
        )
      ],
    );
  }
}

这段代码说明了在考虑状态管理时需要理解的两个重要概念:

  • 封装: 使用 MyCounter 的 Widget 无法看到底层的 count 变量,也无法访问或更改它。
  • 对象生命周期: _MyCounterState 对象及其 count 变量在第一次构建 MyCounter 时创建,并在将其从屏幕中移除之前一直存在。这是一个 短暂状态 的示例。

您可能会发现以下资源很有用:

在 Widget 之间共享状态

#

应用需要存储状态的一些场景包括:

  • 更新共享状态并通知应用的其他部分
  • 监听共享状态的更改,并在其更改时重建 UI

本节探讨了如何在应用中有效地在线程之间共享状态。最常见的模式是:

  • 使用 Widget 构造函数(在其他框架中有时称为“属性传递”)
  • 使用 InheritedWidget(或类似的 API,例如 provider 包)。
  • 使用回调通知父 Widget 某些内容已更改

使用 Widget 构造函数

#

由于 Dart 对象是通过引用传递的,Widget 通常在其构造函数中定义它们需要使用的对象是很常见的。传递到 Widget 构造函数的任何状态都可以用来构建其 UI:

dart
class MyCounter extends StatelessWidget {
  final int count;
  const MyCounter({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

这使得您的 Widget 的其他用户可以清楚地知道他们需要提供什么才能使用它:

dart
Column(
  children: [
    MyCounter(
      count: count,
    ),
    MyCounter(
      count: count,
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        setState(() {
          count++;
        });
      },
    )
  ],
)

通过 Widget 构造函数传递应用的共享数据,可以让任何阅读代码的人清楚地知道存在共享依赖项。这是一种常见的称为 依赖注入 的设计模式,许多框架都利用它或提供工具使其更容易。

使用 InheritedWidget

#

手动将数据传递到 Widget 树中可能会冗长并导致不需要的样板代码,因此 Flutter 提供了 InheritedWidget,它提供了一种在父 Widget 中有效地托管数据的方法,以便子 Widget 可以访问它们而无需将它们存储为字段。

要使用 InheritedWidget,请扩展 InheritedWidget 类并使用 dependOnInheritedWidgetOfExactType 实现静态方法 of()。在构建方法中调用 of() 的 Widget 会创建一个由 Flutter 框架管理的依赖项,因此任何依赖于此 InheritedWidget 的 Widget 都会在该 Widget 使用新数据重新构建且 updateShouldNotify 返回 true 时重新构建。

dart
class MyState extends InheritedWidget {
  const MyState({
    super.key,
    required this.data,
    required super.child,
  });

  final String data;

  static MyState of(BuildContext context) {
    // 此方法查找最近的 `MyState` Widget 祖先。
    final result = context.dependOnInheritedWidgetOfExactType<MyState>();

    assert(result != null, '上下文未找到 MyState');

    return result!;
  }

  @override
  // 如果旧 Widget 的数据与此 Widget 的数据不同,此方法应返回 true。如果为 true,则任何通过调用 `of()` 依赖于此 Widget 的 Widget 都将被重新构建。
  bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}

接下来,从需要访问共享状态的 Widget 的 build() 方法中调用 of() 方法:

dart
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    var data = MyState.of(context).data;
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

使用回调

#

您可以通过公开回调来在值更改时通知其他 Widget。Flutter 提供了 ValueChanged 类型,它声明了一个带单个参数的函数回调:

dart
typedef ValueChanged<T> = void Function(T value);

通过在 Widget 的构造函数中公开 onChanged,您可以为使用此 Widget 的任何 Widget 提供一种方法,以便在您的 Widget 调用 onChanged 时做出响应。

dart
class MyCounter extends StatefulWidget {
  const MyCounter({super.key, required this.onChanged});

  final ValueChanged<int> onChanged;

  @override
  State<MyCounter> createState() => _MyCounterState();
}

例如,此 Widget 可能会处理 onPressed 回调,并使用 count 变量的最新内部状态调用 onChanged

dart
TextButton(
  onPressed: () {
    widget.onChanged(count++);
  },
),

深入探讨

#

有关在 Widget 之间共享状态的更多信息,请查看以下资源:

使用 Listenable

#

既然您已经选择了如何在应用中共享状态,那么如何在状态更改时更新 UI 呢?您如何以一种通知应用其他部分的方式更改共享状态呢?

Flutter 提供了一个名为 Listenable 的抽象类,它可以更新一个或多个侦听器。一些使用 Listenable 的有用方法是:

  • 使用 ChangeNotifier 并使用 ListenableBuilder 订阅它
  • 使用 ValueNotifierValueListenableBuilder

ChangeNotifier

#

要使用 ChangeNotifier,请创建一个扩展它的类,并在类需要通知其侦听器时调用 notifyListeners

dart
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

然后将其传递给 ListenableBuilder 以确保 builder 函数返回的子树在 ChangeNotifier 更新其侦听器时重新构建。

dart
Column(
  children: [
    ListenableBuilder(
      listenable: counterNotifier,
      builder: (context, child) {
        return Text('counter: ${counterNotifier.count}');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.increment();
      },
    ),
  ],
)

ValueNotifier

#

ValueNotifierChangeNotifier 的一个更简单的版本,它存储单个值。它实现了 ValueListenableListenable 接口,因此它与 ListenableBuilderValueListenableBuilder 等 Widget 兼容。要使用它,请使用初始值创建一个 ValueNotifier 实例:

dart
ValueNotifier<int> counterNotifier = ValueNotifier(0);

然后使用 value 字段读取或更新值,并通知任何侦听器值已更改。因为 ValueNotifier 扩展了 ChangeNotifier,所以它也是一个 Listenable,并且可以与 ListenableBuilder 一起使用。但是您也可以使用 ValueListenableBuilder,它在 builder 回调中提供值:

dart
Column(
  children: [
    ValueListenableBuilder(
      valueListenable: counterNotifier,
      builder: (context, child, value) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

深入研究

#

要了解有关 Listenable 对象的更多信息,请查看以下资源:

为您的应用程序架构使用 MVVM

#

既然我们已经了解了如何在应用中共享状态并在其状态更改时通知应用的其他部分,我们就准备好开始考虑如何组织应用中的状态对象了。

本节介绍如何实现一个适用于 Flutter 等反应式框架的设计模式,称为_模型-视图-视图模型 MVVM_。

定义模型

#

模型通常是一个执行低级任务的 Dart 类,例如发出 HTTP 请求、缓存数据或管理系统资源(例如插件)。模型通常不需要导入 Flutter 库。

例如,考虑一个使用 HTTP 客户端加载或更新计数器状态的模型:

dart
import 'package:http/http.dart';

class CounterData {
  CounterData(this.count);

  final int count;
}

class CounterModel {
  Future<CounterData> loadCountFromServer() async {
    final uri = Uri.parse('https://myfluttercounterapp.net/count');
    final response = await get(uri);

    if (response.statusCode != 200) {
      throw ('Failed to update resource');
    }

    return CounterData(int.parse(response.body));
  }

  Future<CounterData> updateCountOnServer(int newCount) async {
    // ...
  }
}

这个模型不使用任何 Flutter 原语,也不对它运行的平台做任何假设;它的唯一工作是使用其 HTTP 客户端获取或更新计数。这允许在单元测试中使用模拟或伪造来实现模型,并定义了应用程序低级组件和构建完整应用程序所需的高级 UI 组件之间的明确界限。

CounterData 类定义了数据的结构,是应用程序的真正“模型”。模型层通常负责应用程序所需的核心算法和数据结构。如果您对定义模型的其他方法感兴趣,例如使用不可变值类型,请查看 pub.dev 上的 freezedbuild_collection 等包。

定义 ViewModel

#

ViewModel视图 绑定到 模型。它保护模型不被视图直接访问,并确保数据流从模型的更改开始。数据流由 ViewModel 处理,它使用 notifyListeners 通知视图某些内容已更改。ViewModel 就像餐馆里的服务员,处理厨房(模型)和顾客(视图)之间的沟通。

dart
import 'package:flutter/foundation.dart';

class CounterViewModel extends ChangeNotifier {
  final CounterModel model;
  int? count;
  String? errorMessage;
  CounterViewModel(this.model);

  Future<void> init() async {
    try {
      count = (await model.loadCountFromServer()).count;
    } catch (e) {
      errorMessage = '无法初始化计数器';
    }
    notifyListeners();
  }

  Future<void> increment() async {
    var count = this.count;
    if (count == null) {
      throw('未初始化');
    }
    try {
      await model.updateCountOnServer(count + 1);
      count++;
    } catch(e) {
      errorMessage = '无法更新计数';
    }
    notifyListeners();
  }
}

请注意,ViewModel 在从模型接收到错误时存储 errorMessage。这保护了视图免受未处理的运行时错误的影响,这些错误可能导致崩溃。相反,视图可以使用 errorMessage 字段显示用户友好的错误消息。

定义视图

#

由于我们的 ViewModel 是一个 ChangeNotifier,任何具有对它的引用的 Widget 都可以使用 ListenableBuilderViewModel 通知其侦听器时重建其 Widget 树:

dart
ListenableBuilder(
  listenable: viewModel,
  builder: (context, child) {
    return Column(
      children: [
        if (viewModel.errorMessage != null)
          Text(
            '错误: ${viewModel.errorMessage}',
            style: Theme.of(context)
                .textTheme
                .labelSmall
                ?.apply(color: Colors.red),
          ),
        Text('Count: ${viewModel.count}'),
        TextButton(
          onPressed: () {
            viewModel.increment();
          },
          child: Text('Increment'),
        ),
      ],
    );
  },
)

此模式允许应用程序的业务逻辑与 UI 逻辑和模型层执行的低级操作分开。

了解有关状态管理的更多信息

#

本页面只是触及了状态管理的表面,因为有很多方法可以组织和管理 Flutter 应用程序的状态。如果您想了解更多信息,请查看以下资源:

反馈

#

由于本网站的此部分仍在不断发展中,我们欢迎您的反馈