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

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

  • 使用 StatefulWidget
  • 使用构造函数、InheritedWidget 和回调在 widget 之间共享状态
  • 使用 Listenable 在某些内容更改时通知其他 widget
  • 将 Model-View-ViewModel (MVVM) 用于应用架构

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

教程:状态管理。这展示了如何将 ChangeNotiferprovider package 结合使用。

本指南不使用 providerRiverpod 等第三方 package。相反,它只使用 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 之间共享状态

#

应用需要存储状态的一些场景包括以下方面

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

本节探讨了如何在应用中有效共享不同 widget 之间的状态。最常见的模式是

  • 使用 widget 构造函数(在其他框架中有时称为“prop drilling”)
  • 使用 InheritedWidget(或类似 API,例如 provider package)。
  • 使用回调通知父 widget 某些内容已更改

使用 widget 构造函数

#

由于 Dart 对象是按引用传递的,因此 widget 通常会在其构造函数中定义它们需要使用的对象。您传递给 widget 构造函数的任何状态都可以用于构建其界面

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()。在 build 方法中调用 of() 的 widget 会创建一个由 Flutter 框架管理的依赖项,因此当此 widget 使用新数据重新构建并且 updateShouldNotify 返回 true 时,任何依赖于此 InheritedWidget 的 widget 都会重建。

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 之间共享状态的更多信息,请查看以下资源

使用可监听对象

#

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

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

  • 使用 ChangeNotifier 并通过 ListenableBuilder 订阅它
  • ValueNotifierValueListenableBuilder 结合使用

ChangeNotifier

#

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

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

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

然后将其传递给 ListenableBuilder,以确保每当 ChangeNotifier 更新其监听器时,builder 函数返回的子树都会重新构建。

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, value, child) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

深入探讨

#

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

将 MVVM 用于应用架构

#

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

本节介绍了如何实现一种与 Flutter 等响应式框架配合良好的设计模式,称为 Model-View-ViewModelMVVM

定义 Model

#

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

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

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 {
    // ...
  }
}

此 model 不使用任何 Flutter 原语,也不对其运行的平台做任何假设;它的唯一工作是使用其 HTTP 客户端获取或更新计数。这使得 model 可以在单元测试中通过 Mock 或 Fake 实现,并定义了应用低级组件与构建完整应用所需的高级 UI 组件之间的清晰边界。

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

定义 ViewModel

#

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

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 {
    final currentCount = count;
    if (currentCount == null) {
      throw('Not initialized');
    }
    try {
      final incrementedCount = currentCount + 1;
      await model.updateCountOnServer(incrementedCount);
      count = incrementedCount;
    } catch(e) {
      errorMessage = 'Could not update count';
    }
    notifyListeners();
  }
}

请注意,当 ViewModel 从 Model 收到错误时,它会存储一个 errorMessage。这可以保护 View 免受未处理的运行时错误(可能导致崩溃)。相反,View 可以使用 errorMessage 字段显示用户友好的错误消息。

定义 View

#

由于我们的 ViewModel 是一个 ChangeNotifier,任何引用它的 widget 都可以使用 ListenableBuilder,以便在 ViewModel 通知其监听器时重建其 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'),
        ),
      ],
    );
  },
)

这种模式允许应用的业务逻辑与界面逻辑以及 Model 层执行的低级操作分离。

了解更多状态管理信息

#

本页仅触及了状态管理的皮毛,因为组织和管理 Flutter 应用状态的方法有很多种。如果您想了解更多信息,请查看以下资源

反馈

#

由于本网站的此部分正在不断完善中,我们欢迎您的反馈