跳至主要内容

状态管理

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 共享状态。最常见的模式是

  • 使用 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) {
    // This method looks for the nearest `MyState` widget ancestor.
    final result = context.dependOnInheritedWidgetOfExactType<MyState>();

    assert(result != null, 'No MyState found in context');

    return result!;
  }

  @override
  // This method should return true if the old widget's data is different
  // from this widget's data. If true, any widgets that depend on this widget
  // by calling `of()` will be re-built.
  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 之间共享状态的更多信息,请查看以下资源

使用可监听对象

#

现在您已经选择了如何在应用中共享状态,那么当状态发生变化时如何更新 UI?如何以通知应用其他部分的方式更改共享状态?

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

  • 使用 ChangeNotifier 并使用 ListenableBuilder 订阅它
  • 使用带有 ValueListenableBuilderValueNotifier

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 = 'Could not initialize counter';
    }
    notifyListeners();
  }

  Future<void> increment() async {
    var count = this.count;
    if (count == null) {
      throw('Not initialized');
    }
    try {
      await model.updateCountOnServer(count + 1);
      count++;
    } catch(e) {
      errorMessage = 'Count not update count';
    }
    notifyListeners();
  }
}

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

定义视图

#

由于我们的 ViewModel 是一个 ChangeNotifier,因此任何引用它的 Widget 都可以使用 ListenableBuilderViewModel 通知其监听器时重新构建其 Widget 树

dart
ListenableBuilder(
  listenable: viewModel,
  builder: (context, child) {
    return Column(
      children: [
        if (viewModel.errorMessage != null)
          Text(
            'Error: ${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 应用的状态。如果您想了解更多信息,请查看以下资源

反馈

#

由于网站的这一部分正在不断发展,我们 欢迎您的反馈