flutternavigationblocflutter-bloc

Flutter bloc makes navigation slower


I am trying to navigate to QuestCartScreen after tapping on discover more quests button.I am using bloc for state management. But it is taking long time to navigate. The action occurs from homepage from the widget _buildAddMoreQuestsContainer.But navigation triggered from home screen.Homepage is the child of homescreen. Navigation occurs after state is QuestsDataLoaded.Is it because of naviagting after data is loaded. My navigation screen requires parameters from the state chanages.

I have given codes of onboarding bloc , Homescreen and homepage.

class HomeScreen extends StatefulWidget {
    static const String routeName = '/home';
    final AuthUtilities authUtilities;
    const HomeScreen({super.key, required this.authUtilities});

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

  class _HomeScreenState extends State<HomeScreen> {
    bool _onboardingHasError = false;

    @override
    void dispose() {
      super.dispose();
    }

    @override
    void initState() {
     super.initState();
      _resetErrorState();
      context.read<HomeBloc>().add(FetchDataEvent());
    }

    // Centralized method to set error state
    void _setErrorState(bool hasError) {
      if (_onboardingHasError != hasError) {
        setState(() {
          _onboardingHasError = hasError;
        });
      }
}

// Centralized method to reset error state
void _resetErrorState() {
  _setErrorState(false);
}

Future<void> _refreshData() async {
  context.read<HomeBloc>().add(FetchDataEvent());
}

@override
Widget build(BuildContext context) {
  return BlocConsumer<HomeBloc, HomeState>(
    listener: (context, homeState) {
      // Handle state changes in the listener instead of during build
      if (homeState is! OnboardingIncomplete) {
        _resetErrorState();
      }
    },
    builder: (context, homeState) {
      return BlocListener<OnboardingBloc, OnboardingState>(
        listener: (context, onboardingState) {
          if (onboardingState is OnboardingError) {
            _setErrorState(true); // Set error state immediately

            // Show a snackbar for network errors
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(onboardingState.message),
                duration: const Duration(seconds: 3),
              ),
            );
          } else if (onboardingState is QuestSelectionCompleted ||
              onboardingState is OnboardingCompleted) {
            _resetErrorState();
          }
        },
        child: Builder(builder: (context) {
          // Simplified logic: only hide AppBar during normal onboarding process
          // Always show AppBar during errors
          final bool hideAppBar =
              homeState is OnboardingIncomplete && !_onboardingHasError;

          return AuthenticatedScaffold(
            forceHideAppBar: hideAppBar,
            isLogoClickable: false,
            currentPage: CurrentRouteEnum.home,
            currentNavIndex: 0,
            hideBottomNavBar: hideAppBar ||
                _onboardingHasError, // Hide bottom nav during onboarding
            body: MultiBlocListener(
              listeners: [
                BlocListener<AuthBloc, AuthState>(listener: (context, state) {
                  if (state is AuthUnauthenticated) {
                    Navigator.of(context).pushAndRemoveUntil(
                      MaterialPageRoute(
                          builder: (context) => const LoginScreen()),
                      (Route<dynamic> route) => false,
                    );
                  }
                }),
                BlocListener<OnboardingBloc, OnboardingState>(
                    listener: (context, state) {
                  if (state is QuestsDataLoaded) {
                    Navigator.push(
                        context,
                        MaterialPageRoute(
                            builder: (context) => QuestCartScreen(
                                  quests: state.quests,
                                  userData: state.userData,
                                  onBack: () {},
                                  isOnboardingProcess: false,
                                )));
                  }
                })
              ],
              child: Stack(children: [
                BlocBuilder<HomeBloc, HomeState>(
                  builder: (context, state) {
                    if (state is HomeLoading || state is HomeInitial) {
                      return const AppLoaderWidget();
                    } else if (state is OnboardingIncomplete) {
                      return OnboardingFlow(
                        userData: state.userData,
                        onErrorStateChanged: _setErrorState,
                      );
                    } else if (state is OnboardingComplete) {
                      return RefreshIndicator(
                        onRefresh: _refreshData,
                        child: ListView(
                          children: [
                            HomePage(
                              userData: state.userData,
                              questData: state.questData,
                            ),
                          ],
                        ),
                      );
                    }
              ]),
            ),
          );
        }),
      );
    },
  );
}
class HomePage extends StatelessWidget {
final User userData;
final List<Quest> questData;

const HomePage({
  super.key,
  required this.userData,
  required this.questData,
});

@override
Widget build(BuildContext context) {
  final chartData = ChartUtils.transformUserJourneyData(
      userData.points, AppColors.doughtnutFillColor);

  final List<Quest> incompleteQuests =
      QuestUtils.getIncompleteAndActiveQuests(questData: questData);

  return SingleChildScrollView(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _buildMainContainer(context, chartData, incompleteQuests),
      ],
    ),
  );
}

Widget _buildMainContainer(BuildContext context,
    List<RadialComponent> chartData, List<Quest> incompleteQuests) {
  return Container(
    decoration: BoxDecoration(
      color: Theme.of(context).cardColor,
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(height: 10.h), // adapt height to screen size
        _buildSectionTitle(
            context, 'Welcome back, ${userData.name.split(' ')[0]}!'),
        SizedBox(height: 5.h),
        GraphPresentation(
          height: 230.h,
          width: double.infinity,
          tooltipBehavior: TooltipBehavior(),
          chartData: chartData,
          onTap: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return const PortfolioScreen();
            }));
          },
        ),
        SizedBox(height: 5.h),
        Padding(
          padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h),
          child: Text(
            AppLiterals.quests,
            style: AppTextStyles.subtitle,
          ),
        ),
        _buildQuestsGrid(context, incompleteQuests),
        SizedBox(height: 5.h),
      ],
    ),
  );
}
Widget _buildQuestsGrid(
  BuildContext context,
  List<Quest> incompleteQuests,
) {
  return Container(
      margin: EdgeInsets.all(5.w),
      decoration: BoxDecoration(
        color: AppColors.lightGreyVariant,
        borderRadius: BorderRadius.circular(15),
      ),
      child: GridBuilderWidget(
        itemCount:
            incompleteQuests.length + (incompleteQuests.length < 4 ? 1 : 0),
        itemBuilder: (p1, index) {
          if (index < incompleteQuests.length) {
            return _buildQuestCartContainer(context, incompleteQuests[index]);
          }
          return _buildAddMoreQuestsContainer(context);
        },
      ));


Widget _buildAddMoreQuestsContainer(
  BuildContext context,
) {
  return BlocBuilder<OnboardingBloc, OnboardingState>(
      builder: (context, state) {
    return Semantics(
      label: AppLiterals.discoverQuests,
      child: CustomContainerButtonWidget(
        imageWidget: Padding(
          padding: EdgeInsets.only(top: 10.h), // Add space on top
          child: Image.asset(
            AppImages.imagAddIcon,
            height: 40.h,
            width: 40.h,
          ),
        ),
        showPrimaryButton: true,
        title: AppLiterals.discoverQuests,
        color: Colors.white,
        isLoading: state is OnboardingLoading,
        onPressed: () {
          // Safely dispatch the event through a future to avoid build-phase errors
          Future.microtask(() {
            if (context.mounted) {
              context.read<OnboardingBloc>().add(FetchQuestCartsData());
            }
          });
        },
      ),
    );
  });
}
}

