Skip to main content
Chromia UI works seamlessly with popular Flutter state management solutions. This guide shows how to integrate theming and component state with BLoC, as demonstrated in the example app.

BLoC Integration

The Chromia UI example app uses flutter_bloc for state management, specifically for theme switching and brand management.

Installation

Add the required dependencies to your pubspec.yaml:
dependencies:
  chromia_ui: ^0.3.0
  flutter_bloc: ^8.1.0
  equatable: ^2.0.5

Theme Management with BLoC

1

Create Theme State

Define a state class that holds theme mode and brand configuration:
import 'package:chromia_ui/chromia_ui.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

class ThemeState extends Equatable {
  final ThemeMode themeMode;
  final BrandConfig selectedBrand;

  const ThemeState({
    this.themeMode = ThemeMode.light,
    this.selectedBrand = BrandConfigs.chromia,
  });

  bool get isDark => themeMode == ThemeMode.dark;

  ThemeState copyWith({
    ThemeMode? themeMode,
    BrandConfig? selectedBrand,
  }) {
    return ThemeState(
      themeMode: themeMode ?? this.themeMode,
      selectedBrand: selectedBrand ?? this.selectedBrand,
    );
  }

  @override
  List<Object?> get props => [themeMode, selectedBrand];
}
2

Create Theme Cubit

Implement a Cubit to manage theme changes:
import 'package:chromia_ui/chromia_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';

class ThemeCubit extends Cubit<ThemeState> {
  ThemeCubit() : super(const ThemeState());

  void toggleTheme() {
    emit(
      state.copyWith(
        themeMode: state.themeMode == ThemeMode.light 
          ? ThemeMode.dark 
          : ThemeMode.light,
      ),
    );
  }

  void changeBrand(BrandConfig brand) {
    emit(
      state.copyWith(
        selectedBrand: brand.copyWith(
          fontFamily: GoogleFonts.poppins().fontFamily,
          monospaceFontFamily: GoogleFonts.sourceCodePro().fontFamily,
        ),
      ),
    );
  }
}
3

Provide the Cubit

Wrap your app with BlocProvider to provide the theme cubit:
import 'package:chromia_ui/chromia_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ThemeCubit(),
      child: const AppView(),
    );
  }
}
4

React to Theme Changes

Use BlocBuilder to rebuild your app when the theme changes:
class AppView extends StatelessWidget {
  const AppView({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeCubit, ThemeState>(
      builder: (context, state) {
        final themeData = state.themeMode == ThemeMode.dark
          ? ChromiaThemeData.fromBrand(
              state.selectedBrand,
              brightness: Brightness.dark,
            )
          : ChromiaThemeData.fromBrand(
              state.selectedBrand,
              brightness: Brightness.light,
            );

        return ChromiaTheme(
          data: themeData,
          child: MaterialApp(
            title: 'Chromia UI Demo',
            theme: ChromiaTheme.of(context).toMaterialTheme(),
            darkTheme: ChromiaTheme.of(context).toMaterialTheme(),
            themeMode: state.themeMode,
            home: const HomePage(),
          ),
        );
      },
    );
  }
}
5

Toggle Theme from UI

Call cubit methods from your widgets:
class ThemeToggleButton extends StatelessWidget {
  const ThemeToggleButton({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeCubit, ThemeState>(
      builder: (context, state) {
        return ChromiaButton(
          label: state.isDark ? 'Light Mode' : 'Dark Mode',
          onPressed: () {
            context.read<ThemeCubit>().toggleTheme();
          },
          icon: Icon(
            state.isDark ? Icons.light_mode : Icons.dark_mode,
          ),
        );
      },
    );
  }
}

Component State Management

For managing component-level state, you can use BLoC, Cubit, or simple StatefulWidget:

Using StatefulWidget (Simple Cases)

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

  @override
  State<FormExample> createState() => _FormExampleState();
}

class _FormExampleState extends State<FormExample> {
  bool _agreedToTerms = false;
  String _selectedOption = 'option1';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ChromiaCheckbox(
          value: _agreedToTerms,
          onChanged: (value) {
            setState(() {
              _agreedToTerms = value ?? false;
            });
          },
        ),
        ChromiaRadioButtonGroup<String>(
          options: ['option1', 'option2', 'option3'],
          groupValue: _selectedOption,
          onChanged: (value) {
            setState(() {
              _selectedOption = value;
            });
          },
        ),
      ],
    );
  }
}

Using Cubit (Complex Cases)

1

Define Form State

class FormState extends Equatable {
  final String email;
  final String password;
  final bool isLoading;
  final String? error;

