Home > front end >  Access the build context in a cubit (Flutter)
Access the build context in a cubit (Flutter)

Time:01-19

Main question

Hello! In Flutter I am using BLoC, and more specifically cubits, to manage the state of my app.

I have a few states in my weather_state.dart file:

  • WeatherInitial
  • WeatherLoading
  • WeatherLoaded
  • WeatherError

For the latter, I would like to pass an error message when the state is emitted, as you can see in this method, from weather_cubit.dart (last line):

Future<void> getWeather(String cityName) async {
  emit(const WeatherLoading());
  try {
    final weather = await _weatherRepository.fetchWeather(cityName);
    emit(WeatherLoaded(weather));
  } on NetworkException {
    emit(const WeatherError('Error fetching the data.'));
  }
}

Now, I just localized my app, and therefore wanted to localize the error message as well. I imported the necessary packages in my cubit, and then accessed the localized error message that way:

emit(WeatherError(AppLocalizations.of(context)!.errorFetchingData));

I get the following error: Undefined name 'context'..
I understand why, obviously, but my question then is:
How do I access the context from inside a cubit?
And if there are multiple ways, what is the most optimized one to do so?

PS: If you have a decent workaround, feel free to mention it, too.

What I found:

Apparently, from this ressource, I found that providing the context that way (here, in my home_screen.dart):

return BlocProvider<WeatherCubit>(
  create: (context) => WeatherCubit(),
    child: MaterialApp(

...was supposed to make it accessible in my cubit. It does not work for me, as you can guess. By the way, I am searching for something a bit better than providing the context as a parameter in the cubit constructor (because that would be a real pain for testing afterwards).

Edit

I know I can pass an error message as a String parameter in my method. I also already use an error message property in my WeatherError state.
My problem is that the error message depends on the behavior of the method itself, which does not make possible passing an argument inside.

I took a simple example before, but I will provide another one there:
Suppose I have a UUID that I'm getting from a QR code scan.
I am passing the UUID as a parameter of the selectCity method:

void selectCity(String? cityUuid) async {
  const uuidPattern =
    r'[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
  final regexUuid = RegExp(uuidPattern);
  if (cityUuid == null) {
    emit(CitySelectionError('The QR code scanner had an error. Please try again.'));
  } else if (!regexUuid.hasMatch(cityUuid)) {
    emit(CitySelectionError('The QR code does not contain a UUID and cannot match a city.'));
  } else {
    emit(CitySelected(cityUuid));
  }
}

The error state:

class CitySelectionError extends CityState {
  final String errorMessage;

  CitySelectionError(this.errorMessage) : super(null);

  // Equatable: don't mind
  @override
  List<Object?> get props => [errorMessage];
}

I first want to check if it is null, as this would be a result of scanning failure, before checking if the string is an actual UUID.
As you can see, the error messages clearly depend on the method implementation. This implies that I cannot pass the error messages as parameters; this would not make any sense.
Any idea about what I am missing?

CodePudding user response:

Idk if it's a good idea to send the context (let's leave the context for the tree to your widgets, not in the logics), in my case I only send the error without the message :

emit(const WeatherError());

Now, in a listener (in case you want to show a Snackbar) you listen to that state and there you show the translation:

return BlocListener<BlocA, StateA>(
      listener: (context, state) {
        if (state is WeatherError) {
           ScaffoldMessenger.of(context)
           ..showSnackBar(
              SnackBar(
                content: Text(AppLocalizations.of(context)!.errorFetchingData),
              ),
           );
        }
      },
      child: ...

CodePudding user response:

The error is normal because Cubit has nothing to do with Widget and is not responsible to build anything.

How do I access the context from inside a cubit?

To strictly answer your question, in order to use the context inside the Cubit, you can pass it as an argument when creating the Cubit.

Something like follow :

return BlocProvider<WeatherCubit>(
  create: (context) => WeatherCubit(context),
    child: MaterialApp(

And so WeatherCubit should look like :

class WeatherCubit extends Cubit<WeatherState> {
  final BuildContext context;
  WeatherCubit(this.context) : super(WeatherState());
}

But, if the aim is to display the error message to the user, for example in a SnackBar, I recommend you to use a BlocListener (or a BlocConsumer if you need a BlocBuilder too) to handle this side-effects. By doing so, you keep a clear separation of responsibilities and respect the BLoC pattern's philosophy.

BlocListener<WeatherCubit, WeatherState>(
  listenWhen: (previous, current) => previous != current,
  listener: (context, state) {
    if (state is WeatherError) {
      ScaffoldMessenger.of(context)..hideCurrentSnackBar()..showSnackBar(SnackBar(content: Text(state.errorMessage)));
    }
  },
  child: child,
),

And finally, the WeatherError :

class WeatherError extends WeatherState {
  final String errorMessage;
  const WeatherError({required this.errorMessage});
}

That way you can access the errorMessage with state.errorMessage when the current state is WeatherError.

  • Related