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

基于Flutter MVVM+Riverpod:合并多Notifier为单一类的实现问询

实现方案:合并Riverpod Notifier为单一主类(贴合MVVM)

1. 定义统一的主状态类

将分散的日期加载状态、时段加载状态、用户选择信息全部封装到一个主状态类中,保留原有子状态的逻辑结构:

// 原日期状态类(保留原有逻辑)
enum BookDatesStatus { initial, loading, loaded, error }
class BookDatesState {
  final BookDatesStatus status;
  final List<DateTime> availableDates;
  final String? errorMessage;

  BookDatesState({
    required this.status,
    required this.availableDates,
    this.errorMessage,
  });

  BookDatesState copyWith({
    BookDatesStatus? status,
    List<DateTime>? availableDates,
    String? errorMessage,
  }) {
    return BookDatesState(
      status: status ?? this.status,
      availableDates: availableDates ?? this.availableDates,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}

// 原时段状态类(保留原有逻辑)
enum SlotsStatus { initial, loading, loaded, error }
class SlotsState {
  final SlotsStatus status;
  final List<String> availableSlots;
  final String? errorMessage;

  SlotsState({
    required this.status,
    required this.availableSlots,
    this.errorMessage,
  });

  SlotsState copyWith({
    SlotsStatus? status,
    List<String>? availableSlots,
    String? errorMessage,
  }) {
    return SlotsState(
      status: status ?? this.status,
      availableSlots: availableSlots ?? this.availableSlots,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}

// 统一主状态类
class AppointmentState {
  final BookDatesState datesState;
  final SlotsState slotsState;
  final DateTime? chosenDate;
  final String? chosenTime;

  AppointmentState({
    required this.datesState,
    required this.slotsState,
    this.chosenDate,
    this.chosenTime,
  });

  AppointmentState copyWith({
    BookDatesState? datesState,
    SlotsState? slotsState,
    DateTime? chosenDate,
    String? chosenTime,
  }) {
    return AppointmentState(
      datesState: datesState ?? this.datesState,
      slotsState: slotsState ?? this.slotsState,
      chosenDate: chosenDate ?? this.chosenDate,
      chosenTime: chosenTime ?? this.chosenTime,
    );
  }

  // 初始状态
  static AppointmentState initial() {
    return AppointmentState(
      datesState: BookDatesState(
        status: BookDatesStatus.initial,
        availableDates: [],
      ),
      slotsState: SlotsState(
        status: SlotsStatus.initial,
        availableSlots: [],
      ),
      chosenDate: null,
      chosenTime: null,
    );
  }
}

2. 实现单一主Notifier类

将原多个Provider/Notifier的逻辑整合到主Notifier中,统一处理数据加载、用户交互逻辑:

final appointmentNotifierProvider =
    NotifierProvider<AppointmentNotifier, AppointmentState>(
  AppointmentNotifier.new,
);

class AppointmentNotifier extends Notifier<AppointmentState> {
  // 替换为你的实际数据源(API/本地存储)
  final _dateDataSource = DateDataSource();
  final _slotDataSource = SlotDataSource();

  @override
  AppointmentState build() {
    // 初始化时自动加载可预约日期
    _loadAvailableDates();
    return AppointmentState.initial();
  }

  // 加载可预约日期(对应原futureBookingDates逻辑)
  Future<void> _loadAvailableDates() async {
    state = state.copyWith(
      datesState: state.datesState.copyWith(status: BookDatesStatus.loading),
    );
    try {
      final dates = await _dateDataSource.fetchAvailableDates();
      state = state.copyWith(
        datesState: state.datesState.copyWith(
          status: BookDatesStatus.loaded,
          availableDates: dates,
        ),
      );
    } catch (e) {
      state = state.copyWith(
        datesState: state.datesState.copyWith(
          status: BookDatesStatus.error,
          errorMessage: e.toString(),
        ),
      );
    }
  }

  // 用户选择日期,同时加载对应时段(整合原ChosenDate + AvailableTimeSlots逻辑)
  Future<void> selectDate(DateTime date) async {
    state = state.copyWith(
      chosenDate: date,
      chosenTime: null, // 切换日期清空已选时段
      slotsState: state.slotsState.copyWith(status: SlotsStatus.loading),
    );
    try {
      final slots = await _slotDataSource.fetchSlotsForDate(date);
      state = state.copyWith(
        slotsState: state.slotsState.copyWith(
          status: SlotsStatus.loaded,
          availableSlots: slots,
        ),
      );
    } catch (e) {
      state = state.copyWith(
        slotsState: state.slotsState.copyWith(
          status: SlotsStatus.error,
          errorMessage: e.toString(),
        ),
      );
    }
  }

  // 用户选择时段(对应原ChosenTime逻辑)
  void selectTime(String time) {
    state = state.copyWith(chosenTime: time);
  }

  // 重置状态(可选)
  void resetAppointment() {
    state = AppointmentState.initial();
    _loadAvailableDates();
  }
}

// 模拟数据源,替换为你的业务实现
class DateDataSource {
  Future<List<DateTime>> fetchAvailableDates() async {
    await Future.delayed(const Duration(seconds: 1));
    return [
      DateTime.now(),
      DateTime.now().add(const Duration(days: 1)),
      DateTime.now().add(const Duration(days: 2)),
    ];
  }
}

class SlotDataSource {
  Future<List<String>> fetchSlotsForDate(DateTime date) async {
    await Future.delayed(const Duration(seconds: 1));
    return date.day == DateTime.now().day
        ? ["09:00", "10:00", "14:00"]
        : ["11:00", "13:00", "15:00"];
  }
}

3. UI层精准刷新组件

通过select方法监听状态的局部变化,实现“选择日期仅刷新时段视图”的需求,避免不必要的重绘:

class AppointmentPage extends ConsumerWidget {
  const AppointmentPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text("预约服务")),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 日期选择区:仅监听datesState和chosenDate
            _DateSelector(
              state: ref.watch(appointmentNotifierProvider.select((s) => s.datesState)),
              chosenDate: ref.watch(appointmentNotifierProvider.select((s) => s.chosenDate)),
              onSelect: (date) => ref.read(appointmentNotifierProvider.notifier).selectDate(date),
            ),
            const SizedBox(height: 20),
            // 时段选择区:仅监听slotsState和chosenTime
            _TimeSlotSelector(
              state: ref.watch(appointmentNotifierProvider.select((s) => s.slotsState)),
              chosenTime: ref.watch(appointmentNotifierProvider.select((s) => s.chosenTime)),
              onSelect: (time) => ref.read(appointmentNotifierProvider.notifier).selectTime(time),
            ),
          ],
        ),
      ),
    );
  }
}

