Light/Dark App Theme with Custom Color in Flutter

Kinjal Dhamat
5 min readMar 2, 2023

--

If you’re working on a Flutter project and want to provide a personalized and comfortable user experience, it’s essential to implement a custom Light/Dark app theme with your own colors.

In this article, we’ll go through the process of adding custom colors to your Light/Dark app theme. By the end of this article, you’ll know how to create a custom app theme that reflects your brand’s identity. Let’s dive in!

Table of content :

1. Create Light/Dark theme

2. Create provider for theme state using river-pod

3. Use theme in app

4. Add custom colors

5. Full source code

1. Create Light/Dark theme:

To create themes for light and dark modes in Flutter, we can use the ThemeData class and customize its colors and other properties according to our needs. To simplify the process of getting a ThemeData object based on the selected light/dark theme, we can create a method that returns a ThemeData object with different values for properties like scaffoldBackgroundColor, bodyColor, thumbColor, listTileTheme, and appBarTheme.

ThemeData getAppTheme(BuildContext context, bool isDarkTheme) {
return ThemeData(
scaffoldBackgroundColor: isDarkTheme ? Colors.black : Colors.white,
textTheme: Theme.of(context)
.textTheme
.copyWith(
titleSmall:
Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: 11),
)
.apply(
bodyColor: isDarkTheme ? Colors.white : Colors.black,
displayColor: Colors.grey,
),
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.all(
isDarkTheme ? Colors.orange : Colors.purple),
),
listTileTheme: ListTileThemeData(
iconColor: isDarkTheme ? Colors.orange : Colors.purple),
appBarTheme: AppBarTheme(
backgroundColor: isDarkTheme ? Colors.black : Colors.white,
iconTheme:
IconThemeData(color: isDarkTheme ? Colors.white : Colors.black54)),
);
}

2. Create provider for theme state using river-pod:

We can use Riverpod to manage the app theme state by storing a boolean value that determines whether the app is currently using the light or dark theme. To do this, we can use the StateProvider class provided by Riverpod.

final appThemeProvider = StateProvider<bool>((ref) => false);

3. Use theme in app:

We’re using Riverpod by wrapping the MyApp widget with ProviderScope to access all providers throughout the app. MyApp extends ConsumerWidget, which allows us to access the WidgetRef object in the build method and access any Riverpod providers using the ref variable. getAppTheme(context, ref.watch(appThemeProvider)) method listen any change in app theme and update app accordingly.

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

@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Flutter Light/Dark Theme',
debugShowCheckedModeBanner: false,
theme: getAppTheme(context, ref.watch(appThemeProvider)),
home: const MyHomePage(),
);
}
}

ref.read(appThemeProvider.notifier).state = value. we are update theme state in appThemeProvider when switch state is change from light/dark mood.

Switch(
activeColor: Colors.orange,
onChanged: (value) {
ref.read(appThemeProvider.notifier).state = value;
},
value: isDarkMode )

4. Add custom color :

Currently, our app uses the same color for all icons and texts. To apply different colors to specific icons we have to create extension for Theme. Create class and extends with ThemeExtension and add necessary fields with you want to customize.

class AppColors extends ThemeExtension<AppColors> {
final Color? color1;
final Color? color2;
final Color? color3;

const AppColors({
required this.color1,
required this.color2,
required this.color3,
});

@override
AppColors copyWith({
Color? color1,
Color? color2,
Color? color3,
}) {
return AppColors(
color1: color1 ?? this.color1,
color2: color2 ?? this.color2,
color3: color3 ?? this.color3,
);
}

@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) {
return this;
}
return AppColors(
color1: Color.lerp(color1, other.color1, t),
color2: Color.lerp(color2, other.color2, t),
color3: Color.lerp(color3, other.color3, t),
);
}
}

Now add this extension attribute in ThemeData in our created method getAppTheme and define theme according colors.

