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

基于Riverpod实现待办App一对多关系的异步初始化问题

基于Riverpod实现待办App的Groups与Tasks一对多关系

我要实现一个简易待办App,包含Groups和Tasks两个实体,一个Group对应多个Task。已实现两个Riverpod Provider,使用sqflite操作SQLite数据库。但因数据库读取是异步任务,在_TasksInCardState中首次调用时tasks为空,需解决基于Riverpod的一对多关系实现问题。

现有Provider代码

groups_provider.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo/models/group.dart';
import 'package:todo/db/db_helper.dart' as dbHelper;
import 'package:todo/providers/tasks_provider.dart';

class GroupsNotifier extends StateNotifier<List<Group>> {
  GroupsNotifier() : super([]);

  void loadGroups() async {
    state = await dbHelper.getAllGroups();
  }

  void addGroup(String title, Color color) async {
    final newId = await dbHelper.addGroup(title, color);
    final newGroup = Group(id: newId, title: title, color: color);

    state = [...state, newGroup];
  }
}

final groupsProvider =
    StateNotifierProvider<GroupsNotifier, List<Group>>((ref) {
  return GroupsNotifier();
});

tasks_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo/models/group.dart';
import 'package:todo/models/task.dart';
import 'package:todo/db/db_helper.dart' as dbHelper;
import 'package:todo/providers/groups_provider.dart';

class TasksNotifier extends StateNotifier<Map<Group, List<Task>>> {
  TasksNotifier() : super({});

  void loadTasks(Group group) async {
    state = {...state, group: await dbHelper.getTasksByGroup(group)};
  }

  void addTask(String title, Group group, bool done) async {
    final newId = await dbHelper.addTask(title, group, done);
    final newTask = Task(id: newId, title: title, group: group, done: done);

    state = {
      ...state,
      group: [...state[group] ?? [], newTask]
    };
  }

  void toggleDone(Task task) {
    final group = task.group;
    final List<Task> tasks = state[group]!;

    final index = tasks.indexOf(task);
    List<Task> newList = [];
    bool newValue = false;
    for (var i = 0; i < state.length; i++) {
      newValue = index == i ? !tasks[i].done : tasks[i].done;
      newList.add(
        Task(
            id: tasks[i].id,
            title: tasks[i].title,
            group: tasks[i].group,
            done: newValue),
      );
    }
    dbHelper.changeTaskDone(task, newValue);
    state = {...state, group: newList};
  }
}

final tasksProvider =
    StateNotifierProvider<TasksNotifier, Map<Group, List<Task>>>((ref) {
  return TasksNotifier();
});

现有UI代码片段

class _TasksInCardState extends ConsumerState<TasksInCard> {
  List<Task> tasks = [];

  @override
  void initState() {
    super.initState();
    ref.read(tasksProvider.notifier).loadTasks(widget.group);
  }

  @override
  Widget build(BuildContext context) {
    tasks = ref.watch(tasksProvider)[widget.group]??[];

    return Expanded(
      child: Directionality(
        textDirection: TextDirection.rtl,
        child: ListView.builder(
          // ... 任务列表构建逻辑
        )
      )
    );
  }
}

问题分析

当前实现中,tasksProvider初始状态为空Map,loadTasks是异步操作,首次Build时ref.watch(tasksProvider)[widget.group]返回null,导致tasks为空数组,UI会先显示空内容,直到异步加载完成后状态更新才会重新Build显示任务。同时,Group作为Map的key,若未正确实现==和hashCode,会导致无法识别同一Group对象,重复加载任务。

解决方案

1. 优化TasksNotifier,维护每个Group的异步状态

将TasksNotifier的状态改为Map<Group, AsyncValue<List<Task>>>,为每个Group单独维护加载中、成功、失败状态:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo/models/group.dart';
import 'package:todo/models/task.dart';
import 'package:todo/db/db_helper.dart' as dbHelper;

class TasksNotifier extends StateNotifier<Map<Group, AsyncValue<List<Task>>>> {
  TasksNotifier() : super({});

  void loadTasks(Group group) async {
    // 避免重复加载
    if (state.containsKey(group)) return;
    // 标记为加载中
    state = {...state, group: const AsyncLoading()};
    try {
      final tasks = await dbHelper.getTasksByGroup(group);
      state = {...state, group: AsyncData(tasks)};
    } catch (e) {
      state = {...state, group: AsyncError(e, StackTrace.current)};
    }
  }

  void addTask(String title, Group group, bool done) async {
    final newId = await dbHelper.addTask(title, group, done);
    final newTask = Task(id: newId, title: title, group: group, done: done);
    
    final currentTasks = state[group]?.value ?? [];
    state = {
      ...state,
      group: AsyncData([...currentTasks, newTask])
    };
  }

  void toggleDone(Task task) async {
    final group = task.group;
    final currentTasks = state[group]?.value ?? [];
    final index = currentTasks.indexOf(task);
    if (index == -1) return;

    final updatedTasks = List<Task>.from(currentTasks);
    final updatedTask = updatedTasks[index].copyWith(done: !updatedTasks[index].done);
    updatedTasks[index] = updatedTask;

    try {
      await dbHelper.changeTaskDone(task, updatedTask.done);
      state = {...state, group: AsyncData(updatedTasks)};
    } catch (e) {
      state = {...state, group: AsyncError(e, StackTrace.current)};
    }
  }
}

final tasksProvider = StateNotifierProvider<TasksNotifier, Map<Group, AsyncValue<List<Task>>>>((ref) {
  return TasksNotifier();
});

2. 完善Group类的相等性判断

确保Group作为Map的key能被正确识别,需实现==和hashCode:

class Group {
  final int id;
  final String title;
  final Color color;

  Group({required this.id, required this.title, required this.color});

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Group &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          title == other.title &&
          color.value == other.color.value;

  @override
  int get hashCode => id.hashCode ^ title.hashCode ^ color.value.hashCode;
}

3. 修改UI层,处理异步状态

在_TasksInCardState中使用AsyncValue.when方法处理不同状态的UI展示:

class _TasksInCardState extends ConsumerState<TasksInCard> {
  @override
  void initState() {
    super.initState();
    ref.read(tasksProvider.notifier).loadTasks(widget.group);
  }

  @override
  Widget build(BuildContext context) {
    final taskAsync = ref.watch(tasksProvider)[widget.group] ?? const AsyncLoading();

    return taskAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, st) => Center(child: Text('加载失败: $e')),
      data: (tasks) {
        return Expanded(
          child: Directionality(
            textDirection: TextDirection.rtl,
            child: tasks.isEmpty
                ? const Center(child: Text('暂无任务'))
                : ListView.builder(
                    itemCount: tasks.length,
                    itemBuilder: (context, index) {
                      final task = tasks[index];
                      return ListTile(
                        title: Text(task.title),
                        leading: Checkbox(
                          value: task.done,
                          onChanged: (_) => ref.read(tasksProvider.notifier).toggleDone(task),
                        ),
                      );
                    },
                  ),
          ),
        );
      },
    );
  }
}

4. 优化GroupsProvider初始化

让GroupsProvider自动加载分组,减少UI层手动调用:

class GroupsNotifier extends StateNotifier<List<Group>> {
  GroupsNotifier() : super([]) {
    // 初始化时自动加载分组
    loadGroups();
  }

  void loadGroups() async {
    state = await dbHelper.getAllGroups();
  }

  // ... 其他方法不变
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 14:12:00