阅读视图

发现新文章,点击刷新页面。

Flutter的状态管理工具

一、Provider

1.原理

Provider 本质上是基于 Flutter 的InheritedWidget 实现的,核心思想是数据自上而下传递,形成一个「数据提供者 - 消费者」的树形结构。

2、使用示例

2.1 定义可监听的状态模型(继承 ChangeNotifier) 核心:数据变化时调用 notifyListeners() 通知组件刷新


class LoginStatusModel extends ChangeNotifier {
  bool _isLogin = false;

  bool get isLogin => _isLogin;

  void updateLoginStatus(bool isLogin) {
    _isLogin = isLogin;
    notifyListeners();  // 关键:通知所有订阅的组件刷新
  }
}

2.2 使用 Provider或其子类,包裹 App实例,并将 状态模型实例作为值传递

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => LoginStatusModel(), 
      child: const MyApp()、
    ),
  );
}

2.3 使用状态数据,在需要监听数据变化的Widget中,使用Provider.of、Consumer获取数据:

  @override
  Widget build(BuildContext context) {
    // 使用 Consumer 监听 CounterModel
    return Consumer<LoginStatusModel>(
      builder: (context, loginStatus, child) {
        return Text('${loginStatus.isLogin}');
      },
    );
  }
}

// 也可以使用  Provider.of() 来获取:
Text('${Provider.of<LoginStatusModel>(context, listen: false).isLogin}')
特性 Provider.of<T>(context) Consumer<T> context.watch<T>() (推荐)
主要用途 灵活获取,常用于非 build 方法中 build 中获取并直接构建子 Widget build 中获取数据用于逻辑判断或属性赋值
是否监听变化 取决于 listen 参数 (默认 true) 是 (自动监听) 是 (自动监听)
代码位置 任意位置 (build 内/外,异步方法中) 只能在 build 方法的 return 树中 只能在 build 方法体内 (return 之前)
是否需要 builder 不需要 需要 (builder 回调) 不需要
典型场景 按钮点击事件、定时器、初始化逻辑 需要根据数据动态生成整个 Widget 时 需要根据数据决定 Widget 的属性 (颜色、文本) 时
性能优化 可设置 listen: false 避免不必要重绘 仅重建 Consumer 及其子节点 重建当前 Widget

A. Provider.of<T>(context)

这是最原始的方法。它的关键在于第二个参数 listen

  • listen: true (默认)

    • 行为:监听数据变化。如果数据变了,当前 Widget 会重建
    • 限制:只能在 build 方法中使用(因为重建需要触发 build)。
    • 缺点:如果在 build 中用默认值,会导致整个父 Widget 重绘,不够精细。
  • listen: false (常用)

    • 行为:不监听数据变化。只获取当前的实例对象。
    • 场景:在事件回调(如 onPressed)、initState、或者异步方法中调用修改数据的方法(如 increment())。
    • 优势:不会因为数据变化导致当前 Widget 无谓重绘。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

B. Consumer 是一个 Widget。它的作用是将“获取数据”和“构建 UI”合二为一。

  • 特点:它提供了一个 builder 函数。只有当数据变化时,只有这个 Consumer 节点及其子节点会重绘,它的父兄弟节点不会重绘。
  • 场景:当你需要根据数据直接返回一个新的 Widget 结构时。

// ✅ 场景:只想让这段文字区域刷新,不影响周围的布局

Consumer<CounterModel>(
  builder: (context, counter, child) {
    // counter 就是 CounterModel 实例
    return Text(
      '当前计数: ${counter.count}',
      style: TextStyle(fontSize: 24, color: Colors.blue),
    );
  },
  // child 参数可用于优化:传递不变的子组件,避免每次重绘都重建它
  // child: Icon(Icons.star), 
)

C. context.watch<T>() —— Consumer 的语法糖 (现代推荐)

这是 provider 6.0+ 版本后最推荐的写法。它等价于 Provider.of<T>(context, listen: true),但写法更简洁。

  • 特点:直接在 build 方法体中使用,返回数据对象。
  • 场景:当你需要在 build 方法中获取数据,用来计算属性、做条件判断,或者组合多个数据源时。
  • 注意:调用 watch 的代码所在的 整个 Widget 的 build 方法 会在数据变化时重跑。如果该 Widget 很大,可能不如 Consumer 精准。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