class OnboardingBloc
  extends flutter_bloc.Bloc<OnboardingEvent, OnboardingState> {
final QuestService questService;
final CauseService causeService;
final SkillService skillService;
final UserService userService;
final NGOService ngoService;
final AuthBloc authBloc;
User? _cachedUserData;

OnboardingBloc({
  required this.questService,
  required this.causeService,
  required this.skillService,
  required this.userService,
  required this.ngoService,
  required this.authBloc,
}) : super(OnboardingInitial()) {
  _initializeUserData();

  on<FetchQuestCartsData>(_handleFetchingQuestCartsData);
  on<PostQuestCarts>(_handlePostQuestId);

}
Future<void> _handleFetchingQuestCartsData(FetchQuestCartsData event,
    flutter_bloc.Emitter<OnboardingState> emit) async {
  emit(OnboardingLoading());
  try {
    final List<Quest> quests =
        await questService.fetchRecommendedQuestsData();
    final userData = await _getUserData();
    emit(QuestsDataLoaded(
      quests: quests,
      userData: userData,
    ));
  } catch (e) {
    emit(OnboardingError(message: 'Failed to fetch the quest data:--$e'));
  }
}

}

Solution

  • After analyzing the code I can see that the navigation happens AFTER awaiting for the questService.fetchRecommendedQuestsData() and _getUserData() are done, so the issue is not bloc related, what you can do is navigate to the other screen directly then load the data, you can display a loading indicator meanwhile the data is loading

    So the flow will be
    - User tap -> event triggered + navigating to the new screen
    - In the new screen have a Bloc builder and there you can show the loading indicator or the data as soon as it arrives