Flutter的这些状态管理,先说说StreamBuilder和ChangeNotifierProvider

1,615 阅读4分钟

在Flutter中,状态管理是一个核心主题,因为它直接影响到应用的性能和开发的复杂性。

常见的状态管理(更细界面)

  1. 使用setState()方法

    • setState()是在StatefulWidget中用来更新界面的基本方式。当你调用setState()时,Flutter会标记这个widget为"dirty"(脏的),并且在下一帧时重建它。适用于局部状态管理,即状态仅在单个Widget内部使用。
  2. 使用ValueNotifierValueListenableBuilder

    • ValueNotifier是一个简单的状态管理工具,它可以保存任何类型的值。当值改变时,所有使用ValueListenableBuilder的widget都会更新。
  3. 使用StreamBuilder

    • 如果你的数据是基于流(Stream)的,可以使用StreamBuilder。它会监听流的变化,并且根据流的最新数据重建界面。
  4. 使用FutureBuilder

    • 对于基于Future的异步操作,可以使用FutureBuilder来构建UI。它会在Future完成时更新UI。
  5. 使用更复杂的状态管理解决方案

ProviderBlocMobX等,这些都是为了更大规模的应用提供的更高级的状态管理框架。

  • 1、 Provider: 这是最受推荐的一种轻量级状态管理方式。Provider不仅可以帮助你管理跨多个屏幕或组件的共享状态,还提供了一个相对简单的实现方式。ChangeNotifierProvider是Provider包中的一个组件,用于将ChangeNotifier类与Flutter UI绑定。

  • Bloc/Cubit: Bloc是一种较为复杂的状态管理库,基于Reactive Programming(响应式编程),主要用于更复杂的应用中。它通过分离业务逻辑和视图层来帮助实现更可维护的代码结构。

  • Riverpod: 由Provider的作者开发,可以看作是Provider的改进版。Riverpod提供了更灵活的依赖重写机制,更好的性能和更少的限制。

  • Redux: 受到Web开发中JavaScript的Redux库的启发,Flutter的Redux库用于管理应用的所有状态。它使用单一的全局状态存储,通过派发actions和reducers来更新状态。

  • GetX: 是一个轻量级且功能丰富的库,用于状态管理、依赖注入和路由管理。GetX易于学习,并且提供了非常简洁的API来处理状态管理。

  • MobX: 另一个响应式状态管理库,通过actions、reactions和observables的概念来管理状态。MobX的核心思想是自动追踪状态变化,并触发相应的响应。


说说 ChangeNotifierProvider

使用ChangeNotifierProvider的步骤

  1. 定义ChangeNotifier类:首先,你需要定义一个类,继承自ChangeNotifier。这个类将持有你想要在应用中共享的数据,并在数据发生变化时通知其监听者。
  2. 创建Provider:在应用的顶层或需要的层级,使用ChangeNotifierProvider来创建你的ChangeNotifier类的实例,并使其下的widget树能够访问到这个实例。
  3. 使用数据:在widget中,你可以使用Provider.of<T>(context)Consumer<T> widget来读取和响应数据变化。

示例代码

下面是一个简单的计数器应用示例,展示了如何使用ChangeNotifierProvider来实现状态管理。


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

// 步骤1: 定义一个继承自ChangeNotifier的Counter类
class Counter extends ChangeNotifier {
  int _count = 0;  // 私有变量,存储计数值

  int get count => _count;  // 公开的getter方法,用于读取计数值

  void increment() {
    _count++;          // 计数值加1
    notifyListeners(); // 通知监听器数据已更新,触发界面重建
  }
}

void main() {
  runApp(ProviderApp());
}

// 步骤2: 使用ChangeNotifierProvider在应用顶层提供Counter实例
class ProviderApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  }
}