总结使用口诀

  • 改数据 (按钮/事件) ➡️ 用 read (或 of(..., listen: false))
  • 显数据 (局部刷新) ➡️ 用 Consumer
  • 显数据 (简单逻辑) ➡️ 用 watch
  • 初始化 (生命周期) ➡️ 用 of(..., listen: false)

二、RiverPod

1.原理

简洁表达:

  • 中心化管理:通过 ProviderContainerProviderScope)统一管理所有状态,状态封装在 Provider 中,脱离 Widget 上下文;

  • 精准订阅分发:基于 Ref 实现 Widget/Provider 对状态的订阅,状态变化时仅通知订阅者,最小化重建;

  • 无上下文 + 类型安全:解决了传统 Provider 的核心痛点,同时通过静态类型检查提升开发效率。

Ref (通常通过 WidgetRef 在 UI 中使用) 是整个状态管理系统的核心控制器上下文对象,是widget和provider,Provider和Provider沟通的唯一桥梁。

2、使用示例

  • 必须使用 ProviderScope 包裹整个应用。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    const ProviderScope( // 👈 必须包裹这里
      child: MyApp(),
    ),
  );
}
2.1 简单状态:@riverpod (替代 StateProvider)
// counter_provider.dart 文件,供后续订阅分发使用
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 使用 @riverpod 注解,运行 build_runner 后会自动生成 CounterProvider
@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    // 初始值
    return 0;
  }

  // 定义修改状态的方法
  void increment() {
    state++; // 👈 直接修改 state 属性,自动通知监听者
  }

  void reset() {
    state = 0;
  }
}

2.2 复杂状态:AsyncNotifier (替代 FutureProvider + StateNotifier)

用于处理异步操作(如网络请求)并管理复杂状态。这是 Riverpod 最强大的部分。

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 模拟用户模型
class User {
  final String name;
  final int age;
  User({required this.name, required this.age});
}

// 定义 AsyncNotifier
@riverpod
class CurrentUser extends _$CurrentUser {
  @override
  Future<User> build() async {
    // 模拟网络延迟
    await Future.delayed(const Duration(seconds: 2));
    
    // 模拟可能发生的错误
    // if (someCondition) throw Exception("Failed to load");

    return User(name: "Alice", age: 25);
  }

  // 修改用户信息的方法
  Future<void> updateAge(int newAge) async {
    state = const AsyncValue.loading(); // 手动设置加载状态
    
    try {
      await Future.delayed(const Duration(seconds: 1)); // 模拟 API 调用
      final user = state.value!; // 获取旧数据
      state = AsyncValue.data(User(name: user.name, age: newAge)); // 更新数据
    } catch (e, st) {
      state = AsyncValue.error(e, st); // 处理错误
    }
  }
}
2.3 组合状态:派生数据 (Derived State)

在一个 Provider 中读取另一个 Provider,实现数据联动。

@riverpod
String userNameRef(UserNameRef ref) {
  // 监听 CurrentUser Provider
  final userAsync = ref.watch(currentUserProvider);

  // 处理异步状态
  return userAsync.when(
    data: (user) => user.name,
    loading: () => "加载中...",
    error: (_, __) => "加载失败",
  );
}
A. 使用 ConsumerWidget (推荐)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 继承 ConsumerWidget
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. 监听简单状态 (Counter)
    // ref.watch 会自动订阅,数据变化时重建此 Widget
    final count = ref.watch(counterProvider); 

    // 2. 监听异步状态 (CurrentUser)
    final userAsync = ref.watch(currentUserProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Riverpod Demo")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 显示异步用户数据
            userAsync.when(
              data: (user) => Text("你好, ${user.name} (年龄: ${user.age})"),
              loading: () => const CircularProgressIndicator(),
              error: (err, stack) => Text("错误: $err"),
            ),
            
            const SizedBox(height: 20),

            // 显示计数
            Text("计数: $count", style: const TextStyle(fontSize: 24)),
            
            const SizedBox(height: 20),

            // 3. 修改状态 (使用 ref.read 或 ref.notifier)
            ElevatedButton(
              onPressed: () {
                // 方式 A: 直接调用生成的 notifier 方法 (推荐)
                ref.read(counterProvider.notifier).increment();
                
                // 方式 B: 如果是 AsyncNotifier
                // ref.read(currentUserProvider.notifier).updateAge(26);
              },
              child: const Text("增加计数"),
            ),
            
            ElevatedButton(
              onPressed: () {
                // 触发异步更新
                ref.read(currentUserProvider.notifier).updateAge(30);
              },
              child: const Text("更新用户年龄 (异步)"),
            ),
          ],
        ),
      ),
    );
  }
}

