You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中Riverpod Stream无法触发新值发射问题排查

问题

我用Flutter的Riverpod和Drift包做了一个监听当月已记录费用的简单Widget,采用Provider > Repository > DAO的架构。但在本地数据库(已通过SQLite表确认)添加或删除当月交易记录时,Widget不会更新显示最新的累计费用,Stream似乎没发射新值,调试很久都没找到问题。

预期行为:

  1. 修改transactions表中的记录(添加/删除)
  2. expensesToDateProvider发射新值
  3. Widget重建并显示新值
// Widget to display latest expenseToDate
class DebugExpensesWidget extends ConsumerWidget {
  const DebugExpensesWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final expensesAsyncValue =
        ref.watch(expensesToDateProvider(budgetDate: DateTime(DateTime.now().year, DateTime.now().month)));

    return expensesAsyncValue.when(
      data: (data) {
        return Text('Expenses to date: $data');
      },
      loading: () => CircularProgressIndicator(),
      error: (error, _) {
        return Text('Error: $error');
      },
    );
  }
}
// Provider that watches spentToDate for the month
@riverpod
Stream<double> expensesToDate(ExpensesToDateRef ref, {required DateTime budgetDate}) {
  return ref.watch(budgetsRepositoryProvider).getExpensesToDate(budgetDate);
}
// Repository to call the DAO to return total expenses to date based on a given budgetDate
Stream<double> getExpensesToDate(DateTime budgetDate) {
  return database.appDatabaseDao.calculateExpensesToDateDao(budgetDate.year, budgetDate.month);
  }
// DAO to calculate Total Expenses to date based on a given month and year
  Stream<double> calculateExpensesToDateDao(int year, int month) {
    final yearStr = year.toString();
    final monthStr = month.toString().padLeft(2, '0');

    const sql = '''
      SELECT SUM(amount) as total
      FROM transactions
      WHERE is_income = 0
      AND strftime('%m', datetime(transaction_date, 'unixepoch')) = ?
      AND strftime('%Y', datetime(transaction_date, 'unixepoch')) = ?
    ''';

    return customSelect(sql, variables: [
      Variable.withString(monthStr),
      Variable.withString(yearStr),
    ]).watch().map((rows) {
      final row = rows.first;
      final total = row.read<double?>('total') ?? 0.0;
      return total;
    });
  }
解决方案

问题核心是Drift无法自动识别自定义SQL对应的表依赖,导致customSelect的watch方法不会在transactions表变化时发射新值。需要手动指定该查询监听的表。

修复代码

修改DAO中的calculateExpensesToDateDao方法,给customSelect添加readsFrom参数,明确指定监听transactions表:

// DAO to calculate Total Expenses to date based on a given month and year
Stream<double> calculateExpensesToDateDao(int year, int month) {
  final yearStr = year.toString();
  final monthStr = month.toString().padLeft(2, '0');

  const sql = '''
    SELECT SUM(amount) as total
    FROM transactions
    WHERE is_income = 0
    AND strftime('%m', datetime(transaction_date, 'unixepoch')) = ?
    AND strftime('%Y', datetime(transaction_date, 'unixepoch')) = ?
  ''';

  // 添加readsFrom参数,指定监听transactions表
  return customSelect(sql, variables: [
    Variable.withString(monthStr),
    Variable.withString(yearStr),
  ], readsFrom: {transactions} // 替换为你实际的Transactions表实例
  ).watch().map((rows) {
    final row = rows.first;
    final total = row.read<double?>('total') ?? 0.0;
    return total;
  });
}

额外说明

  1. Drift的自动监听仅对生成式查询(如select(table).watch())生效,自定义SQL必须手动声明依赖表。
  2. 确保你的transactions表是通过Drift注解生成的(比如class Transactions extends Table {...}),此时会存在对应的transactions常量实例,直接传入readsFrom即可。
  3. 可优化Widget中的日期计算:当前在build方法内每次创建DateTime实例,可能触发不必要的Provider重建,建议提前计算:
@override
Widget build(BuildContext context, WidgetRef ref) {
  // 提前计算当月日期,避免每次build生成新实例
  final currentMonth = DateTime(DateTime.now().year, DateTime.now().month);
  final expensesAsyncValue = ref.watch(expensesToDateProvider(budgetDate: currentMonth));

  // ... 其余代码不变
}

内容的提问来源于stack exchange,提问作者harumon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:12:52