普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月15日首页
昨天以前首页

flutter_riverpod: ^2.6.1 应用笔记 (一)

作者 w_y_fan
2025年8月14日 17:27

flutter_riverpod: ^2.6.1 应用笔记 (一)

本文浅谈自己的关于新版Riverpod注解生成代码的应用流程:

1. 配置

下边是一些关于注解生成代码的相关配置pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  flutter_riverpod: ^2.6.1 # Riverpod的核心包
  riverpod_annotation: ^2.6.1 # 包含注解(如`@riverpod`)的包
  freezed_annotation: ^3.1.0 # 包含一些注解(例如`@freezed`)
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  build_runner: ^2.5.4 # 用于运行`riverpod_generator`,
  # 读取带有`@riverpod`注解的代码,并生成相应的provider代码。
  riverpod_generator: ^2.6.5 # Riverpod的代码生成器
  freezed: ^3.1.0 # 代码生成器

2. 理解

要是先不用类内部属性值的比较(不同于简单的 ==),就先不用freezed_annotationfreezed两个插件。因为Riverpod内部会通过 == 来比较state值的变化,从而更新UI;而普通自定义类默认 == 比较的是引用地址。

所以,状态类型是 简单类型(int、double、String、bool、List/Map 等不可变集合或者类内部没有比较需求,只关心对象引用,就不需要用这两个插件。freezed 的主要作用是生成不可变对象和内容比较逻辑,解决复杂状态的深度比较问题。

接下来是我对flutter_riverpod注解生成代码的一些实践以及实践中的理解:不同于GetXpub.dev/packages/ge… 全家桶,网上有很多对其的实战教程。flutter_riverpod的学习耗费了太多精力了,官方文档看的一知半解,项目实战又不是最新的,教学视频寥寥无几。最后在尽力了解官方文档以及杰哥的笔记再加上自己的尝试,最重要还是尝试。简单记录一下我的理解。

2.1 函数注解生成代码

注解生成代码我把其分为两大类:函数注解生成代码和类注解生成代码。我来一一列举就可以看出他们的区别:

首先要知道的是注解生成的Provider类型是基于函数的返回值类型得出的。

@riverpod  // 1
String helloWorld(Ref ref) {
  return 'Hello World';
}

@Riverpod()  // 2
String helloWorldCeche(Ref ref) {
  return 'Hello World';
}

@Riverpod(keepAlive: true)  // 3
String helloWorldCecheTwo(Ref ref) {
  return 'Hello World';
}

1和2是一样的生成的都是:AutoDisposeProvider 在没有任何监听者(watch/read)后,会自动释放资源并销毁内部状态的 Provider。3生成的是普通 Provider不会自己销毁。所以可以看出@riverpod@Riverpod()区别就是创建的Provider数据提供者是否能够自动销毁。

@ProviderFor(helloWorld) // 1
final helloWorldProvider = AutoDisposeProvider<String>.internal(
  helloWorld,
  name: r'helloWorldProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$helloWorldHash,
  dependencies: null,
  allTransitiveDependencies: null,
);
@ProviderFor(helloWorldCeche) // 2
final helloWorldCecheProvider = AutoDisposeProvider<String>.internal(
  helloWorldCeche,
  name: r'helloWorldCecheProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$helloWorldCecheHash,
  dependencies: null,
  allTransitiveDependencies: null,
);
@ProviderFor(helloWorldCecheTwo)  // 3
final helloWorldCecheTwoProvider = Provider<String>.internal(
  helloWorldCecheTwo,
  name: r'helloWorldCecheTwoProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$helloWorldCecheTwoHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

当然调用他们都是用:helloWorld是返回值,他们的state不能够改变。这是因为函数型 Provider(不管返回 String、Future、Stream)是 纯计算型 Provider,它本身没有 state 属性,只有 build 逻辑。

如果想要有可变 state,要用类式 ProviderNotifier AsyncNotifier)。

final helloWorld = ref.watch(helloWorldProvider);

旧的用法,状态的改变很分散

final myProvider = StateProvider((ref)=>100);
int myValue = ref.watch(myprovider);
ref.read(myProvider.state).state++;

接下来看几种其他常见的返回值类型分别对应哪几种Provider

@riverpod
Future<int> fetchCount(Ref ref) async {
  return 42;
}

@riverpod
Stream<double> numberStream(Ref ref) async* {
  yield 3.14;
}

@ProviderFor(fetchCount)
final fetchCountProvider = AutoDisposeFutureProvider<int>.internal(
  fetchCount,
  name: r'fetchCountProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$fetchCountHash,
  dependencies: null,
  allTransitiveDependencies: null,
);
@ProviderFor(numberStream)
final numberStreamProvider = AutoDisposeStreamProvider<double>.internal(
  numberStream,
  name: r'numberStreamProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$numberStreamHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

用法

final futureCeche = ref.watch(fetchCountProvider);
futureCeche.when(data: (data) => ,error: (error, stackTrace) => ,loading: () => ,);

另外我们知道函数除了有返回值还有参数,带参数的函数注解生成代码:

@riverpod  // 1
String fanGreet(Ref ref, String fanName) {
  return 'Hello $fanName';
}

@Riverpod(keepAlive: true)  // 2
String fanGreetCeche(Ref ref, String fanName) {
  return 'Hello $fanName';
}

// 1
@ProviderFor(fanGreet)
const fanGreetProvider = FanGreetFamily();
class FanGreetProvider extends AutoDisposeProvider<String> {}
// 2
@ProviderFor(fanGreetCeche)
const fanGreetCecheProvider = FanGreetCecheFamily();
class FanGreetCecheProvider extends Provider<String> {}

本质上继承的还是不同的Provider

要是想通过传入参数更改状态值的话,本质上是又创建了一个新的provider实例,并监听新的实例。

以上是函数注解生成代码的简单示例,主要作用还是返回值,要是就这么一点作用的话,体现不出来状态管理啊,状态都没办法改变,只能在初期获取值。

2.2 类注解生成代码

直接展示例子,@riverpod@Riverpod()的区别还是跟上边一样有没有AutoDispose只不过现在生成的是NotifierProvider

2.2.1 普通
@Riverpod()  // 1
class FanCecheOne extends _$FanCecheOne {
  @override
  int build() {
    return 30;
  }
}

@Riverpod()  // 2 
class FanCecheTwo extends _$FanCecheTwo {
  @override
  String build() {
    return 'FanCecheTwo';
  }
}

@riverpod  // 3
class FanCecheThree extends _$FanCecheThree {
  @override
  ({String name, int age}) build() {
    return (name: "xiaoli", age: 20);
  }
}
@ProviderFor(FanCecheOne)  // 1
final fanCecheOneProvider =
    AutoDisposeNotifierProvider<FanCecheOne, int>.internal(
      FanCecheOne.new,
      name: r'fanCecheOneProvider',
      debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
          ? null
          : _$fanCecheOneHash,
      dependencies: null,
      allTransitiveDependencies: null,
    );
@ProviderFor(FanCecheTwo)  // 2
final fanCecheTwoProvider =
    AutoDisposeNotifierProvider<FanCecheTwo, String>.internal(
      FanCecheTwo.new,
      name: r'fanCecheTwoProvider',
      debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
          ? null
          : _$fanCecheTwoHash,
      dependencies: null,
      allTransitiveDependencies: null,
    );
@ProviderFor(FanCecheThree)  // 3
final fanCecheThreeProvider =
    AutoDisposeNotifierProvider<
      FanCecheThree,
      ({String name, int age})
    >.internal(
      FanCecheThree.new,
      name: r'fanCecheThreeProvider',
      debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
          ? null
          : _$fanCecheThreeHash,
      dependencies: null,
      allTransitiveDependencies: null,
    );

返回的都是普通的数据类型,所以生成的都是NotifierProvider

2.2.2 特殊

特殊类型有Stream、Future

@riverpod  // 1
class UserData extends _$UserData {
  @override
  Future<String> build() async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    return 'User Name';
  }

  // 可定义方法触发状态刷新
  Future<void> refresh() async {
    state = AsyncValue.loading(); // 手动设置为 loading
    final data = await build();
    state = AsyncValue.data(data);
  }
}

@riverpod  // 2
class CounterStream extends _$CounterStream {
  @override
  Stream<int> build() async* {
    int i = 0;
    while (true) {
      await Future.delayed(Duration(seconds: 1));
      yield i++;
    }
  }

  // 可在内部定义方法,比如重置计数
  void reset() {
    // 需要调用 invalidate 或重新生成 Stream
    ref.invalidateSelf();
  }
}

生成

@ProviderFor(UserData)  // 1
final userDataProvider =
    AutoDisposeAsyncNotifierProvider<UserData, String>.internal(
      UserData.new,
      name: r'userDataProvider',
      debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
          ? null
          : _$userDataHash,
      dependencies: null,
      allTransitiveDependencies: null,
    );
@ProviderFor(CounterStream)  // 2
final counterStreamProvider =
    AutoDisposeStreamNotifierProvider<CounterStream, int>.internal(
      CounterStream.new,
      name: r'counterStreamProvider',
      debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
          ? null
          : _$counterStreamHash,
      dependencies: null,
      allTransitiveDependencies: null,
    );

可以看出Future生成的是AsyncNotifierProvider,Stream生成的是StreamNotifierProvider

另外在构造函数中传入参数的话就可以实现Family

例子:

@riverpod  // 1
class UserProfile extends _$UserProfile {
  @override
  Future<String> build({required int userId}) async {
    // 模拟根据参数请求数据
    await Future.delayed(Duration(seconds: 1));
    return 'User $userId';
  }
}

@riverpod  // 2
class TimerStream extends _$TimerStream {
  @override
  Stream<int> build({required int start}) async* {
    int i = start;
    while (true) {
      await Future.delayed(Duration(seconds: 1));
      yield i++;
    }
  }
}

生成

@ProviderFor(UserProfile)  // 1
const userProfileProvider = UserProfileFamily();
class UserProfileProvider
    extends AutoDisposeAsyncNotifierProviderImpl<UserProfile, String> {}
@ProviderFor(TimerStream)  // 2
const timerStreamProvider = TimerStreamFamily();
class TimerStreamProvider
    extends AutoDisposeStreamNotifierProviderImpl<TimerStream, int> {}

本质上还是NotifierProvider

调用的列子

// 验证码倒计时的状态管理
// 验证码状态
enum CaptchaState {
  init, // 初始状态
  counting, // 倒计时中
  restart, // 可重新发送
}

@Riverpod(keepAlive: true)
class Captcha extends _$Captcha {
  Timer? _timer;
  @override
  ({CaptchaState state, int countdown}) build(String pageId) {
    // 只保留定时器取消逻辑,不重置状态
    ref.onDispose(() => _timer?.cancel());
    return (state: CaptchaState.init, countdown: 0);
  }

  // 初始化倒计时
  void initCaptcha() {
    // 取消之前的定时器,防止它继续修改状态
    _timer?.cancel();
    _timer = null;
    // 重置状态
    if (state.state != CaptchaState.init) {
      state = (state: CaptchaState.init, countdown: 0);
    }
  }

  // 启动倒计时
  void startCountdown() {
    if (state.state == CaptchaState.counting) {
      return;
    }
    // 取消之前的定时器
    _timer?.cancel();
    // 初始化状态
    state = (state: CaptchaState.counting, countdown: 5);
    // 创建周期性定时器
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      final newCount = state.countdown - 1;
      if (newCount <= 0) {
        timer.cancel();
        // 更新状态为可重新发送
        state = (state: CaptchaState.restart, countdown: 0);
        return;
      }
      // 更新倒计时状态
      state = (state: CaptchaState.counting, countdown: newCount);
    });
  }
}