extensions: <ThemeExtension<AppColors>>[
AppColors(
color1: isDarkTheme ? Colors.blue : Colors.blueGrey,
color2: isDarkTheme ? Colors.pink : Colors.pinkAccent,
color3: isDarkTheme ? Colors.yellow : Colors.limeAccent,
),

Create other extension function which we use to access custom color easily.

AppColors colors(context) => Theme.of(context).extension<AppColors>()!;

We can access this colors in widget using colors(context).color1. If we will not specify the Icon color then it will fetch color from listTileTheme.

ListTile(
leading: Icon(Icons.chat_outlined, color: colors(context).color3),
title: Text( "Help Center", style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
leading: const Icon(Icons.notifications),
title: Text("Notification", style: Theme.of(context).textTheme.titleSmall),
),

5. Full source code:

Here is the code for theme class:

import 'package:flutter/material.dart';

AppColors colors(context) => Theme.of(context).extension<AppColors>()!;

ThemeData getAppTheme(BuildContext context, bool isDarkTheme) {
return ThemeData(
extensions: <ThemeExtension<AppColors>>[
AppColors(
color1: isDarkTheme ? Colors.blue : Colors.green,
color2: isDarkTheme ? Colors.pink : Colors.blue,
color3: isDarkTheme ? Colors.yellow : Colors.red,
),
],
scaffoldBackgroundColor: isDarkTheme ? Colors.black : Colors.white,
textTheme: Theme.of(context)
.textTheme
.copyWith(
titleSmall:
Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: 12),
)
.apply(
bodyColor: isDarkTheme ? Colors.white : Colors.black,
displayColor: Colors.grey,
),
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.all(
isDarkTheme ? Colors.orange : Colors.purple),
),
listTileTheme: ListTileThemeData(
iconColor: isDarkTheme ? Colors.orange : Colors.purple),
appBarTheme: AppBarTheme(
backgroundColor: isDarkTheme ? Colors.black : Colors.white,
iconTheme:
IconThemeData(color: isDarkTheme ? Colors.white : Colors.black54)),
);
}

@immutable
class AppColors extends ThemeExtension<AppColors> {
final Color? color1;
final Color? color2;
final Color? color3;

const AppColors({
required this.color1,
required this.color2,
required this.color3,
});

@override
AppColors copyWith({
Color? color1,
Color? color2,
Color? color3,
}) {
return AppColors(
color1: color1 ?? this.color1,
color2: color2 ?? this.color2,
color3: color3 ?? this.color3,
);
}

@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) {
return this;
}
return AppColors(
color1: Color.lerp(color1, other.color1, t),
color2: Color.lerp(color2, other.color2, t),
color3: Color.lerp(color3, other.color3, t),
);
}
}

Here is code of our main screen:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:light_dark_mode/provider%20/app_theme_provider.dart';
import 'package:light_dark_mode/utils/app_theme.dart';

void main() {
runApp(const ProviderScope(child: MyApp()));
}

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

@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Flutter Light/Dark Theme',
debugShowCheckedModeBanner: false,
theme: getAppTheme(context, ref.watch(appThemeProvider)),
home: const MyHomePage(),
);
}
}

class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = ref.watch(appThemeProvider);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: const Icon(Icons.arrow_back_ios_sharp),
actions: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 15.0),
child: Icon(Icons.add_circle_outline),
)
],
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListView(
children: [
CircleAvatar(
radius: 60,
backgroundColor: Colors.grey,
child: Padding(
padding: const EdgeInsets.all(1), // Border radius
child: ClipRRect(
borderRadius: BorderRadius.circular(60),
child: Image.asset(
"assets/ic_profile.jpeg",
fit: BoxFit.fill,
width: 120,
height: 120,
)),
),
),
Container(
margin: const EdgeInsets.only(top: 10, bottom: 60),
alignment: Alignment.center,
child: Text(
"Testing User",
style: Theme.of(context).textTheme.titleLarge,
),
),
ListTile(
leading: Icon(isDarkMode ? Icons.brightness_3 : Icons.sunny),
title: Text(
isDarkMode ? "Dark mode" : "Light mode",
style: Theme.of(context).textTheme.titleSmall,
),
trailing: Consumer(builder: (context, ref, child) {
return Transform.scale(
scale: 0.7,
child: Switch(
activeColor: Colors.orange,
onChanged: (value) {
ref.read(appThemeProvider.notifier).state = value;
},
value: isDarkMode,
),
);
}),
),
ListTile(
leading: Icon(Icons.grid_on_sharp, color: colors(context).color1,),
title: Text(
"Story",
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
leading: Icon(Icons.settings, color: colors(context).color2),
title: Text("Settings and Privacy",
style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
leading: Icon(Icons.chat_outlined, color: colors(context).color3),
title: Text(
"Help Center",
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
leading: const Icon(Icons.notifications),
title: Text(
"Notification",
style: Theme.of(context).textTheme.titleSmall,
),
),
],
),
),
);
}
}

You can find full code here: Source code.

I hope this article has provided helpful insights into customising the app theme. In this blog, we demonstrated how to customize the colors of individual icons in Flutter using ThemeExtension. While we used colors as an example, we can customize any field to achieve the desired effect.

Thanks for reading this article ❤

--

--

Kinjal Dhamat

Flutter | FlutterFlow | Dart | Android | Kotlin | React Native | Node | Lead Software Engineer