// 主页面
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 步骤3: 使用Provider.of来访问Counter实例
    final counter = Provider.of<Counter>(context, listen: true);

    return Scaffold(
      appBar: AppBar(title: Text("Provider Example")),
      body: Center(
        child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counter.increment, // 调用Counter中的increment方法
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

解析

  • Counter类:这个类继承自ChangeNotifier,管理计数器的状态。当调用increment()方法时,计数器的值会增加,并通过notifyListeners()通知依赖这个状态的widget进行重建。
  • ProviderApp类:这是一个包装了ChangeNotifierProvider的widget,它在应用的顶层提供了Counter类的实例。所有子widget都可以访问到这个实例。
  • HomePage类:这个类使用Provider.of<Counter>(context)来获取Counter实例,并在界面中显示计数值。FloatingActionButton被点击时会触发increment()方法,从而更新界面上显示的计数值。

说说 MultiProvider

在实际开发中,一个应用通常需要管理多个状态对象。为了避免多个 Provider 嵌套过深导致代码臃肿,Flutter 提供了 MultiProvider,它可以在同一个 widget 树上一次性注入多个状态管理对象,从而使代码结构更加清晰易维护。

MultiProvider 的主要作用
  • 简化代码结构:使用 MultiProvider 可以避免嵌套多个 ChangeNotifierProvider 或其他 Provider,从而减少层级,使得代码更易读。
  • 集中管理:将所有需要注入的状态对象集中在一个地方进行管理,便于维护和扩展。
  • 灵活组合:除了 ChangeNotifierProvider 外,MultiProvider 同时支持其他 Provider 类型,如 StreamProviderFutureProvider 等。
使用步骤
  1. 定义多个状态管理类
    如需要管理计数和消息等状态,可以分别定义继承自 ChangeNotifier 的类。
  2. 使用 MultiProvider 注入状态
    在应用的顶层或某个页面中,使用 MultiProvider 一次性注入多个 Provider。
  3. 在子 widget 中访问状态
    使用 Provider.of<T>(context)Consumer<T> 或其他方式,在 widget 树中随时获取并响应状态变化。
示例代码

下面是一个简单的示例,展示如何使用 MultiProvider 同时管理两个状态:一个计数器和一个消息状态。

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

// 定义第一个状态管理类:计数器
class Counter extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

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

// 定义第二个状态管理类:消息
class MessageNotifier extends ChangeNotifier {
  String _message = "Hello";
  String get message => _message;

  void updateMessage(String newMessage) {
    _message = newMessage;
    notifyListeners();
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // 提供计数器状态
        ChangeNotifierProvider(create: (_) => Counter()),
        // 提供消息状态
        ChangeNotifierProvider(create: (_) => MessageNotifier()),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

// 示例页面,分别读取计数器和消息状态
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<Counter>(context);
    final messageNotifier = Provider.of<MessageNotifier>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text("MultiProvider 示例"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('计数: ${counter.count}', style: TextStyle(fontSize: 24)),
            SizedBox(height: 16),
            Text('消息: ${messageNotifier.message}', style: TextStyle(fontSize: 24)),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'btn1',
            onPressed: counter.increment,
            tooltip: '增加计数',
            child: Icon(Icons.add),
          ),
          SizedBox(height: 16),
          FloatingActionButton(
            heroTag: 'btn2',
            onPressed: () => messageNotifier.updateMessage("Hello Provider"),
            tooltip: '更新消息',
            child: Icon(Icons.message),
          ),
        ],
      ),
    );
  }
}

.

以上代码,可以换一种写法

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

// 定义第一个状态管理类:计数器
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners();
  }
}

// 定义第二个状态管理类:消息
class MessageNotifier extends ChangeNotifier {
  String _message = "Hello Provider";
  String get message => _message;
  
  void updateMessage(String newMsg) {
    _message = newMsg;
    notifyListeners();
  }
}