调用

final captchaState = ref.watch(captchaProvider(Homepage.sName),);
final notifier = ref.read(captchaProvider(Homepage.sName).notifier,);
notifier.startCountdown();

3.总结

写到这里虽然没写多少东西,但是脑子已经累了。实践是检验真理的唯一标准,真正实践Riverpod的应用才浅浅的知道了该怎么用这个东西。大概总结一下吧,脑子已经乱了。对于单一数据获取,不去人工更改数据的话,函数注解生成代码已经够了,但是感觉大多数场景满足不了的吧。所以我们还是主要选择类注解生成代码,新版本的用法就是要让我们把数据的build获取以及数据的改变逻辑都放在类中,做到逻辑业务与UI展示分离,降低代码的耦合度,真的挺方便规范开发的。其实看了上述例子,其实我们就可以把类注解生成代码中的build当做函数注解生成代码就更容易理解了。生成的Provider类型完全就是和构造函数的返回值以及传入的参数有关。

Flutter 环境安装

2025年8月13日 15:14

1. 使用镜像

由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,大家可以将如下环境变量添加到到用户环境变量中:

set PUB_HOSTED_URL=https://pub.flutter-io.cn
set FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
set HTTP_PROXY=http://127.0.0.1:7890 // 后面会需要用到  7890是你代理的端口,如有不同自行修改
set HTTPS_PROXY=https://127.0.0.1:7890
set NO_PROXY=localhost,127.0.0.1,::1
// 以上是所有需要设置的环境变量


注意:  此镜像为临时镜像,并不能保证一直可用,读者可以参考flutter.dev/community/c… 以获得有关镜像服务器的最新动态

2. 获取Flutter SDK

  1. 去flutter官网下载其最新可用的安装包,下载地址:flutter.dev/docs/develo…

    注意,Flutter的渠道版本会不停变动,请以Flutter官网为准。另外,在中国大陆地区,要想正常获取安装包列表或下载安装包,可能需要翻墙,读者也可以去Flutter github项目下去下载安装包,地址:github.com/flutter/flu…

  2. 将安装包zip解压到你想安装Flutter SDK的路径(如:C:\src\flutter;注意,不要将flutter安装到需要一些高权限的路径如C:\Program Files)。

  3. 在Flutter安装目录的flutter文件下找到flutter_console.bat,双击运行并启动flutter命令行,接下来,你就可以在Flutter命令行运行flutter命令了。

更新环境变量

如果你想在Windows系统自带命令行运行flutter命令,需要添加以下环境变量到用户PATH:

  • 在开始菜单的搜索功能键入“env”,然后选择 编辑系统环境变量

  • 在“用户变量”下检查是否有名为“Path”的条目:

    • 如果该条目存在, 追加 flutter\bin的全路径,使用 ; 作为分隔符.
    • 如果该条目不存在,创建一个新用户变量 Path ,然后将 flutter\bin 的全路径作为它的值.

重启Windows以应用此更改.

运行 flutter doctor命令

在Flutter命令行运行如下命令来查看是否还需要安装其他依赖,如果需要,安装它们:

flutter doctor

第一次运行flutter命令(如flutter doctor)时,它会下载它自己的依赖项并自行编译。以后再运行就会快得多。缺失的依赖需要安装一下,安装完成后再运行flutter doctor命令来验证是否安装成功。

3)Android设置

Flutter依赖于Android Studio的全量安装。Android Studio不仅可以管理Android 平台依赖、SDK版本等,而且它也是Flutter开发推荐的IDE之一(当然,你也可以使用其他编辑器或IDE,我们将会在后面讨论)。

#安装Android Studio
  1. 下载并安装 Android Studio,下载地址:developer.android.com/studio/inde…
  2. 启动Android Studio,然后执行“Android Studio安装向导”。这将安装最新的Android SDK、Android SDK平台工具和Android SDK构建工具,这些是用Flutter进行Android开发所需要的。

Tips

flutter doctor 可能出现的错误提示:

1.A network error occurred while checking maven.google.com 通过上面的HTTP_PROXY,HTTPS_PROXY环境变量解决

2.Visual Studio not installed; this is necessary to develop Windows apps. Download at visualstudio.microsoft.com/downloads/. Please install the "Desktop development with C++" workload, including all of its default components,这个错误 需下载vs_BuildTools.exe 文件

image.png 到此安装成功

flutter学习第 13 节:本地存储​

作者 叽哥
2025年8月12日 12:14

在移动应用开发中,本地存储是一项关键功能,用于保存用户偏好设置、离线数据、登录状态等信息。Flutter 提供了多种本地存储方案,适用于不同的场景需求。本节课将介绍 Flutter 中常用的本地存储方式,包括轻量级的键值对存储和更复杂的数据库存储,并通过实例展示其实际应用。

一、本地存储概述

本地存储在移动应用中具有重要作用,主要应用场景包括:

  • 保存用户登录状态,避免重复登录
  • 存储应用配置和用户偏好设置
  • 缓存网络请求数据,实现离线功能
  • 存储结构化数据,如聊天记录、任务列表等

Flutter 中常用的本地存储方案有:

  1. shared_preferences:轻量级键值对存储,适用于简单数据
  2. sqflite:SQLite 数据库封装,适用于结构化数据
  3. hive:高性能 NoSQL 数据库,纯 Dart 实现
  4. flutter_secure_storage:安全存储,适用于敏感信息如令牌

本节课重点介绍前两种最常用的存储方案。



二、轻量级存储:shared_preferences

shared_preferences 是 Flutter 社区提供的一个插件,用于存储简单的键值对数据。它在 iOS 上使用 NSUserDefaults,在 Android 上使用 SharedPreferences,提供了跨平台的一致 API。

1. 安装与配置

在 pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.5.3  # 使用最新版本

运行 flutter pub get 安装依赖。

2. 基本使用方法

shared_preferences 支持存储的数据类型包括:Stringintdoublebool 和 List<String>

基本操作步骤:

  1. 获取 SharedPreferences 实例
  2. 使用相应的方法进行数据读写
  3. 无需手动关闭实例
import 'package:shared_preferences/shared_preferences.dart';

// 保存数据
Future<void> saveData() async {
  // 获取实例
  final prefs = await SharedPreferences.getInstance();

  // 存储不同类型的数据
  await prefs.setString('username', 'john_doe');
  await prefs.setInt('age', 30);
  await prefs.setDouble('height', 1.75);
  await prefs.setBool('isPremium', false);
  await prefs.setStringList('hobbies', ['reading', 'sports', 'coding']);
}

// 读取数据
Future<void> readData() async {
  final prefs = await SharedPreferences.getInstance();

  // 读取数据,提供默认值
  String username = prefs.getString('username') ?? 'Guest';
  int age = prefs.getInt('age') ?? 0;
  double height = prefs.getDouble('height') ?? 0.0;
  bool isPremium = prefs.getBool('isPremium') ?? false;
  List<String> hobbies = prefs.getStringList('hobbies') ?? [];

  print('Username: $username');
  print('Age: $age');
  print('Height: $height');
  print('Is Premium: $isPremium');
  print('Hobbies: $hobbies');
}

// 更新数据
Future<void> updateData() async {
  final prefs = await SharedPreferences.getInstance();
  // 更新已存在的键的值
  await prefs.setInt('age', 31);
  await prefs.setBool('isPremium', true);
}

// 删除数据
Future<void> deleteData() async {
  final prefs = await SharedPreferences.getInstance();
  // 删除指定键
  await prefs.remove('height');

  // 清除所有数据
  // await prefs.clear();
}

3. shared_preferences 封装

为了简化使用并避免重复代码,可以封装一个工具类:

import 'package:shared_preferences/shared_preferences.dart';

class PrefsService {
  // 单例模式
  static final PrefsService _instance = PrefsService._internal();
  factory PrefsService() => _instance;
  PrefsService._internal();

  late SharedPreferences _prefs;

  // 初始化
  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  // 存储方法
  Future<void> setString(String key, String value) async {
    await _prefs.setString(key, value);
  }

  Future<void> setInt(String key, int value) async {
    await _prefs.setInt(key, value);
  }

  Future<void> setDouble(String key, double value) async {
    await _prefs.setDouble(key, value);
  }

  Future<void> setBool(String key, bool value) async {
    await _prefs.setBool(key, value);
  }

  Future<void> setStringList(String key, List<String> value) async {
    await _prefs.setStringList(key, value);
  }

  // 读取方法
  String getString(String key, {String defaultValue = ''}) {
    return _prefs.getString(key) ?? defaultValue;
  }

  int getInt(String key, {int defaultValue = 0}) {
    return _prefs.getInt(key) ?? defaultValue;
  }

