Flutter 3.0: Implement dynamic Theming using Bloc-Cubit

In this post, we’re going to learn how to implement dynamic theming in a Flutter app using Bloc as state management.

· Prerequisites
· Getting Started
∘ Project creation
∘ Add the dependencies
∘ Add the logic classes
∘ Apply theme
· Project structure
· App Results
· Conclusion
· References

Prerequisites

This is the list of all the prerequisites for following this story:

  • Flutter 3+ installed
  • Basic knowledge of Dart and Flutter
  • Android Studio or Visual Studio Code

Getting Started

Today, most mobile applications have introduced a dark mode to enhance the user experience. Users can switch themes and choose what suits them best. Flutter defines a visual theme to share the colors, style, and other parameters applicable to the Material component type throughout the application or in particular parts of it.

Flutter applies styling in the following order:

  1. Styles applied to the specific widget.
  2. Themes that override the immediate parent theme.
  3. The main theme for the entire app.

This project is based on the design made by Chirag Mittal on Dribbble.

Project creation

We will start by creating a simple Flutter project.

Add the dependencies

Let’s first include some external packages that we will need. Add these dependencies to pubspec.yaml file.

dependencies:
flutter_bloc: ^8.1.3
get_it: ^7.6.0
shared_preferences: ^2.1.2
flutter_rating_bar: ^4.0.1

We are using:
1- flutter_bloc: It makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable. It is used for state management.
2- get_it: It’s a locator for your objects so you need some other way to notify your UI about changes like Streams or ValueNotifiers.
3- shared_preferences: It is used to set the value in the memory so that even if we close the app and restart it, our data won’t be lost.
4- flutter_rating_bar: It allows displaying the rating bar indicator in the user interface.

Add the logic classes

Create a Dart app_preferences file containing abstract classes and implementation classes.

abstract class AppPreferences {
Future<void> saveThemeMode(bool isDark);
bool isDark();
}

class AppPreferencesImpl implements AppPreferences {
final SharedPreferences _prefs;

AppPreferencesImpl(this._prefs);

@override
saveThemeMode(bool isDark) async {
await _prefs.setBool(AppConstants.darkTheme, isDark);
}

@override
bool isDark() {
return _prefs.getBool(AppConstants.darkTheme) ?? false;
}

}

We have two methods: save dark mode and retrieve the boolean value if dark mode is enabled.

Now, create a class for the cubit theme.

class ThemeCubit extends Cubit<ThemeMode> {
ThemeCubit(super.initialState);

final prefs = getIt<AppPreferences>();

changeTheme(bool val) {
if (prefs.isDark()) {
prefs.saveThemeMode(false);
emit(ThemeMode.light);
} else {
prefs.saveThemeMode(true);
emit(ThemeMode.dark);
}
}
}

This method will trigger once the theme updates after clicking the switch.

Apply theme

Let’s create a custom theme for light and dark modes. We create separate files for each mode.

ThemeData darkAppTheme = ThemeData(
fontFamily: 'Roboto',
primaryColor: primaryColor,
primaryColorLight: primaryLightColor,
indicatorColor: primaryLightColor,
secondaryHeaderColor: const Color(0xFFbfdeff),
disabledColor: const Color(0xffa2a7ad),
brightness: Brightness.dark,
hintColor: const Color(0xFFe4e8ef),
shadowColor: Colors.black.withOpacity(0.4),
cardColor: const Color(0xFF212326),
textTheme: TextTheme(
titleLarge: const TextStyle(color: Color(0xFF8e9fb9)),
titleSmall: const TextStyle(color: Color(0xFFe4e8ef)),
displayMedium: const TextStyle(color: Color(0xFFe4e8ef), fontWeight: FontWeight.normal).copyWith(fontSize: 22),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: const Color(0xFFcda335))),
appBarTheme: const AppBarTheme(backgroundColor: Color(0x4D334257)),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFcda335), secondary: Color(0xFFcda335))
.copyWith(background: const Color(0xFF000000))
.copyWith(error: const Color(0xFFdd3135)));
ThemeData lightAppTheme = ThemeData(
fontFamily: 'Roboto',
primaryColor: primaryColor,
primaryColorLight: primaryLightColor,
indicatorColor: primaryLightColor,
secondaryHeaderColor: const Color(0xFF4794FF),
disabledColor: const Color(0xFFBABFC4),
brightness: Brightness.light,
shadowColor: Colors.grey[300],
hintColor: const Color(0xFF25282D),
cardColor: Colors.white,
textTheme: TextTheme(
titleLarge: const TextStyle(color: Color(0xFF334257)),
titleSmall: const TextStyle(color: Color(0xFF25282D)),
displayMedium: const TextStyle(color: Color(0xFFE84D4F), fontWeight: FontWeight.bold).copyWith(fontSize: 10),
displaySmall: const TextStyle(color: Color(0xFF25282D)),
),
checkboxTheme: const CheckboxThemeData(
side: BorderSide(color: black),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: const Color(0xFFF8F9FA))),
appBarTheme: appbarThemeLight,
sliderTheme: sliderTheme,
scaffoldBackgroundColor: white,
colorScheme: const ColorScheme.light(
primary: Color(0xFFdcb247), secondary: Color(0xFFEBEBEB),)
.copyWith(background: const Color(0xFFF8F9FA))
.copyWith(error: const Color(0xFFE84D4F)),
);