  const FormState({
    this.email = '',
    this.password = '',
    this.isLoading = false,
    this.error,
  });

  FormState copyWith({
    String? email,
    String? password,
    bool? isLoading,
    String? error,
  }) {
    return FormState(
      email: email ?? this.email,
      password: password ?? this.password,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }

  @override
  List<Object?> get props => [email, password, isLoading, error];
}
2

Create Form Cubit

class FormCubit extends Cubit<FormState> {
  FormCubit() : super(const FormState());

  void updateEmail(String email) {
    emit(state.copyWith(email: email, error: null));
  }

  void updatePassword(String password) {
    emit(state.copyWith(password: password, error: null));
  }

  Future<void> submit() async {
    emit(state.copyWith(isLoading: true, error: null));

    try {
      // Your API call here
      await Future.delayed(const Duration(seconds: 2));
      emit(state.copyWith(isLoading: false));
    } catch (e) {
      emit(state.copyWith(
        isLoading: false,
        error: e.toString(),
      ));
    }
  }
}
3

Build Form UI

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => FormCubit(),
      child: BlocBuilder<FormCubit, FormState>(
        builder: (context, state) {
          final cubit = context.read<FormCubit>();

          return Column(
            children: [
              ChromiaTextField(
                label: 'Email',
                onChanged: cubit.updateEmail,
                validator: ChromiaTextFieldValidator.email,
              ),
              SizedBox(height: context.chromiaSpacing.medium),
              ChromiaTextField(
                label: 'Password',
                obscureText: true,
                onChanged: cubit.updatePassword,
                validator: ChromiaTextFieldValidator.required,
              ),
              if (state.error != null) ..[
                SizedBox(height: context.chromiaSpacing.small),
                ChromiaText(
                  state.error!,
                  style: context.chromiaTypography.bodySmall,
                  color: context.chromiaColors.error,
                ),
              ],
              SizedBox(height: context.chromiaSpacing.large),
              ChromiaButton(
                label: 'Login',
                onPressed: cubit.submit,
                isLoading: state.isLoading,
                variant: ChromiaButtonVariant.filled,
              ),
            ],
          );
        },
      ),
    );
  }
}

Brand Switching Example

Here’s how to implement brand switching in your app:
class BrandSelector extends StatelessWidget {
  const BrandSelector({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeCubit, ThemeState>(
      builder: (context, state) {
        return Column(
          children: [
            ChromiaText(
              'Select Brand',
              style: context.chromiaTypography.titleMedium,
            ),
            SizedBox(height: context.chromiaSpacing.medium),
            ChromiaRadioButtonGroup<BrandConfig>(
              options: [
                BrandConfigs.chromia,
                BrandConfigs.forest,
                BrandConfigs.ocean,
                BrandConfigs.sunset,
              ],
              groupValue: state.selectedBrand,
              onChanged: (brand) {
                context.read<ThemeCubit>().changeBrand(brand);
              },
              labelBuilder: (brand) => brand.name,
            ),
          ],
        );
      },
    );
  }
}

Other State Management Solutions

Provider

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.light;
  BrandConfig _selectedBrand = BrandConfigs.chromia;

  ThemeMode get themeMode => _themeMode;
  BrandConfig get selectedBrand => _selectedBrand;

  void toggleTheme() {
    _themeMode = _themeMode == ThemeMode.light 
      ? ThemeMode.dark 
      : ThemeMode.light;
    notifyListeners();
  }

  void changeBrand(BrandConfig brand) {
    _selectedBrand = brand;
    notifyListeners();
  }
}

Riverpod

final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeState>(
  (ref) => ThemeNotifier(),
);

class ThemeNotifier extends StateNotifier<ThemeState> {
  ThemeNotifier() : super(const ThemeState());

  void toggleTheme() {
    state = state.copyWith(
      themeMode: state.themeMode == ThemeMode.light 
        ? ThemeMode.dark 
        : ThemeMode.light,
    );
  }

  void changeBrand(BrandConfig brand) {
    state = state.copyWith(selectedBrand: brand);
  }
}

Best Practices

Theme configuration should be managed at the highest level of your app to ensure consistent theming across all screens.
For straightforward state management like theme switching, use Cubits instead of full Blocs.
Keep theme management separate from business logic. Create dedicated state management for UI concerns vs. data concerns.
Use shared_preferences or hive to persist user theme choices:
Future<void> saveThemeMode(ThemeMode mode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('theme_mode', mode.toString());
}

Theming Guide

Learn more about Chromia UI theming

Example App

View the full BLoC implementation

Build docs developers (and LLMs) love