  double getDouble(String key, {double defaultValue = 0.0}) {
    return _prefs.getDouble(key) ?? defaultValue;
  }

  bool getBool(String key, {bool defaultValue = false}) {
    return _prefs.getBool(key) ?? defaultValue;
  }

  List<String> getStringList(String key, {List<String> defaultValue = const []}) {
    return _prefs.getStringList(key) ?? defaultValue;
  }

  // 删除方法
  Future<void> remove(String key) async {
    await _prefs.remove(key);
  }

  Future<void> clear() async {
    await _prefs.clear();
  }

  // 检查键是否存在
  bool containsKey(String key) {
    return _prefs.containsKey(key);
  }
}

使用封装类:

// 初始化(通常在 app 启动时)
await PrefsService().init();

// 存储数据
await PrefsService().setString('username', 'jane_smith');
await PrefsService().setInt('score', 100);

// 读取数据
String username = PrefsService().getString('username');
int score = PrefsService().getInt('score');

4. shared_preferences 适用场景与局限性

适用场景

  • 存储用户偏好设置(如主题模式、语言选择)
  • 保存简单的用户状态(如登录状态标记)
  • 存储少量的配置信息

局限性

  • 不适合存储大量数据
  • 不支持复杂数据结构
  • 数据存储在明文文件中,不适合存储敏感信息
  • 没有查询功能,只能通过键获取值


三、数据库存储:sqflite 基础

对于需要存储大量结构化数据的场景,sqflite 是更好的选择。sqflite 是 Flutter 中 SQLite 数据库的封装,提供了完整的 SQL 操作能力。

1. 安装与配置

在 pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.4.2  # SQLite 数据库
  path: ^1.9.1     # 用于处理文件路径

运行 flutter pub get 安装依赖。

2. 数据库基本操作

SQLite 数据库操作主要包括:创建数据库和表、插入数据、查询数据、更新数据和删除数据。

创建数据库和表

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  // 数据库名称
  static const _databaseName = "MyDatabase.db";
  // 数据库版本
  static const _databaseVersion = 1;

  // 表名
  static const table = 'notes';

  // 列名
  static const columnId = '_id';
  static const columnTitle = 'title';
  static const columnContent = 'content';
  static const columnCreatedAt = 'created_at';

  // 单例模式
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // 数据库实例
  static Database? _database;
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // 初始化数据库
  _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(path,
        version: _databaseVersion,
        onCreate: _onCreate);
  }

  // 创建表
  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            $columnId INTEGER PRIMARY KEY AUTOINCREMENT,
            $columnTitle TEXT NOT NULL,
            $columnContent TEXT,
            $columnCreatedAt TEXT NOT NULL
          )
          ''');
  }
}

插入数据

// 插入数据
Future<int> insertNote(Map<String, dynamic> note) async {
  Database db = await DatabaseHelper.instance.database;
  // 插入数据并返回新记录的 ID
  return await db.insert(DatabaseHelper.table, note);
}

// 使用示例
void addNote() async {
  Map<String, dynamic> newNote = {
    DatabaseHelper.columnTitle: 'First Note',
    DatabaseHelper.columnContent: 'This is my first note',
    DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
  };

  int id = await insertNote(newNote);
  print('Inserted note with id: $id');
}

查询数据

// 查询所有数据
Future<List<Map<String, dynamic>>> queryAllNotes() async {
  Database db = await DatabaseHelper.instance.database;
  return await db.query(DatabaseHelper.table);
}

// 根据 ID 查询
Future<List<Map<String, dynamic>>> queryNote(int id) async {
  Database db = await DatabaseHelper.instance.database;
  return await db.query(DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void getNotes() async {
  List<Map<String, dynamic>> notes = await queryAllNotes();
  print('All notes: $notes');

  if (notes.isNotEmpty) {
    List<Map<String, dynamic>> singleNote = await queryNote(notes[0][DatabaseHelper.columnId]);
    print('First note: $singleNote');
  }
}

更新数据

// 更新数据
Future<int> updateNote(Map<String, dynamic> note) async {
  Database db = await DatabaseHelper.instance.database;
  int id = note[DatabaseHelper.columnId];
  // 返回受影响的行数
  return await db.update(DatabaseHelper.table, note,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void modifyNote(int noteId) async {
  Map<String, dynamic> updatedNote = {
    DatabaseHelper.columnId: noteId,
    DatabaseHelper.columnTitle: 'Updated Note',
    DatabaseHelper.columnContent: 'This note has been updated',
    DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
  };

  int rowsAffected = await updateNote(updatedNote);
  print('Updated $rowsAffected rows');
}

删除数据

// 删除数据
Future<int> deleteNote(int id) async {
  Database db = await DatabaseHelper.instance.database;
  // 返回受影响的行数
  return await db.delete(DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void removeNote(int noteId) async {
  int rowsDeleted = await deleteNote(noteId);
  print('Deleted $rowsDeleted rows');
}

3. 数据库版本迁移

当应用升级需要修改数据库结构时,需要进行数据库迁移:

// 在 DatabaseHelper 类中修改 _initDatabase 方法
_initDatabase() async {
  String path = join(await getDatabasesPath(), _databaseName);
  return await openDatabase(
    path,
    version: _databaseVersion,
    onCreate: _onCreate,
    onUpgrade: _onUpgrade, // 添加升级回调
    onDowngrade: onDatabaseDowngradeDelete, // 降级时删除数据库
  );
}

// 数据库升级
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
  if (oldVersion < 2) {
    // 版本 2 新增了一个 'category' 列
    await db.execute('''
      ALTER TABLE ${DatabaseHelper.table} 
      ADD COLUMN category TEXT
    ''');
  }
  if (oldVersion < 3) {
    // 版本 3 新增了一个 'priority' 列
    await db.execute('''
      ALTER TABLE ${DatabaseHelper.table} 
      ADD COLUMN priority INTEGER DEFAULT 0
    ''');
  }
}

4. 模型类与数据库操作封装

为了更好地管理数据,通常会创建模型类并封装数据库操作:

// 笔记模型类
class Note {
  final int? id;
  final String title;
  final String? content;
  final String createdAt;
  final String? category;
  final int priority;

  Note({
    this.id,
    required this.title,
    this.content,
    required this.createdAt,
    this.category,
    this.priority = 0,
  });

  // 从 Map 转换为 Note 对象
  factory Note.fromMap(Map<String, dynamic> map) {
    return Note(
      id: map[DatabaseHelper.columnId],
      title: map[DatabaseHelper.columnTitle],
      content: map[DatabaseHelper.columnContent],
      createdAt: map[DatabaseHelper.columnCreatedAt],
      category: map['category'],
      priority: map['priority'] ?? 0,
    );
  }

  // 转换为 Map
  Map<String, dynamic> toMap() {
    return {
      DatabaseHelper.columnId: id,
      DatabaseHelper.columnTitle: title,
      DatabaseHelper.columnContent: content,
      DatabaseHelper.columnCreatedAt: createdAt,
      'category': category,
      'priority': priority,
    };
  }
}

// 封装数据库操作
class NoteService {
  // 插入笔记
  Future<int> insertNote(Note note) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.insert(DatabaseHelper.table, note.toMap());
  }

  // 获取所有笔记
  Future<List<Note>> getAllNotes() async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(DatabaseHelper.table);
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }

  // 根据 ID 获取笔记
  Future<Note?> getNoteById(int id) async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
    if (maps.isNotEmpty) {
      return Note.fromMap(maps.first);
    }
    return null;
  }

  // 更新笔记
  Future<int> updateNote(Note note) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.update(
      DatabaseHelper.table,
      note.toMap(),
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [note.id],
    );
  }

  // 删除笔记
  Future<int> deleteNote(int id) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.delete(
      DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
  }

  // 按类别查询笔记
  Future<List<Note>> getNotesByCategory(String category) async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      where: 'category = ?',
      whereArgs: [category],
    );
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }

  // 按优先级排序查询
  Future<List<Note>> getNotesSortedByPriority() async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      orderBy: 'priority DESC',
    );
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }
}


四、实例:保存用户登录状态

下面实现一个保存用户登录状态的功能,使用 shared_preferences 存储登录信息:

import 'package:shared_preferences/shared_preferences.dart';

class AuthService {
  static const _tokenKey = 'auth_token';
  static const _userIdKey = 'user_id';
  static const _usernameKey = 'username';

  // 保存登录状态
  Future<void> saveLoginState({
    required String token,
    required String userId,
    required String username,
  }) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_tokenKey, token);
    await prefs.setString(_userIdKey, userId);
    await prefs.setString(_usernameKey, username);
  }

  // 获取当前登录用户信息
  Future<Map<String, String>?> getCurrentUser() async {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString(_tokenKey);
    final userId = prefs.getString(_userIdKey);
    final username = prefs.getString(_usernameKey);

    if (token != null && userId != null && username != null) {
      return {
        'token': token,
        'userId': userId,
        'username': username,
      };
    }
    return null;
  }

  // 检查是否已登录
  Future<bool> isLoggedIn() async {
    final user = await getCurrentUser();
    return user != null;
  }

  // 登出,清除登录状态
  Future<void> logout() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_tokenKey);
    await prefs.remove(_userIdKey);
    await prefs.remove(_usernameKey);
  }

  // 获取认证令牌
  Future<String?> getAuthToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_tokenKey);
  }
}

在应用启动时检查登录状态:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 检查登录状态
  AuthService authService = AuthService();
  bool isLoggedIn = await authService.isLoggedIn();
  
  runApp(MyApp(isLoggedIn: isLoggedIn));
}

class MyApp extends StatelessWidget {
  final bool isLoggedIn;

  const MyApp({super.key, required this.isLoggedIn});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Local Storage Demo',
      home: isLoggedIn ? const HomeScreen() : const LoginScreen(),
    );
  }
}

登录页面实现:

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  final _authService = AuthService();
  bool _isLoading = false;

  Future<void> _login(BuildContext context) async {
    setState(() {
      _isLoading = true;
    });

    // 模拟 API 登录请求
    await Future.delayed(const Duration(seconds: 2));

    // 实际应用中应该验证用户名和密码
    if (_usernameController.text.isNotEmpty &&
        _passwordController.text.isNotEmpty) {
      // 保存登录状态
      await _authService.saveLoginState(
        token: 'fake_auth_token_123456',
        userId: 'user_123',
        username: _usernameController.text,
      );

      // 导航到主页
      if (mounted) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (context) => const HomeScreen()),
        );
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Please enter username and password')),
        );
      }
    }

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            const SizedBox(height: 20),
            _isLoading
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: () => _login(context),
                    child: const Text('Login'),
                  ),
          ],
        ),
      ),
    );
  }
}

主页实现:

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _authService = AuthService();
  String? _username;

  @override
  void initState() {
    super.initState();
    _loadUserInfo();
  }

  Future<void> _loadUserInfo() async {
    final user = await _authService.getCurrentUser();
    setState(() {
      _username = user?['username'];
    });
  }

  Future<void> _logout(BuildContext context) async {
    await _authService.logout();
    if (mounted) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => const LoginScreen()),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => _logout(context),
          ),
        ],
      ),
      body: Center(
        child: _username != null
            ? Text('Welcome, $_username! You are logged in.')
            : const Text('Loading user info...'),
      ),
    );
  }
}


五、实例:离线数据缓存

使用 sqflite 实现网络数据的本地缓存,实现离线查看功能:

// 新闻模型类
class Article {
  final String id;
  final String title;
  final String? description;
  final String url;
  final String? urlToImage;
  final String publishedAt;
  final String? content;

  Article({
    required this.id,
    required this.title,
    this.description,
    required this.url,
    this.urlToImage,
    required this.publishedAt,
    this.content,
  });

  // 从 Map 转换
  factory Article.fromMap(Map<String, dynamic> map) {
    return Article(
      id: map['id'],
      title: map['title'],
      description: map['description'],
      url: map['url'],
      urlToImage: map['urlToImage'],
      publishedAt: map['publishedAt'],
      content: map['content'],
    );
  }

  // 转换为 Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'description': description,
      'url': url,
      'urlToImage': urlToImage,
      'publishedAt': publishedAt,
      'content': content,
      'cachedAt': DateTime.now().toIso8601String(), // 缓存时间
    };
  }
}

// 新闻数据库帮助类
class NewsDatabaseHelper {
  static const _databaseName = "NewsDatabase.db";
  static const _databaseVersion = 1;
  static const table = 'articles';

  NewsDatabaseHelper._privateConstructor();
  static final NewsDatabaseHelper instance =
      NewsDatabaseHelper._privateConstructor();

  static Database? _database;
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
    );
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            description TEXT,
            url TEXT NOT NULL,
            urlToImage TEXT,
            publishedAt TEXT NOT NULL,
            content TEXT,
            cachedAt TEXT NOT NULL
          )
          ''');
  }

  // 批量插入或更新文章
  Future<void> batchInsertOrUpdateArticles(List<Article> articles) async {
    Database db = await instance.database;
    Batch batch = db.batch();

    for (var article in articles) {
      batch.insert(
        table,
        article.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace, // 存在则替换
      );
    }

    await batch.commit();
  }

  // 获取所有缓存文章
  Future<List<Article>> getCachedArticles() async {
    Database db = await instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      table,
      orderBy: 'publishedAt DESC',
    );
    return List.generate(maps.length, (i) => Article.fromMap(maps[i]));
  }

  // 获取单篇文章
  Future<Article?> getArticleById(String id) async {
    Database db = await instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      table,
      where: 'id = ?',
      whereArgs: [id],
    );
    if (maps.isNotEmpty) {
      return Article.fromMap(maps.first);
    }
    return null;
  }

  // 清除过期缓存(例如超过7天的)
  Future<void> clearExpiredCache() async {
    Database db = await instance.database;
    final sevenDaysAgo = DateTime.now()
        .subtract(const Duration(days: 7))
        .toIso8601String();
    await db.delete(table, where: 'cachedAt < ?', whereArgs: [sevenDaysAgo]);
  }

  // 清除所有缓存
  Future<void> clearAllCache() async {
    Database db = await instance.database;
    await db.delete(table);
  }
}

