Flutter Dynamic Themes with cubit and hydrated cubit
Learn how to change themes in flutter dynamically with blog cubit and hydrated cubit
Hello guys, In this tutorial, we will gonna learn, how to change themes in flutter apps dynamically with the help of cubit and hydrated cubit.
You can get the full source code here
0: Create New Flutter Project
We will start by creating a brand new flutter project. Make sure you've installed the flutter SDK & android studio from the official website if not then please install those things first from the link below.
After you've installed everything on your machine now it's time to run the flutter command and wait for it to complete.
flutter create dynamic_theme_demo
1: Setup ( Installing Dependencies )
Now let's open the project folder in VSCode or any IDE of your choice and install the required dependencies we will be needing in our project.
Go to the pubspec.yaml
file on the root directory of the project and paste these packages to the dependencies list and after that run theflutter pub get
command if required in VSCode it will be done automatically when you will save the file.
bloc: ^7.2.1
flutter_bloc: ^7.3.1
google_fonts: ^2.1.0
hydrated_bloc: ^7.1.0
path_provider: ^2.0.5
2: Making Themes Presets
After installing all the dependencies, now it's time to make some theme presets.
Create new file named themes.dart
inside themes folder under lib folder
lib/themes/themes.dart
There are many ways to achieve this but we will take the path which makes sense to us. We will create an enum
to represent different theme presets. I've used 6 themes presets but you can use as many as you want.
enum AppTheme {
redDark,
redLight,
blueLight,
blueDark,
greenDark,
greenLight,
}
Below that, we will create a Map<AppTheme, ThemeData>
, so that we can map the correct theme preset using the enum we've created above.
final Map<AppTheme, ThemeData> appThemeData = {
AppTheme.redDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.red,
primarySwatch: Colors.red,
textTheme: GoogleFonts.dancingScriptTextTheme(),
),
AppTheme.redLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.red,
primarySwatch: Colors.red,
textTheme: GoogleFonts.oswaldTextTheme(),
),
AppTheme.blueLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo,
primarySwatch: Colors.indigo,
textTheme: GoogleFonts.ubuntuTextTheme(),
),
AppTheme.blueDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.indigo,
primarySwatch: Colors.indigo,
textTheme: GoogleFonts.irishGroverTextTheme(),
),
AppTheme.greenDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.green,
primarySwatch: Colors.green,
textTheme: GoogleFonts.architectsDaughterTextTheme(),
),
AppTheme.greenLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.green,
primarySwatch: Colors.green,
textTheme: GoogleFonts.permanentMarkerTextTheme(),
),
};
With that done, your themes.dart
file must look like this.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
enum AppTheme {
redDark,
redLight,
blueLight,
blueDark,
greenDark,
greenLight,
}
final Map<AppTheme, ThemeData> appThemeData = {
AppTheme.redDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.red,
primarySwatch: Colors.red,
textTheme: GoogleFonts.dancingScriptTextTheme(),
),
AppTheme.redLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.red,
primarySwatch: Colors.red,
textTheme: GoogleFonts.oswaldTextTheme(),
),
AppTheme.blueLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo,
primarySwatch: Colors.indigo,
textTheme: GoogleFonts.ubuntuTextTheme(),
),
AppTheme.blueDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.indigo,
primarySwatch: Colors.indigo,
textTheme: GoogleFonts.irishGroverTextTheme(),
),
AppTheme.greenDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.green,
primarySwatch: Colors.green,
textTheme: GoogleFonts.architectsDaughterTextTheme(),
),
AppTheme.greenLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.green,
primarySwatch: Colors.green,
textTheme: GoogleFonts.permanentMarkerTextTheme(),
),
};
3: Creating New Cubit
Using the bloc extension create a new cubit in the lib directory. In case you don't know what cubit is? I'd suggest you get familiar with bloc library. You can also get great videos on youtube on bloc & cubit.
So, with that out of the way, we will create a new cubit namely dynamic_theme with the help of bloc extension. That will create two files under the cubit folder.
- dynamic_theme_cubit.dart
- dynamic_theme_state.dart
Dynamic Theme State
This is what it will look like at first
part of 'dynamic_theme_cubit.dart';
@immutable
abstract class DynamicThemeState {}
class DynamicThemeInitial extends DynamicThemeState {}
As we will be dealing with a single state, we don't need to have multiple State classes. And we also don't need the abstract class. So, delete unnecessary classes and also abstract keywords from the main state class.
After performing those deletions, your file should look like this.
part of 'dynamic_theme_cubit.dart';
@immutable
class DynamicThemeState {}
After this, we will create a final field of type AppTheme (enum) and also create a constructor to initialize that field.
part of 'dynamic_theme_cubit.dart';
@immutable
class DynamicThemeState {
final AppTheme theme;
const DynamicThemeState({
required this.theme,
});
}
With that, we are done with the state file. yeah! that's that simple.
Dynamic State Cubit
Now the cubit file will look something like this at first.
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'dynamic_theme_state.dart';
class DynamicThemeCubit extends Cubit<DynamicThemeState> {
DynamicThemeCubit() : super(DynamicThemeInitial());
}
Now the real fun begins. Instead of extending Cubit Abstract Class we will extend HydratedCubit Class. Then we need to override two methods toJson and fromJson. This is because Hydrated Cubit will persist the state as JSON in the device storage media. So we have to implement the state conversion ourselves. And also we will provide the initial state for the cubit. I'll pick the redDark theme as a initial state.
import 'package:flutter_dynamic_theme/themes/themes.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
part 'dynamic_theme_state.dart';
class DynamicThemeCubit extends HydratedCubit<DynamicThemeState> {
DynamicThemeCubit()
: super(
const DynamicThemeState(theme: AppTheme.redDark),
);
@override
DynamicThemeState? fromJson(Map<String, dynamic> json) {
// TODO: implement fromJson
throw UnimplementedError();
}
@override
Map<String, dynamic>? toJson(DynamicThemeState state) {
// TODO: implement toJson
throw UnimplementedError();
}
}
Now Lets implement the toJson function. This function takes State as a parameter which is basically AppTheme (enum), We will convert the enum value to string and create a map with the key theme and return it.
@override
Map<String, dynamic>? toJson(DynamicThemeState state) {
final theme = {
'theme': state.theme.toString(),
};
return theme;
}
With that done, now we will implement the fromJSON method, this function takes Map json as a parameter. we will extract the theme value out of that map and assign it to the theme variable.
After that, we will create an AppTheme variable, and using the firstWhere() function to the values property will check for the matching string and assign the appropriate value to it.
@override
DynamicThemeState? fromJson(Map<String, dynamic> json) {
final theme = json['theme'];
AppTheme currentTheme =
AppTheme.values.firstWhere((e) => e.toString() == theme);
return DynamicThemeState(theme: currentTheme);
}
With that done, now finally we will create a function that changes the theme. It will take AppTheme as a parameter and emit a new state including that AppTheme variable.
void changeTheme({required AppTheme theme}) =>
emit(DynamicThemeState(theme: theme));
With everything done, your dynamic_theme_cubit.dart file will look something like this.
import 'package:flutter_dynamic_theme/themes/themes.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
part 'dynamic_theme_state.dart';
class DynamicThemeCubit extends HydratedCubit<DynamicThemeState> {
DynamicThemeCubit()
: super(
const DynamicThemeState(theme: AppTheme.redDark),
);
@override
DynamicThemeState? fromJson(Map<String, dynamic> json) {
final theme = json['theme'];
AppTheme currentTheme =
AppTheme.values.firstWhere((e) => e.toString() == theme);
return DynamicThemeState(theme: currentTheme);
}
@override
Map<String, dynamic>? toJson(DynamicThemeState state) {
final theme = {
'theme': state.theme.toString(),
};
return theme;
}
void changeTheme({required AppTheme theme}) =>
emit(DynamicThemeState(theme: theme));
}
4: Creating Pages
we will take the bottom-up approach here. There will be two pages altogether
- Home Page
- Settings Page
Home Page
will contain some text to demonstrate the Theme Change.
Settings Page
will contain all the themes as a list which when pressed will trigger a function that will change the app theme.
Settings Page
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dynamic_theme/cubit/dynamic_theme_cubit.dart';
import 'package:flutter_dynamic_theme/themes/themes.dart';
class SettingPage extends StatelessWidget {
const SettingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('App Settings'),
backgroundColor: Theme.of(context).primaryColor,
),
body: BlocBuilder<DynamicThemeCubit, DynamicThemeState>(
builder: (context, state) {
return ListView.builder(
itemCount: AppTheme.values.length,
itemBuilder: (context, index) {
final itemAppTheme = AppTheme.values[index];
return Card(
elevation: 5,
color: appThemeData[itemAppTheme]!.primaryColor,
child: ListTile(
onTap: () => context
.read<DynamicThemeCubit>()
.changeTheme(theme: itemAppTheme),
title: Text(
itemAppTheme.toString(),
style: appThemeData[itemAppTheme]!.textTheme.bodyText1,
),
),
);
},
);
},
),
);
}
}
Home Page
import 'package:flutter/material.dart';
import 'package:flutter_dynamic_theme/pages/settings_page.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Theme Demo'),
backgroundColor: Theme.of(context).primaryColor,
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Home Page',
style: Theme.of(context).textTheme.headline3!.copyWith(
color: Theme.of(context).primaryColor,
),
),
Text(
'Dynamic Theme Demo With Hydrated Cubit',
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Theme.of(context).primaryColor,
),
)
],
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor,
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const SettingPage(),
)),
child: const Icon(Icons.settings),
),
);
}
}
Root Widget
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dynamic_theme/cubit/dynamic_theme_cubit.dart';
import 'package:flutter_dynamic_theme/pages/home_page.dart';
import 'package:flutter_dynamic_theme/themes/themes.dart';
class RootWidget extends StatelessWidget {
const RootWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider<DynamicThemeCubit>(
create: (context) => DynamicThemeCubit(),
child: BlocBuilder<DynamicThemeCubit, DynamicThemeState>(
builder: (context, state) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: appThemeData[state.theme],
home: const HomePage(),
initialRoute: '/',
);
},
),
);
}
}
6: main.dart file
import 'package:flutter/material.dart';
import 'package:flutter_dynamic_theme/pages/root_widget.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getTemporaryDirectory(),
);
runApp(const RootWidget());
}