// 这里提前创建好状态对象
final counterNotifier = CounterNotifier();
final messageNotifier = MessageNotifier();

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // 使用 .value 提供已存在的 counterNotifier 实例
        ChangeNotifierProvider<CounterNotifier>.value(
          value: counterNotifier,
        ),
        // 使用 .value 提供已存在的 messageNotifier 实例
        ChangeNotifierProvider<MessageNotifier>.value(
          value: messageNotifier,
        ),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取状态对象
    final counter = Provider.of<CounterNotifier>(context);
    final message = Provider.of<MessageNotifier>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text("MultiProvider with .value 示例"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              "Counter: ${counter.count}",
              style: TextStyle(fontSize: 24),
            ),
            SizedBox(height: 20),
            Text(
              "Message: ${message.message}",
              style: TextStyle(fontSize: 24),
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: "counterBtn",
            onPressed: counter.increment,
            tooltip: "Increment",
            child: Icon(Icons.add),
          ),
          SizedBox(height: 20),
          FloatingActionButton(
            heroTag: "messageBtn",
            onPressed: () => message.updateMessage("Updated Message"),
            tooltip: "Update Message",
            child: Icon(Icons.message),
          ),
        ],
      ),
    );
  }
}

提前创建的状态对象 在示例中,我们先通过全局变量创建了 counterNotifier 和 messageNotifier 两个状态对象,这样在整个应用中这两个对象都是唯一且共享的。

使用 .value 构造函数 在 MultiProvider 中,我们采用 ChangeNotifierProvider.value 来传入已经存在的实例。这样做能确保当 widget 重建时,不会意外销毁或重新创建状态对象。 集中管理多个状态

通过 MultiProvider,你可以在同一个入口处一次性注入多个状态对象,这样在子 widget 中就能方便地通过 Provider.of(context) 或 Consumer 来获取对应的状态,而不必在多个层级中嵌套 Provider。

这种方式适合用于状态对象已经由外部创建、管理其生命周期的场景,可以更灵活地共享状态并避免不必要的重复创建。

解析
  • 状态类定义

    • Counter 类管理计数器状态,每次调用 increment() 时,计数值增加并调用 notifyListeners() 通知依赖者更新。
    • MessageNotifier 类则管理一个简单的文本消息,通过 updateMessage() 方法更新消息内容。
  • MultiProvider 的使用
    MyAppbuild 方法中,我们使用 MultiProvider 同时注入了 CounterMessageNotifier。这样一来,应用中所有子 widget 都可以通过 Provider.of<T>(context)Consumer<T> 来获取这两个状态。

  • 子 widget 中的数据消费
    HomeScreen 中,我们分别获取了 CounterMessageNotifier 的实例,并根据其状态展示计数和消息,同时提供两个按钮分别更新这两个状态。

通过这种方式,你可以方便地管理和组合多个状态对象,使得应用状态管理更加模块化和可维护。 MultiProvider 在构建大型应用时尤为实用,因为它能帮助你避免嵌套过深,同时提高代码的可读性和扩展性。


说说 StreamController

在Flutter中,StreamController 是一种管理事件流和数据流的核心工具,特别适用于实现应用的响应式编程。它提供了一种方式来异步处理数据,并且在应用的不同部分之间传递信息,这在动态UI更新和复杂事件处理中非常有用。

基本概念

StreamController 允许你创建一个Stream,你可以向这个Stream发送数据,而Stream的订阅者则可以接收并处理这些数据。StreamController 通常包括以下几个主要部分:

  • Stream:一个数据流,订阅者可以监听这个流中的数据。
  • Sink:用于向流中添加数据的接口。
  • builder: 这是一个函数,它根据 stream 的最新数据动态构建widget。它接收的参数是 BuildContextAsyncSnapshot,后者持有最新的数据和流的状态。

创建和使用 StreamController