新闻仓库类,整合网络请求和本地缓存:

import 'package:dio/dio.dart';

class NewsRepository {
  final Dio _dio = Dio();
  final NewsDatabaseHelper _dbHelper = NewsDatabaseHelper.instance;
  final String _apiKey = 'your_news_api_key';

  // 获取头条新闻,优先从网络获取,失败则使用缓存
  Future<List<Article>> getTopHeadlines({bool forceRefresh = false}) async {
    try {
      // 如果不是强制刷新,先检查缓存
      if (!forceRefresh) {
        final cachedArticles = await _dbHelper.getCachedArticles();
        if (cachedArticles.isNotEmpty) {
          return cachedArticles;
        }
      }

      // 网络请求
      Response response = await _dio.get(
        'https://newsapi.org/v2/top-headlines',
        queryParameters: {
          'country': 'us',
          'apiKey': _apiKey,
        },
      );

      // 解析数据
      List<Article> articles = (response.data['articles'] as List)
          .map((item) => Article(
        id: item['url'], // 使用 url 作为唯一标识
        title: item['title'],
        description: item['description'],
        url: item['url'],
        urlToImage: item['urlToImage'],
        publishedAt: item['publishedAt'],
        content: item['content'],
      ))
          .toList();

      // 缓存到本地数据库
      await _dbHelper.batchInsertOrUpdateArticles(articles);

      // 清除过期缓存
      await _dbHelper.clearExpiredCache();

      return articles;
    } catch (e) {
      // 网络请求失败,尝试返回缓存
      final cachedArticles = await _dbHelper.getCachedArticles();
      if (cachedArticles.isNotEmpty) {
        return cachedArticles;
      }
      // 没有缓存,抛出错误
      throw Exception('Failed to fetch news and no cache available');
    }
  }
}


六、其他存储方案简介

1. flutter_secure_storage

用于存储敏感信息,如令牌、密码等,数据会被加密存储:

dependencies:
  flutter_secure_storage: ^9.2.4

使用示例:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

// 存储敏感数据
await storage.write(key: 'auth_token', value: 'sensitive_token_123');

// 读取数据
String? token = await storage.read(key: 'auth_token');

// 删除数据
await storage.delete(key: 'auth_token');

2. Hive

Hive 是一个高性能、轻量级的 NoSQL 数据库,纯 Dart 实现,速度快且易于使用:

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

使用示例:

// 初始化
await Hive.initFlutter();
Hive.registerAdapter(NoteAdapter()); // 注册适配器
await Hive.openBox<Note>('notes');

// 获取盒子
var box = Hive.box<Note>('notes');

// 添加数据
var note = Note(id: 1, title: 'Hive Demo', content: 'Hello Hive');
await box.put(note.id, note);

// 获取数据
Note? savedNote = box.get(1);

// 查询所有数据
List<Note> allNotes = box.values.toList();

// 更新数据
note.title = 'Updated Title';
await box.put(note.id, note);

// 删除数据
await box.delete(1);


七、本地存储最佳实践

  1. 选择合适的存储方案
    • 简单键值对数据使用 shared_preferences
    • 敏感信息使用 flutter_secure_storage
    • 结构化数据使用 sqflite 或 hive
  2. 数据分层管理
    • 封装存储操作,与业务逻辑分离
    • 使用仓库模式(Repository Pattern)整合网络和本地存储
  3. 性能优化
    • 批量操作数据库,减少 IO 次数
    • 及时清理过期缓存,避免存储空间过大
    • 数据库操作放在后台线程,避免阻塞 UI
  4. 错误处理
    • 对所有存储操作添加异常捕获
    • 网络请求失败时,提供使用缓存数据的降级策略
  5. 数据迁移
    • 规划好数据库版本,做好升级迁移方案
    • 重大更新时考虑数据备份和恢复机制
  6. 安全考虑
    • 敏感数据必须加密存储
    • 避免存储不必要的用户隐私数据

flutter学习第 11 节:状态管理进阶:Provider

作者 叽哥
2025年8月11日 18:12

在 Flutter 应用开发中,随着应用规模的扩大,组件之间的状态共享和通信会变得越来越复杂。当多个组件需要访问和修改同一状态时,传统的通过构造函数传递参数的方式会导致代码冗余、耦合度高且难以维护。本节课将介绍 Flutter 中常用的状态管理方案 ——Provider,它基于 InheritedWidget 实现,能够简洁高效地解决跨组件状态共享问题。

一、为什么需要状态管理

在 Flutter 中,所有 UI 都是由 Widget 构成的,而 Widget 是不可变的,状态(State)则是 Widget 中可以变化的数据。当应用简单时,我们可以通过 setState() 管理单个组件的状态,但当状态需要在多个组件之间共享时,就会面临以下问题:

  1. 跨组件通信困难:深层嵌套的子组件需要访问父组件的状态时,必须通过层层传递参数,形成 "prop drilling" 问题
  2. 状态同步复杂:多个组件依赖同一状态时,手动同步状态会导致代码逻辑混乱
  3. 代码可维护性差:状态分散在各个组件中,修改和调试变得困难
  4. 性能问题:不合理的状态管理会导致不必要的组件重建,影响应用性能

举个例子,一个电商应用中的购物车状态可能需要在商品列表、购物车页面、结算页面等多个地方访问和修改。如果使用传统方式,需要在各个页面之间传递购物车数据,当购物车发生变化时,还要手动通知所有相关页面更新,这显然是低效且容易出错的。

状态管理方案正是为了解决这些问题而诞生的,Provider 是其中最简单易用且官方推荐的方案之一。



二、Provider 基本概念与核心类

Provider 是基于 Flutter 原生 InheritedWidget 实现的状态管理库,它的核心思想是将共享状态抽离到一个独立的类中,然后通过一个 Provider 组件在 Widget 树中提供这个状态,最后在需要使用该状态的子组件中进行消费。

