Delowar Hossain
Delowar Hossain

Reputation: 944

Dependent multiple dropdown issues using bloc in Flutter

I have a screen where there are 2 dropdown widgets. When I select first dropdown and want to show a loading widget then I am getting error.

My problems are:

  1. When I select territory then app is showing this error 'setState() or markNeedsBuild() called when widget tree was locked'.
  2. If I remove this line emit(currentState.copyWith(isDoctorLoading: true)); from _getCallCardDoctorListByTerritory method then doctor list is loaded but my selected territory reset automatically.
  3. If there is any optimization tips for UI or bloc please let me know or my bloc is too long, is this approach incorrect?

There are too many codes, please pardon me. I thought all of it should be here for better understanding.

This is my Custom Tab Widget

class GlobalCustomTab extends StatelessWidget {
  const GlobalCustomTab({
    super.key,
    required this.title,
    required this.tabs,
    required this.tabViews,
    required this.onTabChangeActions,
  });

  final String title;
  final List<Widget> tabs;
  final List<Widget> tabViews;
  final List<void Function(BuildContext)> onTabChangeActions;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: tabs.length,
      child: Builder(builder: (context) {
        final TabController tabController = DefaultTabController.of(context);
        tabController.addListener(() {
          if (!tabController.indexIsChanging) {
            _onTabChange(context, tabController.index, onTabChangeActions);
          }
        });
        return Scaffold(
          appBar: CustomAppbar(title: title),
          body: Column(
            children: [
              TabBar(
                unselectedLabelColor: AppColors.secondaryElementText,
                labelStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
                labelPadding: const EdgeInsets.symmetric(vertical: 16),
                indicatorSize: TabBarIndicatorSize.tab,
                dividerColor: Colors.transparent,
                onTap: (index) {
                  _onTabChange(context, index, onTabChangeActions);
                },
                tabs: tabs,
              ),
              Expanded(
                child: TabBarView(children: tabViews),
              ),
            ],
          ),
        );
      }),
    );
  }

  void _onTabChange(BuildContext context, int index, List<void Function(BuildContext)> onTabChangeActions) {
    if (index < onTabChangeActions.length) {
      onTabChangeActions[index](context);
    }
  }
}

This is my main page where I am using that widget

class DailyCallCardPage extends StatelessWidget {
  const DailyCallCardPage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (_) => DoctorDailyCallCardBloc()..add(CallCardDoctorListEvent()),
        ),
        BlocProvider(
          create: (_) => ChemistDailyCallCardBloc()..add(CallCardChemistListEvent()),
        ),
      ],
      child: GlobalCustomTab(
        title: title,
        tabs: const [
          Text('DOCTOR'),
          Text('CHEMIST'),
        ],
        tabViews: [
          const DoctorDailyCallCardPage(),
          const ChemistDailyCallCardPage(),
        ],
        onTabChangeActions: [
          (context) => context
              .read<DoctorDailyCallCardBloc>()
              .add(CallCardDoctorListEvent()),
          (context) => context
              .read<ChemistDailyCallCardBloc>()
              .add(CallCardChemistListEvent()),
        ],
      ),
    );
  }
}

