skip to Main Content

I need to implement a feature in my application that gives the possibility to users to enter a password and switch servers (toggle staging server if they were using production server, or toggle production server if they were using staging server).

The theme of my app needs to be updated as well when the environment server changes to let the user know they’ve changed servers successfully.

I’m using flutter_bloc package and cubits. The environment gets changed successfully, but I can’t seem to get the theme of my app to update in my main function.

Here is my Environment Cubit :


class EnvironmentCubit extends LoggedCubit<EnvironmentState> {
  EnvironmentCubit() : super(EnvironmentState(environment: Constants.environment));

  void toggleServer({required String password}) {
    if (password != Constants.switchServerPassword) {
      emit(state.copyWith(success: false, error: 'Password is incorrect.'));
      return;
    }
    _toggleServer();
  }

  void _toggleServer() {
    final newEnvironment =
        state.environment == Environment.production
            ? Environment.staging
            : Environment.production;
    emit(EnvironmentState(success: true, error: null, environment: newEnvironment));
    print("Environment has changed to " + state.environment.name); // TRIGGERS ✔️
  }
}

class EnvironmentState {
  final bool success;
  final String? error;
  final Environment environment;

  EnvironmentState({this.success = false, this.error, required this.environment});
}

Here is my AppTheme class :


class AppColors {
  static const Color colorPrimary = Colors.green;
  static const Color colorPrimaryDev = Colors.blue;
  static const Color colorPrimaryStaging = Colors.red;
}

class AppTheme {

  static ThemeData theme =
      _generateTheme(Constants.environment);

  static ThemeData _generateTheme(Environment environment) {
    final primaryColor = PrimaryColor.fromEnvironment(environment);

    return ThemeData(
      appBarTheme: AppBarTheme(backgroundColor: primaryColor.color),
      primaryColor: primaryColor.color,
      // Some other styles ...
    );
  }

  static ThemeData getTheme(Environment environment) {
    print("getTheme is called."); // ❌ DOES NOT TRIGGER WHEN STATE CHANGES
    return _generateTheme(environment);
  }
}

enum PrimaryColor {
  development(AppColors.colorPrimaryDev),
  staging(AppColors.colorPrimaryStaging),
  production(AppColors.colorPrimary);

  final Color color;
  const PrimaryColor(this.color);

  static PrimaryColor fromEnvironment(Environment environment) {
    switch (environment) {
      case Environment.development:
        return PrimaryColor.development;
      case Environment.staging:
        return PrimaryColor.staging;
      case Environment.production:
        return PrimaryColor.production;
      default:
        return PrimaryColor.development;
    }
  }
}

Here is my main.dart file:


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureDependencies();
  Bloc.observer = locator.get<Logger>().blocObserver;

  runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider<EnvironmentCubit>(
          create: (context) => EnvironmentCubit(),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<EnvironmentCubit, EnvironmentState>(
      listener: (context, state) {
        print("Listener is called. Environment is " + state.environment.name); // ❌ DOES NOT TRIGGER
      },
      builder: (context, state) {
        final theme = AppTheme.getTheme(state.environment);
        print("Widget is (re-)building."); // ❌ DOES NOT TRIGGER AGAIN AFTER STATE CHANGES
        return MaterialApp.router(
          title: "My App",
          localizationsDelegates: const [
            S.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          routerConfig: locator<RouterConfig<Object>>(),
          theme: theme,
        );
      },
    );
  }

  // The route configuration.
  static RouterConfig<Object> createRouter() => GoRouter(
        initialLocation: Routes.splashScreen,
        routes: <RouteBase>[
          GoRoute(
            path: Routes.home,
            builder: (BuildContext context, GoRouterState state) {
              return const HomePage();
            },
          ),
          GoRoute(
            path: Routes.login,
            builder: (BuildContext context, GoRouterState state) {
              return const LoginPage();
            },
          ),
          // Other routes...
        ],
      );
}

Here’s the Login page where the user can switch servers:


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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => EnvironmentCubit(),
      child: LoginPageContent(),
    );
  }
}

class LoginPageContent extends StatelessWidget {
  final form = FormGroup({
    'password': FormControl<String>(validators: [Validators.required]),
  });

  LoginPageContent({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocConsumer<EnvironmentCubit, EnvironmentState>(
        builder: (context, state) {
          return Padding(
            padding: EdgeInsets.all(16),
            child: ReactiveForm(
              formGroup: form,
              child: Column(
                children: [
                  Text("You are on server: " + state.environment.name),
                  ReactiveTextField<String>(
                    formControlName: 'password',
                    decoration: InputDecoration(labelText: 'Enter password:'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      _validatePassword(context);
                    },
                    child: Text('Confirm'),
                  ),
                ],
              ),
            ),
          );
        },
        listener: (context, state) {
            print("Success. State has changed."); // TRIGGERS ✔️
          }
        },
      ),
    );
  }

  void _validatePassword(BuildContext context) {
    if (form.valid) {
      print("Form is valid."); // TRIGGERS ✔️
      final password = form.control('password').value;
      context.read<EnvironmentCubit>().toggleServer(password: password);
    }
  }
}

On my HomePage, I can display the name of the environment and notice the state changes. But in my main function, the BlocProvider and BlocConsumer don’t seem to react to any state changes, and the theme doesn’t change.

I think the problem might come from the fact I’m using EnvironmentCubit on both the Login page (where state changes effectively) and in my main function (where it doesn’t work).

How can I solve this?

2

Answers


  1. Chosen as BEST ANSWER

    I'm still a newbie and just realised I couldn't use two distinct instances of EnvironmentCubit, since they both have their own state and absolutely do not synchronise.

    I removed the BlocProvider from my LoginPage and declare the cubit in the build method of my widget:

    final environmentCubit = BlocProvider.of<EnvironmentCubit>(context);
    
    

    Then I edited the _validatePassword() function to add the cubit as a parameter:

    void _validatePassword(BuildContext context, EnvironmentCubit environmentCubit) {
        if (form.valid) {
          final password = form.control('password').value;
          environmentCubit.toggleServer(password: password);
        }
      }
    

    ...and in my ElevatedButton's onPressed() function, I called the _validatePassword() function like this:

    _validatePassword(context, environmentCubit);
    

    Now it works perfectly.


  2. Great you found a workaround.

    So first off: An easy way to work with blocs/cubit is to use it with a dependency Injection tool. Usually this is GetIt. If you had registered your EnvironmentCubit as a singleton in GetIt. Whenever you use getIt to call an instance, it will get the same bloc instance. And your consumers would respond to the same cubit/bloc instance all the time unless you use create a local instance.

    Additionally, I actually thought your states didn’t extend Equatable. and I was right. Why?
    One more reason states may not rebuild in Bloc/Cubit, is the Bloc Consumer/Listener thinks the new Environment States being emitted are the equal to the Old Environment states present. Equatable makes the listener know the states aren’t the same. So, your EnvironmentState should extend Equatable, where you have to override it’s props field.
    This should get your blocs rebuilding when the state changes.

    What does Equatable do?
    "Equatable is used to simplify the process of comparing instances of the same object without the need for boilerplate codes of overriding “==” and hashCodes."
    like below

        class EnvironmentState extends Equatable {
      final bool success;
      final String? error;
      final Environment environment;
    
      EnvironmentState(
          {this.success = false, this.error, required this.environment});
    
      @override
      List<Object?> get props => [
            success,
            error,
            environment,
          ];
    }
    

    You can have a look at This Get It introduction
    and this Equatable example

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search