1. 核心类介绍

  • ChangeNotifier:一个实现了观察者模式的类,当状态发生变化时,调用 notifyListeners() 方法通知所有监听者更新
  • ChangeNotifierProvider:一个 Widget,用于在 Widget 树中提供 ChangeNotifier 实例,使其子树中的 Widget 可以访问该实例
  • Consumer:用于在子 Widget 中获取并监听 ChangeNotifier 实例,当状态变化时会重建自身
  • Provider.of:另一种获取 ChangeNotifier 实例的方法,可以选择是否监听状态变化

2. 安装 Provider

在使用 Provider 之前,需要在 pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.5  # 请使用最新版本

然后运行 flutter pub get 安装依赖。



三、Provider 基本使用流程

使用 Provider 管理状态通常遵循以下步骤:

  1. 创建一个继承自 ChangeNotifier 的状态类,封装需要共享的状态和修改状态的方法
  2. 使用 ChangeNotifierProvider 在 Widget 树的适当位置提供该状态实例
  3. 在需要使用状态的子 Widget 中,通过 Consumer 或 Provider.of 获取状态并使用

下面通过一个简单的计数器示例来演示基本用法:

1. 创建状态类

import 'package:flutter/foundation.dart';

// 继承 ChangeNotifier
class Counter with ChangeNotifier {
  int _count = 0;

  // 提供获取状态的方法
  int get count => _count;

  // 提供修改状态的方法
  void increment() {
    _count++;
    // 通知所有监听者
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

2. 提供状态

在 Widget 树中使用 ChangeNotifierProvider 提供状态,通常放在应用的根部:

import 'package:provider/provider.dart';

void main() {
  runApp(
    // 提供 Counter 实例
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Demo',
      home: const CounterPage(),
    );
  }
}

3. 消费状态

在子 Widget 中通过 Consumer 或 Provider.of 获取并使用状态:

使用 Consumer

import 'package:provider/provider.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter Demo')),
      body: Center(
        // 使用 Consumer 获取 Counter 实例
        child: Consumer<Counter>(
          builder: (context, counter, child) {
            // builder 方法会在状态变化时重建
            return Text(
              'Current count: ${counter.count}',
              style: const TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              // 使用 Provider.of 获取实例(不监听变化)
              Provider.of<Counter>(context, listen: false).decrement();
            },
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () {
              Provider.of<Counter>(context, listen: false).increment();
            },
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

Consumer 性能优化

Consumer 的 child 参数可以用于优化性能,避免不必要的重建:

Consumer<Counter>(
  builder: (context, counter, child) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 这个 Text 会在状态变化时重建
        Text('Count: ${counter.count}'),
        // 这个子组件不会重建,因为它被提取到了 child 参数中
        child!,
      ],
    );
  },
  // 这个子组件只会构建一次
  child: const Text('This is a static text'),
)

使用 Provider.of

Provider.of<T>(context) 会获取最近的 T 类型的 Provider,并在状态变化时重建当前 Widget:

// 监听状态变化,状态变化时会重建当前 Widget
final counter = Provider.of<Counter>(context);

// 不监听状态变化,通常用于修改状态的场景
final counter = Provider.of<Counter>(context, listen: false);

使用示例:

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({super.key});

  @override
  Widget build(BuildContext context) {
    // 获取 Counter 实例并监听变化
    final counter = Provider.of<Counter>(context);

    return Text(
      'Count: ${counter.count}',
      style: const TextStyle(fontSize: 24),
    );
  }
}

class CounterActions extends StatelessWidget {
  const CounterActions({super.key});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        // 获取 Counter 实例但不监听变化
        Provider.of<Counter>(context, listen: false).increment();
      },
      child: const Icon(Icons.add),
    );
  }
}


四、多个状态管理

当应用中有多个独立的状态需要管理时,可以使用 MultiProvider 来组织多个 Provider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => Counter()),
        ChangeNotifierProvider(create: (context) => ThemeProvider()),
        ChangeNotifierProvider(create: (context) => UserProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

这样,在子组件中可以分别获取不同的状态:

// 获取计数器状态
final counter = Provider.of<Counter>(context);

// 获取主题状态
final themeProvider = Provider.of<ThemeProvider>(context);


五、实例:用 Provider 管理购物车状态

下面通过一个完整的购物车示例,展示如何使用 Provider 管理复杂状态:

1. 定义数据模型

// 商品模型
class Product {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });
}

// 购物车项模型
class CartItem {
  final String id;
  final String productId;
  final String name;
  final int quantity;
  final double price;

  CartItem({
    required this.id,
    required this.productId,
    required this.name,
    required this.quantity,
    required this.price,
  });

  // 计算小计
  double get totalPrice => price * quantity;
}

2. 创建购物车状态管理类

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

class Cart with ChangeNotifier {
  // 存储购物车项,键为商品ID
  final Map<String, CartItem> _items = {};

  // 提供不可修改的购物车项视图
  UnmodifiableMapView<String, CartItem> get items => UnmodifiableMapView(_items);

  // 获取购物车项数量
  int get itemCount => _items.length;

  // 计算购物车总价
  double get totalAmount {
    var total = 0.0;
    _items.forEach((key, cartItem) {
      total += cartItem.price * cartItem.quantity;
    });
    return total;
  }

  // 添加商品到购物车
  void addItem(Product product) {
    if (_items.containsKey(product.id)) {
      // 如果商品已在购物车中,增加数量
      _items.update(
        product.id,
            (existingItem) => CartItem(
          id: existingItem.id,
          productId: existingItem.productId,
          name: existingItem.name,
          quantity: existingItem.quantity + 1,
          price: existingItem.price,
        ),
      );
    } else {
      // 如果商品不在购物车中,添加新项
      _items.putIfAbsent(
        product.id,
            () => CartItem(
          id: DateTime.now().toString(),
          productId: product.id,
          name: product.name,
          quantity: 1,
          price: product.price,
        ),
      );
    }
    notifyListeners();
  }

  // 从购物车中移除商品
  void removeItem(String productId) {
    _items.remove(productId);
    notifyListeners();
  }

  // 减少购物车中商品的数量
  void removeSingleItem(String productId) {
    if (!_items.containsKey(productId)) {
      return;
    }

    if (_items[productId]!.quantity > 1) {
      _items.update(
        productId,
            (existingItem) => CartItem(
          id: existingItem.id,
          productId: existingItem.productId,
          name: existingItem.name,
          quantity: existingItem.quantity - 1,
          price: existingItem.price,
        ),
      );
    } else {
      _items.remove(productId);
    }
    notifyListeners();
  }

  // 清空购物车
  void clear() {
    _items.clear();
    notifyListeners();
  }
}

3. 创建商品列表状态类

class Products with ChangeNotifier {
  final List<Product> _items = [
    Product(
      id: 'p1',
      name: 'Red Shirt',
      price: 29.99,
      imageUrl: 'https://picsum.photos/200/300?random=1',
    ),
    Product(
      id: 'p2',
      name: 'Trousers',
      price: 59.99,
      imageUrl: 'https://picsum.photos/200/300?random=2',
    ),
    Product(
      id: 'p3',
      name: 'Yellow Scarf',
      price: 19.99,
      imageUrl: 'https://picsum.photos/200/300?random=3',
    ),
    Product(
      id: 'p4',
      name: 'A Shoes',
      price: 99.99,
      imageUrl: 'https://picsum.photos/200/300?random=4',
    ),
  ];

  // 获取商品列表
  List<Product> get items => [..._items];
}

4. 在应用中提供状态

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (ctx) => Products()),
        ChangeNotifierProvider(create: (ctx) => Cart()),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shopping Cart Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const ProductsOverviewScreen(),
      routes: {
        CartScreen.routeName: (ctx) => const CartScreen(),
      },
    );
  }
}

5. 实现商品列表页面

class ProductsOverviewScreen extends StatelessWidget {
  const ProductsOverviewScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // 获取商品列表
    final productsData = Provider.of<Products>(context);
    final products = productsData.items;

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Shop'),
        actions: [
          // 购物车图标,显示商品数量
          Consumer<Cart>(
            builder: (ctx, cart, ch) =>
                Badge(label: Text(cart.itemCount.toString()), child: ch!),
            child: IconButton(
              icon: const Icon(Icons.shopping_cart),
              onPressed: () {
                Navigator.of(context).pushNamed(CartScreen.routeName);
              },
            ),
          ),
        ],
      ),
      body: GridView.builder(
        padding: const EdgeInsets.all(10.0),
        itemCount: products.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 3 / 2,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        itemBuilder: (ctx, i) => ProductItem(products[i]),
      ),
    );
  }
}

class ProductItem extends StatelessWidget {
  final Product product;

  const ProductItem(this.product, {super.key});

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(10),
      child: GridTile(
        footer: GridTileBar(
          backgroundColor: Colors.black87,
          title: Text(product.name, textAlign: TextAlign.center),
          trailing: IconButton(
            icon: const Icon(Icons.shopping_cart),
            onPressed: () {
              // 将商品添加到购物车
              Provider.of<Cart>(context, listen: false).addItem(product);
              // 显示添加成功提示
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: const Text('Added item to cart!'),
                  duration: const Duration(seconds: 2),
                  action: SnackBarAction(
                    label: 'UNDO',
                    onPressed: () {
                      Provider.of<Cart>(
                        context,
                        listen: false,
                      ).removeSingleItem(product.id);
                    },
                  ),
                ),
              );
            },
            color: Theme.of(context).colorScheme.secondary,
          ),
        ),
        child: Image.network(product.imageUrl, fit: BoxFit.cover),
      ),
    );
  }
}

6. 实现购物车页面

class CartScreen extends StatelessWidget {
  static const routeName = '/cart';

