flutter ChangeNotifier

5,956 阅读11分钟

Flutter 中的 State

基本术语中的 State 是应用程序当前实例的描述。为了实现动态页面和应用程序,我们根据需要一次又一次地重建 State 。 Flutter 非常高效,可以在给定时间处理多个 State 更新。但是,为了使用最少的资源,提供了完整的包部分来实现 Flutter 中的 State 管理。

State 是可以在构建 widget 时同步读取的信息,并且可能在 widget 的生命周期内发生变化。widget 实现者有责任使用 State.setState 确保在此类 State 更改时及时通知 State 。

Flutter 中 State 管理的传统解决方案是 setState,但是它有一些缺点,例如深度耦合、昂贵的重建等。还有其他提供简单有效的 State 管理的包,如 GetX、BLoC、RiverPod、Provider 等。

传统实现

import 'package:change_notifier_example/widgets.dart';
import 'package:flutter/material.dart';

class TraditionalSolution extends StatefulWidget {
  const TraditionalSolution({Key? key}) : super(key: key);

  @override
  State<TraditionalSolution> createState() => _TraditionalSolutionState();
}

class _TraditionalSolutionState extends State<TraditionalSolution> {
  // 1
  List<String> todoList = <String>[];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("Traditional Implementation"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 2
          todoList.add("Random Value");
          setState(() {});
        },
        child: const Icon(Icons.add),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          // 3
          return getListTile(todoList, index);
        },
        itemCount: todoList.length,
      ),
    );
  }
  import 'package:flutter/material.dart';

AppBar getAppBar(String title) {
  return AppBar(
    centerTitle: true,
    elevation: 0,
    title: Text(title),
  );
}

ListTile getListTile(List<String> items, int index) {
  return ListTile(
    leading: CircleAvatar(child: Text(index.toString())),
    contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
    title: Text(items[index]),
  );
}

}
  • 首先,我们在内部创建一个 List< String > 来存储我们的数据。请注意,Stateful Widget 包含列表的声明,现在它是该 Stateful Widget 的私有状态。现在如果我们想在这个页面之外使用这个列表,我们不能使用它,因为它绑定到 widget 的生命周期。
  • 在 FloatingActionButton 的 onPressed 内部,我们将另一个项目添加到待办事项列表中,并调用 setState 来更新当前状态。在这里,需要注意的是,我们不能将用户重定向到另一个页面来添加项目。这是因为一旦我们离开这个页面,待办事项列表就会被处理掉。
  • 在这里,我们只是在 ListView 中显示列表的内容。

这种类型的状态称为 Ephemeral State,它绑定到单个页面。虽然此代码能够添加和显示项目,但它不是动态的。它只有一页,不能跨页持久化/保存数据。而在大多数现实世界的应用程序中,我们将数据保留在屏幕上,以便提供各种功能,例如修改、删除、添加等。这种跨越两个页面的状态称为应用程序状态。

使用全局变量提升状态

正如我们刚刚看到的,我们的列表绑定到 Widget 的生命周期。为了以一种直接的方式解决这个问题,我们可以做的是将我们的 List 声明为一个全局变量并在其他各种屏幕上访问它。这个实现也有一些缺点,我们将在实现后研究:

import 'package:flutter/material.dart';

class TraditionalSolution extends StatefulWidget {
  const TraditionalSolution({Key? key}) : super(key: key);

  @override
  State<TraditionalSolution> createState() => _TraditionalSolutionState();
}

//? Global Variable
List<String> todoList = <String>[];