This is Doctor Card widget which is the first tab view

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<DoctorDailyCallCardBloc, DoctorDailyCallCardState>(
      builder: (context, state) {
        if (state is DailyCallCardLoading) {
          return const Center(child: LoadingWidget());
        } else if (state is DailyCallCardError) {
          return Center(child: CustomErrorWidget(message: state.message));
        } else if (state is DailyCallCardLoaded) {
          return SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: kHorizontal, vertical: elementPadding),
            child: Column(
              children: [
                CreateCallCardWidget(),
                _buildDivider(context),
                if (state.isLoadingList!)
                  const Center(child: LoadingWidget())
                else
                  CallCardListWidget(callCardList: state.doctorPlanList),
              ],
            ),
          );
        }
        return const SizedBox.shrink();
      },
    );
  }

  Widget _buildDivider(BuildContext context) {
    return CustomDivider(
      title: 'Call Card',
      widget: Flexible(
        child: Row(
          children: [
            Image.asset(AppIcons.dateIcon),
            Expanded(
              child: CustomDatePicker(
                onChanged: (value) {
                  context.read<DoctorDailyCallCardBloc>().add(CallCardDoctorPlanListEvent(visitDate: value));
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

This is my create call card widget

class CreateCallCardWidget extends StatelessWidget {
  CreateCallCardWidget({super.key});

  final callCardKey = GlobalKey<FormState>();

  final DailyCallCardReqParams params = DailyCallCardReqParams(
      visitDate: DateFormat('yyyy-MM-dd', 'en').format(DateTime.now()),
      visitTime: DateFormat('hh:mm a', 'en').format(DateTime.now()));

  @override
  Widget build(BuildContext context) {
    return Container(
      color: AppColors.secondaryElement,
      padding: const EdgeInsets.symmetric(
          horizontal: kHorizontal, vertical: elementPadding),
      margin: const EdgeInsets.symmetric(vertical: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          customTitle(title: 'Create Call Card', fontSize: 16.sp),
          const Gap(18),
          Form(
            key: callCardKey,
            child: Column(
              children: [
                CustomDateTimeRow(
                  onDateChanged: (value) {
                    params.visitDate = value;
                  },
                  onTimeChanged: (value) {
                    params.visitTime = value;
                  },
                ),
                BlocBuilder<DoctorDailyCallCardBloc, DoctorDailyCallCardState>(
                  builder: (context, state) {
                    if (state is DailyCallCardLoaded) {
                      return Column(
                        children: [
                          Visibility(
                            visible: state.showTerritory ?? false,
                            child: CustomDropdownSearch(
                              labelText: 'Select Territory',
                              items: (f, s) => state.territoryList,
                              itemAsString: (Territory data) => '${data.territoryName}\n${data.upperTerritoryCode}',
                              onChanged: (value) {
                                context.read<DoctorDailyCallCardBloc>().add(CallCardDoctorListByTerritoryEvent(territoryCode: value!.upperTerritoryCode));
                              },
                            ),
                          ),
                          state.isDoctorLoading!
                              ? const Center(child: LoadingWidget())
                              : CustomDropdownSearch(
                                  labelText: 'Select Doctor',
                                  items: (f, s) => state.doctorList,
                                  itemAsString: (Doctor data) => data.name!,
                                  onChanged: (value) {
                                    params.doctorID = value!.doctorId;
                                  },
                                ),
                          CustomTextFormField(
                            hintText: 'Write a comment here....',
                            maxLines: 2,
                            onChanged: (value) {
                              params.opinion = value;
                            },
                          ),
                          state.isSettingPlan!
                              ? const ThreeDotLoading()
                              : CustomButton(
                                  title: 'Set Plan',
                                  onPressed: () {
                                    if (callCardKey.currentState!.validate()) {
                                      context
                                          .read<DoctorDailyCallCardBloc>()
                                          .add(SetCallCard(params: params));
                                    }
                                  },),
                        ],
                      );
                    }
                    return const SizedBox.shrink();
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class CustomDateTimeRow extends StatelessWidget {
  const CustomDateTimeRow({
    super.key,
    this.onDateChanged,
    this.onTimeChanged,
  });

  final Function(String?)? onDateChanged;
  final Function(String?)? onTimeChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          flex: 1,
          child: CustomContainer(
            height: 45.r,
            child: Row(
              children: [
                Image.asset(AppIcons.dateIcon),
                Expanded(
                  child: CustomDatePicker(
                    onChanged: onDateChanged,
                  ),
                ),
              ],
            ),
          ),
        ),
        const Gap(12),
        Expanded(
          flex: 1,
          child: CustomContainer(
            height: 45.r,
            child: Row(
              children: [
                Image.asset(AppIcons.timeIcon),
                Expanded(
                  child: CustomTimePicker(
                    onChanged: onTimeChanged,
                  ),
                ),
              ],
            ),
          ),
        )
      ],
    );
  }
}

This is my bloc class

class DoctorDailyCallCardBloc extends Bloc<DoctorDailyCallCardEvent, DoctorDailyCallCardState> {
  String visitDate = '';
  final String currentDate = DateTime.now().toString().split(' ')[0];

  DoctorDailyCallCardBloc() : super(DailyCallCardInitial()) {
    on<CallCardDoctorListEvent>(_getCallCardDoctorList);
    on<CallCardDoctorListByTerritoryEvent>(_getCallCardDoctorListByTerritory);
    on<SetCallCard>(_setDailyCallCard);
    on<CallCardDoctorPlanListEvent>(_setDailyCallCardLoaded);
  }

  Future<void> _getCallCardDoctorList(CallCardDoctorListEvent event, Emitter<DoctorDailyCallCardState> emit) async {
    emit(DailyCallCardLoading());

    visitDate = event.visitDate ?? currentDate;
    final String role = await GlobalStorage.getRole() ?? '';
    List<Territory> territoryList = [];
    List<Doctor> doctorList = [];
    List<Doctor> doctorPlanList = [];

    try {
      // Initialize futures based on role
      List<Future<Either>> futures = [
        sl<GetDoctorPlanUseCase>().call(param: {'visitDate': visitDate}),
      ];

      if (role.contains('T')) {
        futures.add(sl<GetDoctorUseCase>().call());
      } else {
        futures.add(sl<AllTerritoryUseCase>().call());
      }

      // Execute all required use cases concurrently
      final results = await Future.wait(futures);

      // Extract results
      final Either doctorPlanResult = results[0];
      final Either doctorResult = results[1];
      final Either? territoryResult = !role.contains('T') ? results[1] : null;

      // Handle doctorResult
      await doctorResult.fold((error) async {
        emit(DailyCallCardError(message: error));
        return;
      }, (doctorResponse) async {
        final DoctorEntity doctorData = DoctorEntity.fromJson(doctorResponse);
        if (doctorData.status!) {
          doctorList = doctorData.dataList ?? [];
        }
      });

      // Handle doctorPlanResult
      await doctorPlanResult.fold((error) async {
        emit(DailyCallCardError(message: error));
        return;
      }, (doctorPlanResponse) async {
        final DoctorEntity doctorPlanData = DoctorEntity.fromJson(doctorPlanResponse);
        if (doctorPlanData.status!) {
          doctorPlanList = doctorPlanData.dataList!;
        }
      });

      // Handle territoryResult if applicable
      if (territoryResult != null) {
        await territoryResult.fold((error) async {
          emit(DailyCallCardError(message: error));
        }, (territoryResponse) async {
          final AllTerritoryEntity territoryData = AllTerritoryEntity.fromJson(territoryResponse);
          if (territoryData.status!) {
            territoryList = territoryData.territoryList ?? [];
          }
        });
      }

      // Emit loaded state
      emit(DailyCallCardLoaded(
        doctorList: doctorList,
        doctorPlanList: doctorPlanList,
        territoryList: territoryList,
        showTerritory: role.contains('T') ? false : true,
        isDoctorLoading: false,
        isSettingPlan: false,
        isLoadingList: false,
      ));
    } catch (e, stacktrace) {
      emit(DailyCallCardError(message: e.toString()));
      debugPrint('Error from $runtimeType: ${e.toString()}');
      debugPrint('Stacktrace from $runtimeType: $stacktrace');
    }
  }

  Future<void> _getCallCardDoctorListByTerritory(CallCardDoctorListByTerritoryEvent event, Emitter<DoctorDailyCallCardState> emit) async {
    final currentState = state;
    if (currentState is DailyCallCardLoaded) {
      emit(currentState.copyWith(isDoctorLoading: true));

      try {
        final Either result = await sl<GetDoctorByTerritoryUseCase>().call(param: {'code': event.territoryCode});

        // Initialize state data
        List<Doctor> doctorList = [];

        await result.fold((error) async {
          emit(DailyCallCardError(message: error));
          return;
        }, (doctorByTerritoryResponse) async {
          final DoctorEntity doctorData = DoctorEntity.fromJson(doctorByTerritoryResponse);
          if (doctorData.status!) {
            doctorList = doctorData.dataList!;
          }
        });

        emit(currentState.copyWith(showTerritory: true, isDoctorLoading: false, doctorList: doctorList));
      } catch (e, stacktrace) {
        emit(DailyCallCardError(message: e.toString()));
        debugPrint('Error from $runtimeType: ${e.toString()}');
        debugPrint('Stacktrace from $runtimeType: $stacktrace');
      }
    }
  }

  Future<void> _setDailyCallCard(SetCallCard event, Emitter<DoctorDailyCallCardState> emit) async {
    final currentState = state;
    if (currentState is DailyCallCardLoaded) {
      emit(currentState.copyWith(isSettingPlan: true));

      Either result = await sl<SetDoctorPlanUseCase>().call(param: event.params);
      await result.fold((error) async {
        emit(DailyCallCardError(message: error));
      }, (response) async {
        final bool status = response['status'];
        final String message = response['message'];
        if (status) {
          Utils.showToast(message);
          emit(currentState.copyWith(isSettingPlan: false));
          add(CallCardDoctorPlanListEvent(visitDate: visitDate));
        }
      });
    }
  }

  Future<void> _setDailyCallCardLoaded(CallCardDoctorPlanListEvent event, Emitter<DoctorDailyCallCardState> emit) async {
    final currentState = state;
    if (currentState is DailyCallCardLoaded) {
      emit(currentState.copyWith(isLoadingList: true));
      visitDate = event.visitDate ?? currentDate;
      try {
        final Either result = await sl<GetDoctorPlanUseCase>().call(param: {'visitDate': visitDate});

        // Initialize state data
        List<Doctor> doctorPlanList = [];

        await result.fold((error) async {
          emit(DailyCallCardError(message: error));
          return;
        }, (doctorPlanResponse) async {
          final DoctorEntity doctorData = DoctorEntity.fromJson(doctorPlanResponse);
          if (doctorData.status!) {
            doctorPlanList = doctorData.dataList!;
          }
        });

        emit(currentState.copyWith(doctorPlanList: doctorPlanList, isLoadingList: false));
      } catch (e, stacktrace) {
        emit(DailyCallCardError(message: e.toString()));
        debugPrint('Error from $runtimeType: ${e.toString()}');
        debugPrint('Stacktrace from $runtimeType: $stacktrace');
      }
    }
  }
}

This is state class

@immutable
sealed class DoctorDailyCallCardState extends Equatable {
  @override
  List<Object?> get props => [];
}

final class DailyCallCardInitial extends DoctorDailyCallCardState {}

final class DailyCallCardLoading extends DoctorDailyCallCardState {}

final class DailyCallCardByDoctorLoading extends DoctorDailyCallCardState {}

final class DailyCallCardLoaded extends DoctorDailyCallCardState {
  final List<Doctor> doctorList;
  final List<Doctor> doctorPlanList;
  final List<Territory> territoryList;
  final bool? isDoctorLoading;
  final bool? isSettingPlan;
  final bool? isLoadingList;
  final bool? showTerritory;

  DailyCallCardLoaded({
    required this.doctorList,
    required this.doctorPlanList,
    required this.territoryList,
    this.isDoctorLoading,
    this.isSettingPlan = false,
    this.isLoadingList = false,
    this.showTerritory,
  });

  DailyCallCardLoaded copyWith({
    List<Doctor>? doctorList,
    List<Doctor>? doctorPlanList,
    List<Territory>? territoryList,
    bool? isDoctorLoading,
    bool? isSettingPlan,
    bool? isLoadingList,
    bool? showTerritory,
  }) {
    return DailyCallCardLoaded(
      doctorList: doctorList ?? this.doctorList,
      doctorPlanList: doctorPlanList ?? this.doctorPlanList,
      territoryList: territoryList ?? this.territoryList,
      isDoctorLoading: isDoctorLoading ?? this.isDoctorLoading,
      isSettingPlan: isSettingPlan ?? this.isSettingPlan,
      isLoadingList: isLoadingList ?? this.isLoadingList,
      showTerritory: showTerritory ?? this.showTerritory,
    );
  }

  @override
  List<Object?> get props => [
        doctorList,
        doctorPlanList,
        territoryList,
        isDoctorLoading,
        isSettingPlan,
        isLoadingList,
        showTerritory,
      ];
}

final class DailyCallCardError extends DoctorDailyCallCardState {
  final String message;

  DailyCallCardError({
    required this.message,
  });

  @override
  List<Object> get props => [message];
}

This is the event

@immutable
sealed class DoctorDailyCallCardEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class CallCardDoctorListEvent extends DoctorDailyCallCardEvent {
  final String? visitDate;

  CallCardDoctorListEvent({
    this.visitDate,
  });

  @override
  List<Object?> get props => [visitDate];
}

class CallCardDoctorListByTerritoryEvent extends DoctorDailyCallCardEvent {
  final String? territoryCode;

  CallCardDoctorListByTerritoryEvent({
    this.territoryCode,
  });

  @override
  List<Object?> get props => [territoryCode];
}

class SetCallCard extends DoctorDailyCallCardEvent {
  final DailyCallCardReqParams params;

  SetCallCard({
    required this.params,
  });

  @override
  List<Object> get props => [params];
}

class CallCardDoctorPlanListEvent extends DoctorDailyCallCardEvent {
  final String? visitDate;

  CallCardDoctorPlanListEvent({
    this.visitDate,
  });

  @override
  List<Object?> get props => [visitDate];
}

Upvotes: 0

Views: 28

Answers (1)

Delowar Hossain
Delowar Hossain

Reputation: 944

My problem is solved after changing my CreateCallCardWidget from StatelessWidget to StatefulWidget and updating my onChanged function of CustomDropDownWidget.

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

  @override
  State<CreateCallCardWidget> createState() => _CreateCallCardWidgetState();
}

class _CreateCallCardWidgetState extends State<CreateCallCardWidget> {
  final callCardKey = GlobalKey<FormState>();
  late DailyCallCardReqParams params;
  Territory? selectedTerritory;

  @override
  void initState() {
    super.initState();
    params = DailyCallCardReqParams(
      visitDate: DateFormat('yyyy-MM-dd', 'en').format(DateTime.now()),
      visitTime: DateFormat('hh:mm a', 'en').format(DateTime.now()),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: AppColors.secondaryElement,
      padding: const EdgeInsets.symmetric(
          horizontal: kHorizontal, vertical: elementPadding),
      margin: const EdgeInsets.symmetric(vertical: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          customTitle(title: 'Create Call Card', fontSize: 16.sp),
          const Gap(18),
          Form(
            key: callCardKey,
            child: Column(
              children: [
                CustomDateTimeRow(
                  onDateChanged: (value) {
                    params.visitDate = value;
                  },
                  onTimeChanged: (value) {
                    params.visitTime = value;
                  },
                ),
                BlocBuilder<DoctorDailyCallCardBloc, DoctorDailyCallCardState>(
                  builder: (context, state) {
                    if (state is DailyCallCardLoaded) {
                      return Column(
                        children: [
                          Visibility(
                            visible: state.showTerritory ?? false,
                            child: CustomDropdownSearch(
                              labelText: 'Select Territory',
                              items: (f, s) => state.territoryList,
                              itemAsString: (Territory data) => '${data.territoryName}\n${data.upperTerritoryCode}',
                              onChanged: (value) {
                                setState(() {
                                  selectedTerritory = value;
                                });
                                context.read<DoctorDailyCallCardBloc>().add(CallCardDoctorListByTerritoryEvent(territoryCode: value!.upperTerritoryCode));
                              },
                            ),
                          ),
                          state.isDoctorLoading!
                              ? const Center(child: ThreeDotLoading())
                              : CustomDropdownSearch(
                                  labelText: 'Select Doctor',
                                  items: (f, s) => state.doctorList,
                                  itemAsString: (Doctor data) => data.name!,
                                  onChanged: (value) {
                                    params.doctorID = value!.doctorId;
                                  },
                                ),
                          CustomTextFormField(
                            hintText: 'Write a comment here....',
                            maxLines: 2,
                            onChanged: (value) {
                              params.opinion = value;
                            },
                          ),
                          state.isSettingPlan!
                              ? const ThreeDotLoading()
                              : CustomButton(
                                  title: 'Set Plan',
                                  onPressed: () {
                                    if (callCardKey.currentState!.validate()) {
                                      context
                                          .read<DoctorDailyCallCardBloc>()
                                          .add(SetCallCard(params: params));
                                    }
                                  },
                                ),
                        ],
                      );
                    }
                    return const SizedBox.shrink();
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Upvotes: 0

Related Questions