  const CartScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<Cart>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Cart'),
      ),
      body: Column(
        children: [
          Card(
            margin: const EdgeInsets.all(15),
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text('Total', style: TextStyle(fontSize: 20)),
                  const Spacer(),
                  Chip(
                    label: Text(
                      '$${cart.totalAmount.toStringAsFixed(2)}',
                      style: TextStyle(
                        color: Theme.of(context).primaryTextTheme.titleLarge?.color,
                      ),
                    ),
                    backgroundColor: Theme.of(context).primaryColor,
                  ),
                  TextButton(
                    onPressed: () {
                      // 这里可以添加结算逻辑
                    },
                    child: const Text('ORDER NOW'),
                  )
                ],
              ),
            ),
          ),
          const SizedBox(height: 10),
          Expanded(
            child: ListView.builder(
              itemCount: cart.items.length,
              itemBuilder: (ctx, i) {
                final cartItem = cart.items.values.toList()[i];
                return ListTile(
                  leading: CircleAvatar(
                    child: FittedBox(
                      child: Text('$${cartItem.price}'),
                    ),
                  ),
                  title: Text(cartItem.name),
                  subtitle: Text('Total: $${(cartItem.price * cartItem.quantity).toStringAsFixed(2)}'),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove),
                        onPressed: () {
                          cart.removeSingleItem(cartItem.productId);
                        },
                      ),
                      Text('${cartItem.quantity}'),
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () {
                          // 这里可以添加增加商品数量的逻辑
                          // 简化示例中暂不实现
                        },
                      ),
                    ],
                  ),
                );
              },
            ),
          )
        ],
      ),
    );
  }
}


六、Provider 高级用法

1. Selector 优化重建

Selector 是 Consumer 的进阶版本,它可以根据指定的条件决定是否重建,进一步优化性能:

Selector<Counter, int>(
  // 选择需要监听的状态
  selector: (context, counter) => counter.count,
  // 只有当选中的状态变化时才会重建
  builder: (context, count, child) {
    return Text('Count: $count');
  },
)

Selector 接收两个泛型参数:第一个是 ChangeNotifier 类型,第二个是需要监听的状态类型。只有当 selector 方法返回的值发生变化时,builder 才会被调用。

2. 状态持久化

结合 shared_preferences 或 hive 等本地存储库,可以实现状态的持久化:

class Counter with ChangeNotifier {
  int _count = 0;
  final SharedPreferences _prefs;

  Counter(this._prefs) {
    // 从本地存储加载状态
    _count = _prefs.getInt('count') ?? 0;
  }

  int get count => _count;

  void increment() {
    _count++;
    // 保存状态到本地存储
    _prefs.setInt('count', _count);
    notifyListeners();
  }
}

// 提供带持久化的状态
ChangeNotifierProvider(
  create: (context) => Counter(SharedPreferences.getInstance()),
  child: MyApp(),
)

3. 状态封装与业务逻辑分离

对于复杂应用,建议将业务逻辑与状态管理分离,保持 ChangeNotifier 的简洁:

// 业务逻辑层
class CartService {
  Future<void> addToCart(Product product) async {
    // 处理添加到购物车的业务逻辑,如网络请求等
    await Future.delayed(const Duration(milliseconds: 300));
  }
}

// 状态管理层
class Cart with ChangeNotifier {
  final CartService _cartService;
  final List<CartItem> _items = [];

  Cart(this._cartService);

  List<CartItem> get items => [..._items];

  Future<void> addItem(Product product) async {
    try {
      // 调用业务逻辑
      await _cartService.addToCart(product);
      // 更新状态
      _items.add(CartItem(...));
      notifyListeners();
    } catch (e) {
      // 处理错误
      rethrow;
    }
  }
}


七、Provider 使用最佳实践

  1. 状态粒度适中:避免创建过大的状态类,应根据功能模块拆分状态
  2. 最小重建原则:使用 Consumer 和 Selector 限制重建范围,避免不必要的重建
  3. 单一职责:每个状态类应只负责管理相关的一组状态,遵循单一职责原则
  4. 不可变数据:在状态类中,对于复杂数据结构,建议使用不可变对象,通过替换整个对象来更新状态
  5. 避免在 build 方法中创建状态:确保 ChangeNotifier 实例的创建在 create 方法中,而不是在 build 方法中
  6. 清理资源:如果状态类中使用了需要手动释放的资源(如定时器),应在 dispose 方法中清理
class TimerModel with ChangeNotifier {
  late Timer _timer;
  int _seconds = 0;

  TimerModel() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _seconds++;
      notifyListeners();
    });
  }

  int get seconds => _seconds;

  @override
  void dispose() {
    // 清理资源
    _timer.cancel();
    super.dispose();
  }
}

从简易到通用:FunctionThrottleDebounce 升级全记录(支持同步 & 异步、任意参数、可取消)

作者 苏元
2025年8月11日 18:08

从简易到通用:FunctionThrottleDebounce 升级全记录(支持同步 & 异步、任意参数、可取消)

目的:由于原来的扩展存在潜在的问题,为了方便后期维护,于是进行优化!

项目中原来的版本:

import 'dart:async';

import 'package:flutter/material.dart';

extension FunctionExtension on Function {
  VoidCallback throttle() {
    return FunctionProxy(this).throttle;
  }

  VoidCallback throttleWithTimeout({int? timeout}) {
    return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
  }

  VoidCallback debounce({int? timeout}) {
    return FunctionProxy(this, timeout: timeout).debounce;
  }
}

class FunctionProxy {
  static final Map<String, bool> _funcThrottle = {};
  static final Map<String, Timer> _funcDebounce = {};
  final Function? target;

  final int timeout;

  FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

  void throttle() async {
    String key = hashCode.toString();
    bool enable = _funcThrottle[key] ?? true;
    if (enable) {
      _funcThrottle[key] = false;
      try {
        await target?.call();
      } catch (e) {
        rethrow;
      } finally {
        _funcThrottle.remove(key);
      }
    }
  }

  void throttleWithTimeout() {
    String key = hashCode.toString();
    bool enable = _funcThrottle[key] ?? true;
    if (enable) {
      _funcThrottle[key] = false;
      Timer(Duration(milliseconds: timeout), () {
        _funcThrottle.remove(key);
      });
      target?.call();
    }
  }

  void debounce() {
    String key = hashCode.toString();
    Timer? timer = _funcDebounce[key];
    timer?.cancel();
    timer = Timer(Duration(milliseconds: timeout), () {
      Timer? t = _funcDebounce.remove(key);
      t?.cancel();
      target?.call();
    });
    _funcDebounce[key] = timer;
  }
}

潜在问题

这样的实现有几个可能踩坑的地方:

1. hashCode 用法有风险

  • hashCode.toString() 作为 Map key,但这里的 hashCodeFunctionProxy 实例的哈希值,而不是原始 target 函数的哈希值。

  • 每次调用 throttle() 时,都会 new FunctionProxy(...),所以 hashCode 变了 → 导致同一个函数节流/防抖不起作用。

    也就是说,现在这个实现可能根本没法限制住相同的函数,因为每次 key 都不一样。

  • 如果想正确限制某个函数,key 应该跟 target 函数对象本身绑定,比如:

    String key = target.hashCode.toString();
    

    或者用 identityHashCode(target)


2. throttle() 异步执行时会立即释放锁

  • 这里:

    await target?.call();
    _funcThrottle.remove(key);
    

    如果 target 是异步的,执行完才释放锁没问题,但如果是同步的,就等于立刻释放了锁 → 节流时间其实为 0。

    • 一般节流会用 Timer 固定一段时间后再释放。

3. 防抖(debounce)调用时丢失参数

  • 调用 target?.call() 时,没有把传入参数传递过去,所以如果原函数需要参数,这个防抖会执行失败或者不符合预期。
    • 正确做法应该是用 Function.apply(target!, args) 传参数。

4. 类型安全缺失

  • 现在所有地方都用 Function,没有泛型支持,会导致编译器无法检查参数和返回类型,运行时才报错。

  • 可以把 FunctionProxy 改成泛型,比如:

    class FunctionProxy<T extends Function> {
      final T target;
      ...
    }
    

5. 全局 Map 会导致内存泄漏

  • _funcThrottle_funcDebounce 是静态的,理论上应该在合适的时机清理。
  • 如果的函数是一次性的,但 key 永远不清除,就会一直占用内存。

修改后的版本:

import 'dart:async';

/**
    使用示例和场景说明:

    1. 普通节流 throttle
    -------------------------
    用法:
    final throttledFn = someFunction.throttle(milliseconds: 800);
    throttledFn(); // 触发调用,800ms 内再次调用会被忽略

    场景:
    - 按钮防止重复点击提交
    - 避免短时间内多次触发耗资源事件

    2. 立即执行节流 throttleWithTimeout
    -------------------------
    用法:
    final throttledImmediateFn = someFunction.throttleWithTimeout(milliseconds: 800);
    throttledImmediateFn(); // 立即执行,之后 800ms 内忽略调用

    场景:
    - 搜索输入框实时搜索,首次立即发请求,短时间内不重复请求
    - UI 动画首次立即触发,防止连续快速触发

    3. 可取消节流 throttleCancellable
    -------------------------
    用法:
    final (fn, cancel) = someFunction.throttleCancellable(milliseconds: 800);
    fn(); // 调用
    cancel(); // 取消冷却状态,允许立即再次调用

    场景:
    - 网络请求节流,用户操作中断时提前允许重新触发
    - 复杂多步骤任务,可根据业务主动取消节流限制

    4. 普通防抖 debounce
    -------------------------
    用法:
    final debouncedFn = someFunction.debounce(milliseconds: 500);
    debouncedFn(args); // 多次调用,只有最后一次在 500ms 后执行

    场景:
    - 输入框实时搜索,用户停止输入后再发请求
    - 调整窗口大小,等调整完成后才触发计算

    5. 立即执行防抖 debounceImmediate
    -------------------------
    用法:
    final debouncedImmediateFn = someFunction.debounceImmediate(milliseconds: 500);
    debouncedImmediateFn(); // 第一次立即执行,之后 500ms 内忽略调用

    场景:
    - 表单验证第一次立即提示,短期内不重复弹窗
    - 重要事件第一次响应,之后忽略

    6. 可取消防抖 debounceCancellable
    -------------------------
    用法:
    final (fn, cancel) = someFunction.debounceCancellable(milliseconds: 500);
    fn(args); // 调用
    cancel(); // 取消未触发的调用

    场景:
    - 实时搜索中,用户切换页面取消搜索请求
    - 输入框清空时取消等待执行的搜索请求

    -------------------------
    所有方法支持同步和异步函数,支持无参数和多参数调用。

    使用时建议根据场景选择合适方法,常见选择:
    - 普通按钮防重复点击用 throttle
    - 输入框实时搜索用 debounce
    - 需要立即触发且节流用 throttleWithTimeout
    - 需要手动控制取消时用可取消版本

 */