B. 在非 Widget 类中使用 (Riverpod 的杀手锏)

由于不依赖 Context,你可以在任何地方(如路由守卫、服务类、甚至 main 函数之后)访问状态。

// 例如:在一个普通的 Dart 类中
class AnalyticsService {
  final Ref ref; // 注入 Ref

  AnalyticsService(this.ref);

  void logCount() {
    // 直接读取当前值,不订阅变化 (类似 listen: false)
    final currentCount = ref.read(counterProvider);
    print("当前计数是: $currentCount");
  }
  
  void subscribeToCount() {
    // 也可以手动监听变化
    ref.listen(counterProvider, (previous, next) {
      print("计数从 $previous 变成了 $next");
    });
  }
}

特性 Provider (旧) Riverpod (新)
依赖 Context ✅ 强依赖 (BuildContext) ❌ 无依赖 (WidgetRefRef)
类型安全 ⚠️ 运行时检查 (容易崩溃) ✅ 编译时检查 (配合代码生成)
异步支持 🆗 需要 FutureProvider 🚀 原生强大 (AsyncValue, when)
状态组合 😐 较难,容易嵌套地狱 🤩 极简 (ref.watch 其他 Provider)
测试难度 😫 需要 Mock Context 😃 极易 (直接创建 ProviderContainer)
代码量 多 (样板代码) 少 (配合 @riverpod 宏)
学习曲线 低 (但精通难) 中 (概念多,但逻辑清晰)
  1. 始终使用代码生成 (@riverpod) :不要手动编写 Provider(...),让宏帮你处理类型安全和样板代码。

  2. 拆分小 Provider:不要试图用一个 Provider 管理所有状态。将计数器、用户信息、主题设置拆分成不同的 Provider,然后按需组合。

  3. 善用 AsyncValue:处理异步数据时,利用 .when() 方法优雅地处理 loadingdataerror 三种状态,避免大量的 if/else 判断。

  4. 区分 watch 和 read

    • 在 build 方法中需要重建 UI时用 ref.watch
    • 事件回调(如按钮点击)中修改数据时用 ref.read(...).notifier
    • 非 build 环境(如服务类)中用 ref.read 或 ref.listen

以下是自己的理解修正:

  1. watch:是订阅者。它监听 Provider 的数据变化,一旦变化,自动触发 UI 刷新(或 Provider 重算)。

  2. read:是获取动作

    • 事件回调(如按钮点击)中,我们使用 ref.read(provider.notifier) 来获取控制器,然后调用它的方法来修改数据
    • 修改数据后,Riverpod 会自动通知所有 watch 该数据的地方进行刷新。
  3. notifier:是控制器(遥控器)。它持有修改数据的方法(如 increment)。

你的目的 应该用什么? 结果
显示数据 (Text, Image, List) ref.watch() 数据变,UI 自动刷新 ✅
按钮点击/手势 (修改数据) ref.read(...).notifier 获取控制器,修改数据 ✅
按钮点击/手势 (读取参数) ref.read() 获取当前值,用于逻辑判断 ✅
定时器/异步回调 ref.read() 获取最新值,避免闭包旧值 ✅
纯 Dart 类/服务 ref.read() 访问全局状态 ✅
Build 中显示数据 ref.read() ❌ 界面不会更新 (Bug)

flutter存储知识点总结

一、数据存储

1、本地持久化存储SharedPreferences

SharedPreferences为轻量级存储,存储少量简单数据,键值对形式,不适合大量、复杂数据的存储。