The ThemeData class provides properties we can override to adjust the theme for MaterialApp to use. The colorScheme and textThemeare used by the Material components to compute default values for visual properties. To indicate a dark theme, set the ColorScheme’s brightness attribute to Brightness.dark.

RootApp’s MaterialApp is wrapped with ThemeBloc’s BlocBuilder which provides the state theme when the user changes it.

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

// This widget is the root of your application.
@override
Widget build(BuildContext context) {

return BlocBuilder<ThemeCubit, ThemeMode>(
buildWhen: (previous, current) => previous != current,
builder: (_, state) {
return MaterialApp(
title: AppConstants.appTitle,
debugShowCheckedModeBanner: false,
theme: lightAppTheme,
darkTheme: darkAppTheme,
themeMode: state,
initialRoute: PlacesScreen.routeName,
onGenerateRoute: AppRouter.onGenerateRoute,
);
},
);
}
}

Widgets whose appearance should align with the overall theme can obtain the current theme’s configuration with Theme.of. Material components typically depend exclusively on the colorScheme and textTheme. These properties are guaranteed to have non-null values.

The static Theme.of method finds the ThemeData value specified for the nearest BuildContext ancestor.

PlacesScreen class code source

final places = [
Place(title: "London Tour", description: "4N/5D", price: "\$150.00", rating: 4, reviewCount: 40, image: "https://media.istockphoto.com/id/1365147272/es/foto/puente-octavio-frias-de-oliveira-en-sao-paulo-brasil-am%C3%A9rica-latina.jpg?s=612x612&w=0&k=20&c=fo2cesqIkiVtNbPkxeekDknvaNeLODxgLvcWbtNnk-I="),
Place(title: "Exclusive Dublin tour", description: "3N/4D", price: "\$110.00", rating: 4, reviewCount: 56, image: "https://images.unsplash.com/photo-1528728329032-2972f65dfb3f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8Z2VybWFueXxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=500&q=60"),
Place(title: "Madrid city tour", description: "4N/4D", price: "\$140.00", rating: 4, reviewCount: 38, image: "https://images.unsplash.com/photo-1616231339921-abd6c61276e2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fG1hZHJpZHxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=500&q=60")
];


class PlacesScreen extends StatefulWidget {
static const String routeName = "/places";

const PlacesScreen({Key? key}) : super(key: key);

@override
PlacesScreenState createState() => PlacesScreenState();
}

class PlacesScreenState extends State<PlacesScreen> {
bool isDark = false;

void _changeTheme() {
BlocProvider.of<ThemeCubit>(context).changeTheme(isDark);
}

@override
void initState() {
isDark = getIt<AppPreferences>().isDark();
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
elevation: 0.0,
automaticallyImplyLeading: false,
leadingWidth: 40,
title: Row(
children: [
Text(
AppConstants.lightModeTitle,
style: TextStyle(color: Theme.of(context).textTheme.titleSmall!.color, fontWeight: (isDark) ? FontWeight.normal : FontWeight.bold),
),
const SizedBox(width: 5,),
Text(
"·",
style: TextStyle(color: Theme.of(context).textTheme.titleSmall!.color, fontWeight: FontWeight.bold),
),
const SizedBox(width: 5,),
Text(
AppConstants.darkModeTitle,
style: TextStyle(color: Theme.of(context).textTheme.titleSmall!.color, fontWeight: (isDark) ? FontWeight.bold : FontWeight.normal),
)
],
),
actions: [
Switch(
value: isDark,
inactiveTrackColor: black,
activeColor: white,
inactiveThumbColor: (isDark) ? black : white,
onChanged: (value) {
setState(() {
isDark = !isDark;
});
_changeTheme();
},
)
]),
body: SafeArea(
bottom: false,
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
ListView.builder(
itemCount: places.length,
itemBuilder: (context, index) => PlaceCardView(
place: places[index],
press: () {},
),
)
],
),
),
],
),
));
}
}

Project structure

This is the final folders & files structure for our project:

App Results

Conclusion

In this post, we have seen how to implement dynamic theming in a Flutter app using Bloc as state management.

The complete source code is available on GitHub.

IYou can reach out to me and follow me on MediumTwitterGitHub

Thanks for reading!

References

👉 Link to Medium blog

Related Posts