/// 扩展 Function,提供通用节流、防抖方法
/// 特点:
/// 1. 支持任意参数(无参数、单参数、多参数)
/// 2. 支持同步 & 异步函数
/// 3. 所有方法返回的函数可直接作为事件回调(免 as 转换)
/// 4. 部分方法支持手动取消
extension FunctionThrottleDebounce on Function {
  //============================ 节流类 ============================

  /// 1. 普通节流(延迟执行版)
  /// 调用场景:按钮点击、短时间内重复触发的事件
  /// 原理:第一次触发立即执行,之后在 [milliseconds] 毫秒内屏蔽再次触发
  /// 不支持手动取消,适用于生命周期短的场景(如单次页面会话)
  F throttle<F extends Function>({int milliseconds = 500}) {
    bool enable = true;

    return (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      try {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      } finally {
        Timer(Duration(milliseconds: milliseconds), () {
          enable = true;
        });
      }
    }) as F;
  }

  /// 2. 立即执行节流(带超时节流)
  /// 调用场景:需要第一次立刻触发,之后进入冷却期
  /// 例如:搜索请求第一次就发,后面一段时间内不再发
  F throttleWithTimeout<F extends Function>({int milliseconds = 500}) {
    bool enable = true;

    return (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      if (args == null) {
        await Function.apply(this, []);
      } else if (args is List) {
        await Function.apply(this, args);
      } else {
        await Function.apply(this, [args]);
      }

      Timer(Duration(milliseconds: milliseconds), () {
        enable = true;
      });
    }) as F;
  }

  /// 3. 可取消节流(延迟执行版)
  /// 调用场景:与 [throttle] 类似,但可在外部取消冷却状态
  /// 适用于任务提前结束、需要立即允许下一次执行的场景
  /// 调用返回值: (wrappedFn, cancelFn)
  (F, void Function()) throttleCancellable<F extends Function>(
      {int milliseconds = 500}) {
    bool enable = true;
    Timer? timer;

    void cancel() {
      timer?.cancel();
      enable = true;
    }

    final wrapped = (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      try {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      } finally {
        timer = Timer(Duration(milliseconds: milliseconds), () {
          enable = true;
        });
      }
    }) as F;

    return (wrapped, cancel);
  }

  //============================ 防抖类 ============================

  /// 4. 普通防抖(延迟执行版)
  /// 调用场景:输入框实时搜索、窗口调整大小事件
  /// 原理:重复调用会重置定时器,直到最后一次调用才执行
  F debounce<F extends Function>({int milliseconds = 500}) {
    Timer? timer;

    return (([dynamic args]) {
      timer?.cancel();

      timer = Timer(Duration(milliseconds: milliseconds), () async {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      });
    }) as F;
  }

  /// 5. 立即执行防抖(Leading Debounce)
  /// 调用场景:第一次调用立即执行,后续调用在 [milliseconds] 内忽略
  /// 例如:表单验证第一次立即提示,短期内不重复提示
  F debounceImmediate<F extends Function>({int milliseconds = 500}) {
    Timer? timer;
    bool firstCall = true;

    return (([dynamic args]) {
      if (firstCall) {
        firstCall = false;
        if (args == null) {
          Function.apply(this, []);
        } else if (args is List) {
          Function.apply(this, args);
        } else {
          Function.apply(this, [args]);
        }
      }
      timer?.cancel();
      timer = Timer(Duration(milliseconds: milliseconds), () {
        firstCall = true;
      });
    }) as F;
  }

  /// 6. 可取消防抖(延迟执行版)
  /// 调用场景:与 [debounce] 类似,但可在外部取消未触发的调用
  /// 适用于用户取消输入、提前终止操作的场景
  /// 调用返回值: (wrappedFn, cancelFn)
  (F, void Function()) debounceCancellable<F extends Function>(
      {int milliseconds = 500}) {
    Timer? timer;

    void cancel() {
      timer?.cancel();
    }

    final wrapped = (([dynamic args]) {
      timer?.cancel();

      timer = Timer(Duration(milliseconds: milliseconds), () async {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      });
    }) as F;

    return (wrapped, cancel);
  }
}

升级后的版本: ✅ 同步 + 异步通用 —— 同一套 API 直接支持 voidFuture 函数 ✅ 任意参数支持 —— 无参数、单参数、多参数全都直接调用 ✅ 可取消机制 —— 支持在需要时主动终止计时器,避免内存泄漏和无意义调用 ✅ 调用姿势一致 —— UI 层和业务层都能无缝使用

这样,在网络请求、表单提交、按钮点击等场景下,不管是 Flutter UI 事件还是业务逻辑函数,都能保持统一的调用体验,并且更健壮。

Flutter开发环境安装指南

作者 卢叁
2025年8月11日 11:39

Flutter开发环境安装指南

📋 目录

🖥️ 系统要求

macOS版本要求

  • 最低版本: macOS 10.14 (Mojave) 或更高版本

  • 推荐版本: macOS 11.0 (Big Sur) 或更高版本

  • 最新版本: macOS 14.0 (Sonoma) 或更高版本

硬件要求

  • 处理器: Intel Core i5 或 Apple Silicon (M1/M2/M3)

  • 内存: 至少 8GB RAM (推荐 16GB)

  • 磁盘空间: 至少 15GB 可用空间 (推荐 20GB)

  • 网络: 稳定的网络连接用于下载SDK和依赖

推荐配置

  • 处理器: Apple Silicon (M1/M2/M3) 芯片
  • 内存: 16GB 或更高
  • 存储: SSD 硬盘,50GB 可用空间
  • 网络: 高速网络连接

注:若能科学上网最好,否则安装相应镜像

📥 安装步骤

1. 安装Homebrew (如果未安装)

# 安装Homebrew包管理器
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 验证安装
brew --version

若提示失败,按以下操作:

sudo vim /etc/hosts
185.199.108.133 raw.githubusercontent.com

2. 安装Flutter SDK

方法一: 使用Homebrew安装 (推荐)
# 使用Homebrew安装Flutter
brew install --cask flutter

# 验证安装
flutter --version
方法二: 手动下载安装
# 创建开发目录
mkdir -p ~/development
cd ~/development

# 下载Flutter SDK
curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.x.x-stable.zip

# 解压Flutter SDK
unzip flutter_macos_3.x.x-stable.zip

# 验证安装
~/development/flutter/bin/flutter --version

3. 配置环境变量

配置Shell环境
# 确定你使用的Shell
echo $SHELL

# 如果使用zsh (macOS Catalina及以后版本的默认Shell)
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc
source ~/.zshrc

# 如果使用bash
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.bash_profile
source ~/.bash_profile

# 验证PATH配置
echo $PATH | grep flutter

4. 安装依赖工具

安装Xcode
# 从App Store安装Xcode
# 或使用命令行安装
xcode-select --install

# 验证Xcode安装
xcode-select --print-path
安装Git
# 使用Homebrew安装Git
brew install git

# 或从官网下载: https://git-scm.com/download/mac

# 配置Git用户信息
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
安装Android Studio
# 使用Homebrew安装Android Studio
brew install --cask android-studio

# 或从官网下载: https://developer.android.com/studio
安装CocoaPods
# 安装CocoaPods (iOS开发必需)
sudo gem install cocoapods

# 验证安装
pod --version

⚙️ 环境配置

1. 配置iOS开发环境

配置Xcode
# 设置Xcode命令行工具路径
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer

# 接受Xcode许可协议
sudo xcodebuild -license accept

# 验证Xcode配置
xcodebuild -version
安装iOS模拟器
# 打开Xcode
open -a Xcode

# 在Xcode中安装模拟器:
# 1. 打开 Xcode > Window > Devices and Simulators
# 2. 选择 Simulators 标签
# 3. 点击 "+" 按钮添加新的模拟器
# 4. 选择设备类型和iOS版本
配置CocoaPods
# 初始化CocoaPods
pod setup

# 验证CocoaPods配置
pod --version

2. 配置Android开发环境

安装Android SDK
# 打开Android Studio
open -a "Android Studio"

# 在Android Studio中安装SDK:
# 1. 打开 Android Studio > Preferences > Appearance & Behavior > System Settings > Android SDK
# 2. 安装以下组件:
#    - Android SDK Platform-Tools
#    - Android SDK Build-Tools (最新版本)
#    - Android SDK Platform (API 33+)
#    - Android SDK Command-line Tools
#    - Android Emulator
配置Android环境变量(可跳过)
# 编辑Shell配置文件
# 如果使用zsh
echo 'export ANDROID_HOME=$HOME/Library/Android/sdk' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/tools' >> ~/.zshrc
echo 'export PATH=$PATH:$ANDROID_HOME/emulator' >> ~/.zshrc

# 如果使用bash
echo 'export ANDROID_HOME=$HOME/Library/Android/sdk' >> ~/.bash_profile
echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools' >> ~/.bash_profile
echo 'export PATH=$PATH:$ANDROID_HOME/tools' >> ~/.bash_profile
echo 'export PATH=$PATH:$ANDROID_HOME/emulator' >> ~/.bash_profile

# 重新加载配置
source ~/.zshrc  # 或 source ~/.bash_profile

3. 配置Flutter

# 运行Flutter doctor检查环境
flutter doctor

# 接受Android许可协议
flutter doctor --android-licenses

# 升级Flutter到最新版本
flutter upgrade

# 配置Flutter镜像源 (中国用户)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

🛠️ IDE配置

1. Android Studio (强烈推荐)

安装Android Studio
# 使用Homebrew安装Android Studio
brew install --cask android-studio

# 或从官网下载: https://developer.android.com/studio
# 下载macOS版本并安装
首次启动配置
# 1. 启动Android Studio
open -a "Android Studio"