import 'package:shared_preferences/shared_preferences.dart'; 
// 存储数据 
Future<void> saveData() async { 
// 获取 SharedPreferences 实例 
final prefs = await SharedPreferences.getInstance(); 
// 存储不同类型的数据 await prefs.setString('user_token', 'abc123456'); // 字符串
await prefs.setInt('user_age', 25); // 整数 a
wait prefs.setBool('is_login', true); // 布尔值 
await prefs.setDouble('height', 1.75); // 浮点数 
await prefs.setStringList('hobbies', ['读书', '运动']); // 字符串列表 } 

// 读取数据 Future<void> readData() async { 
final prefs = await SharedPreferences.getInstance(); 
// 读取数据(第二个参数是默认值,避免 null) 
String? token = prefs.getString('user_token') ?? ''; 
int age = prefs.getInt('user_age') ?? 0;
bool isLogin = prefs.getBool('is_login') ?? false; 
print('token: $token, age: $age, isLogin: $isLogin'); 
} 
// 删除数据 
Future<void> removeData() async { 
final prefs = await SharedPreferences.getInstance(); await prefs.remove('user_token'); 
// 删除单个键 
// await prefs.clear(); 
// 清空所有数据 }

2、Provider存储

Provider 是运行时的内存状态管理工具非本地持久化存储,Provider 存储的数据只在 App 运行时有效,重启后丢失。它的核心价值是让数据在多个 Widget 之间共享、响应式更新,是 “内存级” 的数据存储与共享方案。适用于需要跨组件共享、实时响应更新的运行时数据。 Provider的原理是基于 Flutter 原生的 InheritedWidget 实现的,而 InheritedWidget 的核心特性就是通过 Context 向上查找共享数据

  • 从 Provider 中读取 / 监听数据:必须依赖 context(因为要确定查找的上下文范围);

  • 从 Provider 中修改数据:通常也需要 context,但有替代方案(无需 Context);

  • 初始化 / 注入 Provider:不需要 context(在根节点创建时)。

2.1 定义数据模型
import 'package:flutter/foundation.dart';

class ContractDataModel extends ChangeNotifier {
  // ========== 核心数据字段 (仅保留3个示例) ==========

  // 1. 房源类型名称 (来自第一步)
  String _goodsTypeName = '';

  // 2. 租客姓名 (来自第二步)
  String _customerName = '';

  // 3. 租金单价 (来自第三步)
  String _unitPrice = '';

  // ========== Getters ==========
  String get goodsTypeName => _goodsTypeName;
  String get customerName => _customerName;
  String get unitPrice => _unitPrice;

  // ========== 统一更新方法 ==========
  /// 更新核心数据
  /// 只要传入的值不为 null 就更新,允许空字符串覆盖原有数据
  void updateCoreInfo({
    String? goodsTypeName,
    String? customerName,
    String? unitPrice,
  }) {
    if (goodsTypeName != null) _goodsTypeName = goodsTypeName;
    if (customerName != null) _customerName = customerName;
    if (unitPrice != null) _unitPrice = unitPrice;

    // 通知监听者重建 UI
    notifyListeners();
  }

  // ========== 验证数据是否完整 (示例) ==========
  bool validateCoreInfo() {
    if (_goodsTypeName.isEmpty) return false;
    if (_customerName.isEmpty) return false;
    if (_unitPrice.isEmpty) return false;
    return true;
  }

  // ========== 获取所有数据的 Map ==========
  Map<String, dynamic> toMap() {
    return {
      'goodsTypeName': _goodsTypeName,
      'customerName': _customerName,
      'unitPrice': _unitPrice,
    };
  }

  // ========== 清空所有数据 ==========
  void clear() {
    _goodsTypeName = '';
    _customerName = '';
    _unitPrice = '';

    notifyListeners();
  }
}
2.2 更新provider中的数据
final contractModel = Provider.of<ContractDataModel>(context, listen: false);
contractModel.updatePersonInfo(
  customerCertificateId: _customerCertificateId,
  customerName: _customerName,
);
2.3 获取provider,提取内部的数据
late _contractDataModel = Provider.of<ContractDataModel>(context, listen: false);
if(_contractDataModel.signClientType == '1'){//企业
  _customerCertificateType = 'Z';
}else{
  _customerCertificateType = '';
}

tips: 使用late的作用是什么? late 是用来修饰延迟初始化变量的关键字。

  • late用来声明变量时无法立即赋值的问题,Provider依赖context,而 context 通常在 Widget 的 build 方法、initState的位置才能获取,声明无法获取从而报错。
  • 允许变量非空但延迟复制。(Dart空安全核心)Dart 开启空安全后,未用 ? 标记的变量必须声明时赋值或用 late 修饰。

image.png

二、Provider 流程页面剖析

image.png

应该这样理解:

✅ Provider 是在 CreateReserveStepPage 中创建的(第52行)

✅ 三个 Step Widget 各自有自己独立的 context

✅ 但它们都是 ChangeNotifierProvider 的子孙节点

✅ 所以它们都能通过各自的 context 向上查找,找到同一个 Provider 实例

或者说:

✅ 三个 Widget 的 context 都能访问到在 CreateReserveStepPage 中创建的 Provider,因为它们都在 Provider 的子树中。

如下:

class CreateReserveStepPage extends StatefulWidget {
final Map params;
const CreateReserveStepPage({Key key,this.params}) : super(key: key);
@override
_CreateReserveStepPage createState() => _CreateReserveStepPage();
}

class _CreateReserveStepPage extends State<CreateReserveStepPage> {
final BrnMetaHorizontalStepsManager _stepsManager = BrnMetaHorizontalStepsManager();
int _currentIndex = 0;
bool _isCompleted = false;
Timer _timer;
int _elapsed = 0;
Map<String, dynamic> _contractParams = {};
final List<String> _stepTitles = ['合同信息', '租客/入住人信息', '账单/补充信息'];
// 验证回调函数
Function _validateContractWidget;
Function _validatePersonWidget;
Function _validateBillWidget;
// 保存数据回调函数
Function _savePersonWidget;
Function _saveBillWidget;
// 添加ScrollController
final ScrollController _scrollController = ScrollController();
void initState() {
  super.initState();
}

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    create: (_) => ContractDataModel(),
    builder: (providerContext, child) {
      return Scaffold(
        appBar: BrnAppBar(
          title: '创建签约',
          leading: IconButton(
            icon: Icon(Icons.arrow_back_ios_new, color: Colors.black),
            onPressed: () => BoostUtil.finish(),
          ),
        ),
        body: Column(
          children: [
            _stepsManager.buildSteps(
              steps: _stepTitles.map((title) => BrunoStep(stepContentText: title)).toList(),
              currentIndex: _currentIndex,
              isCompleted: _isCompleted,
            ),
            const SizedBox(height: 24),
            Expanded(
              child: SingleChildScrollView(
                controller: _scrollController,
                child: _buildStepContent(_currentIndex),
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
              decoration: BoxDecoration(
                color: Colors.white,
                border: Border(top: BorderSide(color: Color(0xFFF0F0F0), width: 1)),
              ),
              child: _buildBottomButtons(providerContext),
            ),
          ],
        ),
      );
    },
  );
}

Widget _buildStepContent(int index) {
  switch (index) {
    case 0:
      return ReservationStepContractWidget(
        params: this.widget.params,
        onDataChanged: _handleContractDataChanged,
        onValidateCallback: (validateFunc) {
          _validateContractWidget = validateFunc;
        },
      );
    case 1:
      return ReservationStepPersonWidget(
        onDataChanged: _handlePersonDataChanged,
        onValidateCallback: (validateFunc) {
          _validatePersonWidget = validateFunc;
        },
        onSaveCallback: (saveFunc) {
          _savePersonWidget = saveFunc;
        },
      );
    case 2:
      return ReservationStepBillWidget(
        onDataChanged: _handleBillDataChanged,
        onValidateCallback: (validateFunc) {
          _validateBillWidget = validateFunc;
        },
        onSaveCallback: (saveFunc) {
          _saveBillWidget = saveFunc;
        },
      );
    default:
      return SizedBox();
  }
}

❌