1

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?

justAsking
  • 11
  • 3
  • The aim is to display a SnackBar or something like that to the user with the error right ? – redbnlrg Jan 19 '23 at 10:00
  • @redbnlrg Yes, I would like to access the state of the WeatherError state to get the error message and display it inside of a snackbar. – justAsking Jan 19 '23 at 10:39

2 Answers2

2

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: ...

EDIT :

Yes, the logic will be like this (unless you are already looking for a way to handle errors better), but for this case we will make the condition, however, we will send a key that will help you to find which translation to look for.

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('qrError'));
  } else if (!regexUuid.hasMatch(cityUuid)) {
    emit(CitySelectionError('qrNoMatch'));
  } else {
    emit(CitySelected(cityUuid));
  }
}

In your listener, inside your AppLocalization you will have a method called translate(), in this method you can send that key and by means of a .json it will take the translation, but for this I leave you this answer that explains it much better:

String xml file in Flutter

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

I hope it can fix or optimize your code better :)

Daniel Roldán
  • 1,328
  • 3
  • 20
  • Hello, thank you for your answer. How would you go about having multiple error messages needed for the same state? I edited my answer to explain it in details. Say 1 error message if it is null and another one if it does not match an additionnal condition. To me, this would result in having business logic outside of the cubit, which is not good, right? – justAsking Jan 19 '23 at 12:42
  • So, you don't want to have the if else condition ? or do you want to pass both error messages ? – Daniel Roldán Jan 19 '23 at 12:53
  • Yeah I basically want to pass both. But not really in fact, because that makes providing every error message mandatory for the caller. That's really ugly, right? Because the error message I want to display will change depending on the conditions (null, not matching the regex), that will be processed inside the cubit. Everything feels intricated in the bad way... – justAsking Jan 19 '23 at 14:28
  • Indeed, creating different attributes for each message is not very optimal, what I would do is yes, do the logic evidently in your cubit, and depending on the error, you send a key String message... I will do the update for you: – Daniel Roldán Jan 19 '23 at 15:33
  • Okay thanks for the explanation, I will look into your solution. You said there were a better way to handle the errors, could you provide some documentation/resources? I am interested in every way I can improve my code. Concerning the main topic, I ended up creating two different states for the two different errors. These states extend a general error state. – justAsking Jan 19 '23 at 16:02
  • You can take a look the TDD architecture, that it's a clean logic in a flutter project : https://resocoder.com/flutter-clean-architecture-tdd/ and in that course, you'll see the error handling with Either – Daniel Roldán Jan 20 '23 at 09:16
0

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.

redbnlrg
  • 267
  • 1
  • 9
  • Hello, thank you for the quick answer. I am already using a BlocListener for this purpose. However, what I don't understand is how am I supposed to get access to the `state.errorMessage`, when this errorMessage is the string I want to localize (and therefore the one for which I need the context). – justAsking Jan 19 '23 at 10:44
  • You can create your class WeatherError with a String errorMessage parameter. I edit my answer. – redbnlrg Jan 19 '23 at 10:47