在Flutter中,你通常会这样使用StreamController

  1. 创建 StreamController:你可以创建一个标准的或广播类型的StreamController。标准的StreamController只允许单个监听者,而广播类型的可以有多个监听者。

    StreamController<int> controller = StreamController<int>();
    

    如果需要多个订阅者,你应该使用广播类型:

    StreamController<int> controller = StreamController<int>.broadcast();
    
  2. 向 Stream 添加数据:使用Sink接口,通常是controller.sink.add(value)方法来向流中添加数据。

    controller.sink.add(10);
    
  3. 监听数据:使用Stream.listen方法来订阅数据流,并定义处理数据的方式。

    controller.stream.listen((data) {
      print("Received: $data");
    });
    
  4. 关闭 StreamController:当完成流的使用后,你应该关闭StreamController以释放资源。

    controller.close();
    

来点代码吧

常见的一个例子

import 'dart:async';

class CounterController {
  final StreamController<int> _controller = StreamController<int>();
  int _counter = 0;

  // 外部订阅的 Stream
  Stream<int> get stream => _controller.stream;

  // 递增计数器并添加数据到流
  void incrementCounter() {
    _counter++;
    _controller.sink.add(_counter); // 发送新的计数值到流
  }

  // 关闭 StreamController 防止内存泄漏
  void dispose() {
    _controller.close();
  }
}
import 'package:flutter/material.dart';
import 'counter_controller.dart'; // 确保正确导入控制器

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final CounterController _controller = CounterController();

  @override
  void dispose() {
    _controller.dispose(); // 适当时机关闭控制器
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream Counter Example')),
      body: Center(
        child: StreamBuilder<int>(
          stream: _controller.stream,
          initialData: 0,
          builder: (context, snapshot) {
            // 检查是否有数据
            if (snapshot.hasData) {
              return Text('Button tapped ${snapshot.data} times',
                style: TextStyle(fontSize: 20));
            }
            return CircularProgressIndicator();
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _controller.incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}


如果是一个stream的变化,page多个widget都需要更新?

假设我们想在页面上添加两个文本:一个显示点击次数,另一个显示点击次数的平方值。这两个文本都会响应同一个计数器值的变化。

如果多个widget需要根据同一个Stream的变化进行更新,你可以使用多个StreamBuilder来监听相同的Stream。这种方式非常灵活,因为你可以在不同的部分UI中分别处理数据,从而让每个部分根据Stream的当前状态独立更新。这样做的好处是,它允许你为每个StreamBuilder提供不同的builder逻辑,可以根据具体的UI需求定制展示方式。

import 'dart:async';

class CounterController {
  final StreamController<int> _controller = StreamController<int>();
  int _counter = 0;

  // 外部订阅的 Stream
  Stream<int> get stream => _controller.stream;

  // 递增计数器并添加数据到流
  void incrementCounter() {
    _counter++;
    _controller.sink.add(_counter); // 发送新的计数值到流
  }

  // 关闭 StreamController 防止内存泄漏
  void dispose() {
    _controller.close();
  }
}


import 'package:flutter/material.dart';
import 'counter_controller.dart'; // 确保正确导入控制器

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final CounterController _controller = CounterController();

  @override
  void dispose() {
    _controller.dispose(); // 适当时机关闭控制器
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream Counter Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            StreamBuilder<int>(
              stream: _controller.stream,
              initialData: 0,
              builder: (context, snapshot) {
                // 检查是否有数据
                if (snapshot.hasData) {
                  return Text('Button tapped ${snapshot.data} times',
                    style: TextStyle(fontSize: 20));
                }
                return CircularProgressIndicator();
              },
            ),
            StreamBuilder<int>(
              stream: _controller.stream,
              initialData: 0,
              builder: (context, snapshot) {
                // 检查是否有数据
                if (snapshot.hasData) {
                  int squared = snapshot.data! * snapshot.data!;
                  return Text('Square of taps: $squared',
                    style: TextStyle(fontSize: 20));
                }
                return CircularProgressIndicator();
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _controller.incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

我们使用了两个StreamBuilder来响应同一个Stream的数据更新,每个StreamBuilder处理数据的方式不同。这种方法让你能够构建更复杂的响应式UI,而且每个组件可以根据相同数据的不同视图进行更新。