# 2. 完成初始设置向导:
#    - 选择 "Standard" 安装类型
#    - 选择UI主题 (推荐 "Darcula" 深色主题)
#    - 等待下载和安装Android SDK
安装Flutter插件
# 在Android Studio中安装必需插件:
# 1. 打开 Android Studio > Preferences (或按 Cmd+,)
# 2. 选择 Plugins
# 3. 搜索并安装以下插件:
#    - Flutter Plugin (官方插件,必需)
#    - Dart Plugin (官方插件,必需)
#    - Flutter Enhancement Suite (推荐)
#    - Flutter Widget Snippets (推荐)
#    - Awesome Flutter Snippets (推荐)
# 4. 重启Android Studio
配置Flutter SDK路径
# 在Android Studio中配置Flutter SDK:
# 1. 打开 Android Studio > Preferences
# 2. 选择 Languages & Frameworks > Flutter
# 3. 设置Flutter SDK路径:
#    - 如果使用Homebrew安装: /opt/homebrew/bin/flutter
#    - 如果手动安装: ~/development/flutter
# 4. 点击 "Apply""OK"
配置Android SDK
# 在Android Studio中配置Android SDK:
# 1. 打开 Android Studio > Preferences
# 2. 选择 Appearance & Behavior > System Settings > Android SDK
# 3. 在 SDK Platforms 标签页安装:
#    - Android 14.0 (API 34)
#    - Android 13.0 (API 33)
#    - Android 12.0 (API 31)
# 4. 在 SDK Tools 标签页安装:
#    - Android SDK Build-Tools
#    - Android SDK Platform-Tools
#    - Android SDK Command-line Tools
#    - Android Emulator
#    - Intel x86 Emulator Accelerator (HAXM installer)
创建Android虚拟设备 (AVD)
# 在Android Studio中创建模拟器:
# 1. 打开 Tools > AVD Manager
# 2. 点击 "Create Virtual Device"
# 3. 选择设备类型 (推荐 Pixel 7)
# 4. 选择系统镜像 (推荐 API 34)
# 5. 配置AVD名称和设置
# 6. 点击 "Finish"
Android Studio优化设置
// 在Android Studio中优化设置:
// 1. 打开 Preferences > Editor > General
// 2. 启用 "Auto Import" 和 "Optimize imports on the fly"
// 3. 在 Preferences > Editor > Code Style > Dart 中设置:
//    - Line separator: Unix and OS X (\n)
//    - Right margin: 80
// 4. 在 Preferences > Editor > Inspections 中启用:
//    - Dart > General > Unused import
//    - Dart > General > Unused local variable

2. VS Code (备选方案)

安装VS Code
# 使用Homebrew安装VS Code
brew install --cask visual-studio-code

# 或从官网下载: https://code.visualstudio.com/
安装扩展
# 打开VS Code,安装以下必需扩展:
# 1. Flutter (官方扩展)
# 2. Dart (官方扩展)
# 3. Flutter Widget Snippets
# 4. Awesome Flutter Snippets
# 5. Flutter Intl (国际化支持)
# 6. Error Lens (错误提示增强)
配置设置
// 在VS Code中按 Cmd+Shift+P,输入 "Preferences: Open Settings (JSON)"
{
    "dart.flutterSdkPath": "/opt/homebrew/bin/flutter",
    "dart.sdkPath": "/opt/homebrew/bin/cache/dart-sdk",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
        "source.fixAll": true
    },
    "dart.lineLength": 80,
    "dart.previewFlutterUiGuides": true,
    "dart.previewFlutterUiGuidesCustomTracking": true
}

3. Xcode (iOS开发必需)

配置Xcode
# 确保Xcode已正确安装和配置
# 在Xcode中安装iOS模拟器:
# 1. 打开 Xcode > Window > Devices and Simulators
# 2. 选择 Simulators 标签
# 3. 添加所需的iOS模拟器:
#    - iPhone 15 Pro (推荐)
#    - iPhone 14
#    - iPad Pro (11-inch)

🎯 为什么推荐Android Studio?

Android Studio的优势

  1. 官方支持: Flutter官方推荐的IDE,获得最佳支持

  2. 完整工具链: 集成Android SDK、模拟器、调试工具

  3. 智能提示: 强大的代码补全和错误检测

  4. 可视化调试: 内置Flutter Inspector和性能分析工具

  5. 热重载: 完美的热重载支持,开发效率高

  6. 插件生态: 丰富的Flutter和Dart插件

  7. 多平台支持: 同时支持iOS、Android、Web开发

  8. 团队协作: 适合团队开发,配置统一

与VS Code的对比

特性 Android Studio VS Code
官方支持 ✅ 官方推荐 ⚠️ 社区支持
开箱即用 ✅ 配置简单 ⚠️ 需要额外配置
性能分析 ✅ 内置工具 ⚠️ 需要插件
调试体验 ✅ 优秀 ✅ 良好
资源占用 ⚠️ 较高 ✅ 较低
启动速度 ⚠️ 较慢 ✅ 快速

✅ 验证安装

1. 运行Flutter Doctor

flutter doctor -v

**预期输出: **

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.x.x, on macOS 14.0, locale zh-CN)
[✓] macOS Version (Installed version of macOS is version 10.14 or higher)
[✓] Xcode - develop for iOS and macOS (Xcode 15.0)
[✓] Chrome - develop for the web (Chrome version 119.0.6045.105)
[✓] Android Studio (version 2023.1.1)
[✓] VS Code (version 1.84.2)
[✓] Connected device (3 available)
[✓] Network resourcesNo issues found!

注意: Android Studio 是Flutter官方推荐的IDE,提供最完整的Flutter开发体验。


### 2. 创建测试项目
```bash
# 创建新项目
flutter create my_first_app

# 进入项目目录
cd my_first_app

# 运行项目
flutter run

3. 测试不同平台

iOS (推荐优先测试)
# 启动iOS模拟器
open -a Simulator

# 运行iOS应用
flutter run -d ios

# 或指定特定模拟器
flutter devices  # 查看可用设备
flutter run -d "iPhone 15 Pro"  # 指定设备
Android
# 启动Android模拟器
# 在Android Studio中: Tools > AVD Manager > 启动模拟器

# 或使用命令行启动
emulator -avd Pixel_7_API_34  # 替换为你的AVD名称

# 运行Android应用
flutter run -d android
macOS桌面应用
# 启用macOS桌面支持
flutter config --enable-macos-desktop

# 运行macOS应用
flutter run -d macos
Web
# 启动Web开发服务器
flutter run -d chrome

# 或使用Safari
flutter run -d web-server
# 然后在Safari中访问 http://localhost:8080

🔧 常见问题

1. Flutter Doctor 显示问题

Xcode问题
# 解决方案
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept

# 如果仍有问题,尝试重新安装Xcode命令行工具
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install
Android License问题
# 解决方案
flutter doctor --android-licenses
# 对所有提示输入 'y' 接受
CocoaPods问题
# 如果CocoaPods安装失败
sudo gem uninstall cocoapods
sudo gem install cocoapods -n /usr/local/bin

# 或使用Homebrew安装
brew install cocoapods

# 初始化CocoaPods
pod setup
Git问题
# 确保Git已正确安装
git --version

# 配置Git用户信息
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

2. 网络连接问题

设置镜像源 (中国用户)
# 设置Flutter镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

# 或添加到环境变量文件
echo 'export PUB_HOSTED_URL=https://pub.flutter-io.cn' >> ~/.bash_profile
echo 'export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn' >> ~/.bash_profile
代理设置
# 设置HTTP代理
export HTTP_PROXY=http://proxy.example.com:8080
export HTTPS_PROXY=http://proxy.example.com:8080

# 设置Flutter代理
flutter config --proxy http://proxy.example.com:8080

3. 权限问题

macOS权限问题
# 修复Flutter目录权限
sudo chown -R $(whoami) ~/development/flutter
chmod +x ~/development/flutter/bin/flutter

# 修复Homebrew权限问题
sudo chown -R $(whoami) /opt/homebrew
sudo chown -R $(whoami) /usr/local

# 如果使用Intel Mac
sudo chown -R $(whoami) /usr/local
安全设置问题
# 如果遇到"无法验证开发者"错误
# 1. 打开 系统偏好设置 > 安全性与隐私
# 2. 点击"仍要打开""允许"
# 3. 或在终端中运行
sudo spctl --master-disable

4. 依赖下载失败

# 清理缓存
flutter clean
flutter pub cache clean

# 重新获取依赖
flutter pub get

# 如果仍有问题,尝试
flutter pub cache repair

🚀 下一步

1. 学习资源

2. 开发工具

3. 社区资源

4. 创建第一个应用

# 创建新项目
flutter create my_app

# 运行项目
cd my_app
flutter run

# 开始开发!

📝 注意事项

  1. 定期更新: 保持Flutter SDK和相关工具的最新版本

  2. 备份配置: 备份重要的环境配置和项目文件

  3. 网络稳定: 确保网络连接稳定,特别是在下载SDK和依赖时

  4. 权限管理: 注意文件系统权限,避免权限相关的问题

  5. 版本兼容: 确保Flutter、Dart和相关工具的版本兼容性

  6. macOS 特定:

    1. 确保Xcode版本与macOS版本兼容
    2. 定期更新Xcode和命令行工具
    3. 注意Apple Silicon和Intel芯片的差异
  7. 存储空间: 确保有足够的磁盘空间,Xcode和模拟器会占用大量空间

  8. 安全设置: 在macOS安全设置中允许开发者工具运行

🆘 获取帮助

如果在安装过程中遇到问题,可以通过以下方式获取帮助:


祝您在 macOS 上Flutter开发愉快! 🎉


🍎 macOS特有优势

使用macOS进行Flutter开发的优势:

  1. 原生iOS开发: 可以直接开发和测试iOS应用
  2. 性能优化: Apple Silicon芯片提供优秀的性能表现
  3. 工具集成: 与Xcode、Safari等原生工具完美集成
  4. 多平台支持: 可以同时开发iOS、Android、Web和macOS应用
  5. 开发体验: macOS提供了优秀的开发体验和工具链

Happy Flutter Development on macOS ! 🚀

flutter_flavorizr 多渠道打包、多环境打包利器,不需要再一个个手动配置了

2025年8月10日 11:25
在 app 开发中,测试工程师经常会说,我要在一个手机上同时安装测试环境、预发布环境、正式环境的包,又或者一套代码上架多个 app,怎么搞,手动配置多个环境过程繁琐。怎么搞?无意间看到 flutter
❌
❌