class _TraditionalSolutionState extends State<TraditionalSolution> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("Traditional Implementation"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 2
          Navigator.of(context)
              .push(MaterialPageRoute(
            builder: (context) => const AddPage(),
          ))
              .then((value) => setState(() {}));
        },
        child: const Icon(Icons.add),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              // 3
              Navigator.of(context)
                  .push(MaterialPageRoute(
                builder: (context) => ModifyPage(index: index),
              ))
                  .then((value) => setState(() {}));
            },
            child: getListTile(todoList, index),
          );
        },
        itemCount: todoList.length,
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: "Default Text");

    return Scaffold(
      appBar: getAppBar("Add ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          todoList.add(textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

class ModifyPage extends StatelessWidget {
  const ModifyPage({
    Key? key,
    required this.index,
  }) : super(key: key);
  final int index;

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: todoList[index]);

    return Scaffold(
      appBar: getAppBar("Update ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          todoList[index] = textEditingController.text;
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

AppBar getAppBar(String title) {
  return AppBar(
    centerTitle: true,
    elevation: 0,
    title: Text(title),
  );
}

ListTile getListTile(List<String> items, int index) {
  return ListTile(
    leading: CircleAvatar(child: Text(index.toString())),
    contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
    title: Text(items[index]),
  );
}
  • 将列表声明为全局变量,以便其生命周期不受 widget 生命周期的限制,并且可以从任何地方访问。
  • 导航到 AddPage 并且每当我们返回 HomePage 时都会调用 setState。
  • 导航到 ModifyPage,当我们返回时调用 setState 来更新 UI。

image.png

这个实现看起来是动态的,因为它有 3 个页面,并且在任何这些页面上进行更改时,这些更改也会反映在 HomePage 上。然而,这个实现有一些地方是错误的:

  • HomePage、AddPage 和 ModifyPage 之间的重度耦合。
  • 如果用户不添加或修改数据,我们仍然在 HomePage 上调用 setState。
  • UI 是动态的,但不是反应式的。
  • 这个实现就像是在 Flutter 框架下工作。在 Flutter 中,最好将所需的依赖项注入到 widget 树中。

Flutter 中的 ChangeNotifier

ChangeNotifier 是一个类,它在我们想要通知其更改时向其侦听器提供通知。这意味着可以订阅扩展 ChangeNotifier 的类,并在类发生更改时调用其 notifyListeners() 方法。此调用将通知所有附加的侦听器。 ChangeNotifier 是 Flutter 原生的,主要与 Provider 和 RiverPod 包一起使用。

ValueNotifier 是一个带有单个值的 ChangeNotifier,它会在其 value 属性更改时通知其侦听器。但是,它只包含一个值,当我们使用扩展 ChangeNotifier 的类时,我们可以在其中定义多个值。

class MyValueNotifier extends ValueNotifier<int>{
  // Only holds single value
  int getValue() => value;
}

class MyChangeNotifier extends ChangeNotifier{
  // Can hold multiple values
}

现在让我们为我们的 TODO 应用程序实现 ChangeNotifier:

import 'package:flutter/material.dart';
import 'dart:collection';

class ItemNotifier extends ChangeNotifier {
  final List<String> _items = <String>[];
  int _size = 0;

  List<String> getItems() => UnmodifiableListView(_items);
  int getSize() => _size;

  void add(String value) {
    _items.add(value);
    _size++;
    notifyListeners();
  }

  void delete(int index){
    _items.removeAt(index);
    _size--;
    notifyListeners();
  }

  void modify(int index, String data){
    _items[index] = data;
    notifyListeners();
  }
}

Flutter 中使用 ChangeNotifier 的方法

我们有几种方法可以在 Flutter 中使用我们的变更通知器。

  • 使用 .addListener 方法,因为 ChangeNotifier 是一种 Listenable。
  • 另一种方法是使用 AnimatedBuilder 使用 ChangeNotifier,因为它也需要一个 Listenable。
  • 最后,我们可以使用 ChangeNotifierProvider、Consumer 和 Provider 的组合。所有这些功能都是由 Provider 包提供给我们的。

第一种和第二种方法依赖于在全局范围内声明 ChangeNotifier,而第三种方法将依赖类注入到 Widget Tree 中,以便我们可以在任何地方访问它。

在开始实施之前,只是为了确保它不会变得混乱。请记住,我们有 3 个页面,HomePage、AddPage 和 ModifyPage。每个实现都将包含自己的 AddPage 和 ModifyPage。另外,请注意 getAppBar 和 getListTile 函数是在一个名为 widgets.dart 的类中声明的。这些函数是单独声明的,因为它们在所有实现中都是相同的。

# lib/widgets.dart

import 'package:flutter/material.dart';

AppBar getAppBar(String title) {
  return AppBar(
    centerTitle: true,
    elevation: 0,
    title: Text(title),
  );
}

ListTile getListTile(List<String> items, int index) {
  return ListTile(
    leading: CircleAvatar(child: Text(index.toString())),
    contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
    title: Text(items[index]),
  );
}

使用 .addListener((){})

class ListenChangeNotifier extends StatefulWidget {
  const ListenChangeNotifier({Key? key}) : super(key: key);

  @override
  State<ListenChangeNotifier> createState() =>
      _ListenChangeNotifierState();
}

ItemNotifier itemNotifier = ItemNotifier();

class _ListenChangeNotifierState extends State<ListenChangeNotifier> {
  @override
  void initState() {
    super.initState();
    // 2
    itemNotifier.addListener(() => mounted ? setState(() {}) : null);
  }

  @override
  void dispose() {
    // 3
    itemNotifier.removeListener(() {});
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("Default Change Notifier Example"),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context).push(
          MaterialPageRoute(builder: (context) => const AddPage()),
        ),
        child: const Icon(Icons.add),
      ),
      body: _getListView(),
    );
  }

  ListView _getListView() {
    return ListView.builder(
      itemCount: itemNotifier.getSize(),
      itemBuilder: (context, index) {
        return GestureDetector(
          onTap: () => Navigator.of(context).push(
            MaterialPageRoute(builder: (context) => ModifyPage(index: index)),
          ),
          child: getListTile(itemNotifier.getItems(), index),
        );
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: "Default Text");

    return Scaffold(
      appBar: getAppBar("Add ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          itemNotifier.add(textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

class ModifyPage extends StatelessWidget {
  const ModifyPage({
    Key? key,
    required this.index,
  }) : super(key: key);
  final int index;

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: itemNotifier.getItems()[index]);

    return Scaffold(
      appBar: getAppBar("Update ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          itemNotifier.modify(index, textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

这种方法还将 ItemNotifier 声明为全局变量。但是,请注意,我们仅通过向 ItemNotifier 添加侦听器来侦听有效事件。当我们手动添加侦听器时,必须在不需要时将其丢弃。但是,在这种方法中,我们没有将依赖项注入到 widget 树中,它与全局范围内的 widget 树不同。

使用 AnimatedBuilder 消费

正如我们所讨论的,AnimatedBuilder 接受任何 Listenable 作为参数。 ChangeNotifier 类也扩展了 Listenable,因此我们可以简单地使用 AnimatedBuilder。

import 'package:flutter/material.dart';
import 'dart:collection';

class ItemNotifier extends ChangeNotifier {
  final List<String> _items = <String>[];
  int _size = 0;

  List<String> getItems() => UnmodifiableListView(_items);
  int getSize() => _size;

  void add(String value) {
    _items.add(value);
    _size++;
    notifyListeners();
  }

  void delete(int index){
    _items.removeAt(index);
    _size--;
    notifyListeners();
  }

  void modify(int index, String data){
    _items[index] = data;
    notifyListeners();
  }
}

class AnimatedChangeNotifierExample extends StatefulWidget {
  const AnimatedChangeNotifierExample({Key? key}) : super(key: key);

  @override
  State<AnimatedChangeNotifierExample> createState() =>
      _AnimatedChangeNotifierExampleState();
}

// 1
ItemNotifier itemNotifier = ItemNotifier();

class _AnimatedChangeNotifierExampleState
    extends State<AnimatedChangeNotifierExample> {
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      // 2
      animation: itemNotifier,
      builder: (context, child) {
        return Scaffold(
          appBar: getAppBar("Animated Change Notifier Example"),
          floatingActionButton: FloatingActionButton(
            onPressed: () => itemNotifier.add("Random Name"),
            child: const Icon(Icons.add),
          ),
          body: ListView.builder(
            itemBuilder: (context, index) {
              return getListTile(itemNotifier.getItems(), index);
            },
            itemCount: itemNotifier.getSize(),
          ),
        );
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: "Default Text");

    return Scaffold(
      appBar: getAppBar("Add ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          itemNotifier.add(textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

class ModifyPage extends StatelessWidget {
  const ModifyPage({
    Key? key,
    required this.index,
  }) : super(key: key);
  final int index;

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
    TextEditingController(text: itemNotifier.getItems()[index]);

    return Scaffold(
      appBar: getAppBar("Update ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          itemNotifier.modify(index, textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

AppBar getAppBar(String title) {
  return AppBar(
    centerTitle: true,
    elevation: 0,
    title: Text(title),
  );
}

ListTile getListTile(List<String> items, int index) {
  return ListTile(
    leading: CircleAvatar(child: Text(index.toString())),
    contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
    title: Text(items[index]),
  );
}

这里我们也声明了全局的 ItemNotifier,它类似于监听,但是这里我们不必手动调用setState。 AnimatedBuilder 在内部管理订阅,因此我们不需要在这里处理 ItemNotifier 监听器。

我们讨论的两种方法都没有将依赖注入到 widget 树中,依赖存在于全局范围内。现在我们来看看 Flutter 中的 ChangeNotifierProvider 和 Provider。它们提供强大的依赖注入,使得依赖在整个 widget 树中都可用。

image.png

Flutter 中的 ChangeNotifierProvider

ChangeNotifierProvider 旨在为 Flutter 中的 ChangeNotifier 提供更精细的用例。像 AnimatedBuilder、StreamBuilder、FutureBuilder、ValueListenableBuilder 等其他构建器一样。它就像一个接受 ChangeNotifier 并更新其任何值更改的子级的构建器。

要在 Flutter 中使用 ChangeNotifierProvider,我们需要将 Provider 依赖添加到我们的 pubspec.yaml 中。

添加最新的 Provider 依赖:

 provider: ^6.0.2

在我们的例子中,ChangeNotifierProvider< T > 包裹在 MaterialApp 周围。请不要使用下层 Widget Tree 中不需要的依赖项污染范围。在我们的例子中,我们只有 3 个页面,并且都需要访问相同的依赖项,因此为了简单起见,我们将其包装在 MaterialApp 中。

依赖注入

ChangeNotifierProvider(
  create: (context) => ItemNotifier(),
  child: MaterialApp(...),
);

正如看到的那样,它也遵循单例模式,实例在 create 方法中创建一次,并且可供底层 widget 使用。 ChangeNotifierProvider 是 Flutter 中 InheritedWidgets 的简化版。在这里,我们已经将 ItemNotifier 注入到 Widget Tree 中,现在我们可以在我们的应用程序中访问它。

我们还可以使用 MultiProvider 将多个依赖项注入到 Widget Tree 中:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (context) => ItemNotifier()),
    Provider(create: (context) => SomeOtherClass()),
  ],
  child: const MyApp(),
);

请注意,ChangeNotifierProvider 接受一个 ChangeNotifier 类,而 Provider 接受一个简单的类。 Provider 用于我们想要共享值但不想更新侦听器的情况。

访问我们的依赖

有两种方法可以访问我们的依赖项,一种是使用 Consumer< T > 小部件,另一种是使用 Provider.of< T >(context)。

  1. 使用 Consumer< T > 当我们想要在值更改时重建 widget 时,我们使用 Consumer< T >。必须提供类型 < T > 以便 Provider 可以理解您所指的依赖项。
Consumer<ItemNotifier>(
  builder: (context, value, child) {...},
  child: SomeWidget(),
);

Consumer widget 有两个参数,builder 参数是必需的,child 参数是可选的。 child 参数是不受 ChangeNotifier 中任何更改影响的任何昂贵的 widget 。

  1. 使用 Provider.of< T >(context) 当您需要访问依赖项但不想对用户界面进行任何更改时,将使用 Provider.of< T >(context)。我们可以使用 Consumer< T > ,但这会浪费资源。我们只是将监听设置为 false,表示我们不需要监听来自 ChangeNotifier 的更新。
Provider.of<ItemNotifier>(context, listen: false).delete(0);

我们在需要更新 UI 的地方使用 Consumer。 Provider.of(context) 用于我们不需要任何关于所做更改的进一步通知的地方,因此我们将 listen 参数设置为 false 并使用 ItemNotifier 类中提供的函数。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:change_notifier_example/widgets.dart';
import 'data/item_notifier.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => ItemNotifier(),
      child: MaterialApp(
        home: const ChangeNotifierExample(),
      ),
    );
  }
}

class ChangeNotifierExample extends StatefulWidget {
  const ChangeNotifierExample({Key? key}) : super(key: key);

  @override
  State<ChangeNotifierExample> createState() => _ChangeNotifierExampleState();
}

class _ChangeNotifierExampleState extends State<ChangeNotifierExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("ChangeNotifier Builder"),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context).push(
          MaterialPageRoute(builder: (context) => const AddPage()),
        ),
        child: const Icon(Icons.add),
      ),
      body: Consumer<ItemNotifier>(builder: (context, value, child) {
        return ListView.builder(
          itemBuilder: (context, index) {
            return GestureDetector(
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute(
                    builder: (context) => ModifyPage(index: index)),
              ),
              child: getListTile(value.getItems(), index),
            );
          },
          itemCount: value.getSize(),
        );
      }),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController =
        TextEditingController(text: "Default Text");

    return Scaffold(
      appBar: getAppBar("Add ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<ItemNotifier>(context, listen: false)
              .add(textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

class ModifyPage extends StatelessWidget {
  const ModifyPage({
    Key? key,
    required this.index,
  }) : super(key: key);
  final int index;

  @override
  Widget build(BuildContext context) {
    TextEditingController textEditingController = TextEditingController(
      text: Provider.of<ItemNotifier>(context, listen: false).getItems()[index],
    );

    return Scaffold(
      appBar: getAppBar("Update ToDoItem"),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<ItemNotifier>(context, listen: false)
              .modify(index, textEditingController.text);
          Navigator.pop(context);
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15.0),
          child: TextField(
            controller: textEditingController,
          ),
        ),
      ),
    );
  }
}

结论

在这篇文章中,我们了解了 ChangeNotifier 和 ChangeNotifierProvider 的基础知识。我们讨论了如何在 Flutter 中创建和使用 ChangeNotifier。 ChangeNotifer 是 Flutter 的原生功能,即您无需添加任何依赖项即可使用它,但它通常与 Provider 一起使用以提供高级功能。