class _DateSelector extends StatelessWidget {
  final BookDatesState state;
  final DateTime? chosenDate;
  final Function(DateTime) onSelect;

  const _DateSelector({
    required this.state,
    required this.chosenDate,
    required this.onSelect,
  });

  @override
  Widget build(BuildContext context) {
    switch (state.status) {
      case BookDatesStatus.loading:
        return const CircularProgressIndicator();
      case BookDatesStatus.error:
        return Text("加载日期失败:${state.errorMessage}");
      case BookDatesStatus.loaded:
        return Wrap(
          spacing: 8,
          children: state.availableDates.map((date) {
            final isSelected = chosenDate?.isAtSameMomentAs(date) ?? false;
            return ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: isSelected ? Colors.blue : null,
              ),
              onPressed: () => onSelect(date),
              child: Text("${date.month}/${date.day}"),
            );
          }).toList(),
        );
      default:
        return const SizedBox();
    }
  }
}

class _TimeSlotSelector extends StatelessWidget {
  final SlotsState state;
  final String? chosenTime;
  final Function(String) onSelect;

  const _TimeSlotSelector({
    required this.state,
    required this.chosenTime,
    required this.onSelect,
  });

  @override
  Widget build(BuildContext context) {
    switch (state.status) {
      case SlotsStatus.loading:
        return const CircularProgressIndicator();
      case SlotsStatus.error:
        return Text("加载时段失败:${state.errorMessage}");
      case SlotsStatus.loaded:
        return Wrap(
          spacing: 8,
          children: state.availableSlots.map((slot) {
            final isSelected = chosenTime == slot;
            return ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: isSelected ? Colors.green : null,
              ),
              onPressed: () => onSelect(slot),
              child: Text(slot),
            );
          }).toList(),
        );
      default:
        return const Text("请先选择预约日期");
    }
  }
}

核心优势

  • 贴合MVVM:主Notifier作为ViewModel,统一处理业务逻辑与状态管理,UI层仅负责展示和触发事件
  • 保留原有流程:通过select监听局部状态,选择日期时仅刷新时段组件,避免整页重绘
  • 状态集中管理:所有预约相关状态统一维护,避免分散状态导致的逻辑混乱
  • 扩展性强:后续新增预约字段(如备注、用户信息),只需在主状态类中添加字段即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 14:05:20