tl;dr
- The easiest and quickest way to convert a hex
String
to aColor
is by using the unnamed constructor and prepending0xFF
like this:Color(0xFFFF0000)
- Another option is to parse the string using the static .parse() method defined on
int
- There are also packages for this
- If you want to use this for
MaterialColor
to provide a primary color for your app, click here
Colors in computers can be described using different models. The most common way in the web is via the red, green and blue channel, which is part of the additive RGB color model. Many people use the hexadecimal numeral system, which lets you describe a color using only 6 characters instead of three decimal values from 0 to 255.
Let’s clarify which ways we have to process this kind of notation in Flutter.
The quickest way: the default Color
constructor
Although the Color class in Flutter does not provide a String
constructor that can parse hexadecimal numbers, its default constructor expects an int
. Lastly, a hexadecimal number is nothing else but an int
. But how do we tell the Dart compiler that what we enter is in the hexadecimal system instead of the decimal system? Because this clearly doesn’t work:
1const myHexInt = Color(FF00FF); // Leads to "Undefined name FF00FF" error
2const myHexValue = Color('FF00FF'); // Makes our argument a String
For historic reasons, starting with the programing language C, a notation for hexadecimal numbers started to become necessary. It was a design decision to denote it with a 0x
prefix.
Almost every modern language still follows this convention.
Keep in mind that it’s an ARGB notation. This means that it has 8 characters where the first two characters determine the alpha or opacity.
1/// 0xFFFF0000
2/// └└++++++++▶ 0x -> Hexadecimal prefix
3/// └└++++++▶ FF -> 100 % Opacity
4/// └└++++▶ FF -> 100 % Red
5/// └└++▶ 00 -> 0 % Green
6/// └└▶ 00 -> 0 % Blue
7const myRedColorInt = 0xFFFF0000; // This works!
8const Color myRedColor = Color(myRedColorInt); // Here we can put the int now
Pros / Cons
This ways seems very pragmatic as we have achieved our goal in very limited time and with little effort. But it’s also not very flexible. So here are the pros and cons of this method:
Advantages
- Pragmatic and quick solution - can be done in a matter of seconds
- No need to install a package or create a class with custom code
- Since we can make all the colors
const
, it can be processed during compile time and consumes very little resources
Disadvantages
- The same repetitive task of calling the
Color
constructor. This sometimes feels wrong, especially in bigger projects - If the input is dynamic (e. g. comes from an API), it’s presumably a
String
like'#ababab'
. This won’t work
The most elegant way: conversion from String
The above mentioned approach has a problem: if the input comes from an external source like an API or a database, we can’t use it. That’s because we are setting the value to a static int
. However, the value coming from external will be most likely a String
.
Now how do we convert a String
to a hexadecimal int
?
Let’s write our own function for this:
1Color? fromHexString(String input) {
2 String normalized = input.replaceFirst('#', '');
3
4 if (normalized.length == 6) {
5 normalized = 'FF$normalized';
6 }
7
8 if (normalized.length != 8) {
9 return null;
10 }
11
12 final int? decimal = int.tryParse(normalized, radix: 16);
13 return decimal == null ? null : Color(decimal);
14}
First we replace the first occurrence of #
with an empty String
. This makes our function work with either #
as the prefix or not.
After that, we check the number of characters in the input String
. For now, we only accept RRGGBB
and AARRGGBB
, making 6 and 8 the only valid lengths for the input String
.
If it has a length of 6, we still need the alpha value to see anything so we prepend FF
to the String
.
Finally, we try to transform the String
to an int
with a function called int.tryParse(), which returns null
on error (compared to int.parse()).
The result is put into the Color
constructor if it’s not null
and returned.
3-digit color strings
Did you know that in CSS you can also define HEX colors with a 3-digit color string?
If the red, green and blue channels each have two identical characters, you can omit the second one. For example f00
will be interpreted as ff0000
.
If we want to extend this to alpha channel as well, we need to support 4-digit color strings.
Let’s extend our code to handle these cases, too.
1Color? convert(String input) {
2 const int base = 16;
3
4 String normalized = input.replaceFirst('#', '').toUpperCase();
5
6 if (normalized.length < 3) {
7 return null;
8 }
9
10 final String r = '${normalized[0]}${normalized[0]}';
11 final String g = '${normalized[1]}${normalized[1]}';
12 final String b = '${normalized[2]}${normalized[2]}';
13
14 if (normalized.length == 3) {
15 // Example: f00 => ff0000 => Red, 100 % Alpha
16 final int? decimal = int.tryParse('FF$r$g$b', radix: base);
17 return decimal == null ? null : Color(decimal);
18 }
19
20 if (normalized.length == 4) {
21 // Example: f00f => ff0000ff => Red, 100 % Alpha
22 final String a = '${normalized[3]}${normalized[3]}';
23 final int? decimal = int.tryParse('$r$g$b$a', radix: base);
24 return decimal == null ? null : Color(decimal);
25 }
26
27 if (normalized.length == 6) {
28 // Example: ff0000 => Rot, Red % Alpha
29 normalized = 'FF$normalized';
30 }
31
32 // Example: ffff0000 => Red, 100 % Alpha
33 final int? decimal = int.tryParse(normalized, radix: base);
34 return decimal == null ? null : Color(decimal);
35}
Now we have support for 3-digit, 4-digit, 6-digit and 8-digit hex color strings. Great!
Wrapping it with a class
The function works great but where do we put it? We don’t want to duplicate the function everywhere we want to use it. Since we are using an OOP language, we also can’t just export it. Actually, we need to wrap it with a class.
1class HexColor extends Color {
2 HexColor._(super.value);
3
4 static int? _buildColorIntFromHex(String input) {
5 const int base = 16;
6
7 String normalized = input.replaceFirst('#', '').toUpperCase();
8
9 if (normalized.length < 3) {
10 return null;
11 }
12
13 final String r = '${normalized[0]}${normalized[0]}';
14 final String g = '${normalized[1]}${normalized[1]}';
15 final String b = '${normalized[2]}${normalized[2]}';
16
17 if (normalized.length == 3) {
18 // Example: f00 => ff0000 => Red, 100 % Alpha
19 return int.tryParse('FF$r$g$b', radix: base);
20 }
21
22 if (normalized.length == 4) {
23 // Example: f00f => ff0000ff => Red, 100 % Alpha
24 final String a = '${normalized[3]}${normalized[3]}';
25 return int.tryParse('$r$g$b$a', radix: base);
26 }
27
28 if (normalized.length == 6) {
29 // Example: ff0000 => Rot, Red % Alpha
30 normalized = 'FF$normalized';
31 }
32
33 // Example: ffff0000 => Red, 100 % Alpha
34 return int.tryParse(normalized, radix: base);
35 }
36
37 factory HexColor(final String hexColorText) {
38 int? hexColor = _buildColorIntFromHex(hexColorText);
39
40 if (hexColor == null) {
41 throw HexColorParseException();
42 }
43
44 return HexColor._(hexColor);
45 }
46}
47
48class HexColorParseException implements Exception {}
We have created this HexColor
class which extends Color
and whose main responsibility is providing the convenience constructor we are missing in the super
class.
In order to achieve this, we make our function static
and let it return an int
instead of a Color
.
We then create a factory
constructor because we want to handle the case of an invalid hex color String
. In this case we throw the newly created Exception
: HexColorParseException
.
We create a private constructor (._()
) inside the class which we then call from inside our factory
constructor.
Now we can easily create a new Color
from a hex color string by typing these lines:
1Color myColorFromAHexString = HexColor('f00');
Pros / Cons
This kind of implementation looks rather sophisticated but it has taken some time to implement. Let’s look at the pros and cons at a glance:
Advantages
- We handle all cases we need (also 3-digit and 4-digit hex codes)
- We know exactly what happens as we have written the code ourselves
- We can use dynamic hex strings and convert them easily
Disadvantages
- Compared to directly calling the
Color
constructor, we can’t useconst
. So if we know the color at compile-time, we should always use theconst
constructor as it’s more efficient - There is an implementation effort
- We need to maintain the code ourselves
The most efficient way: using the hexcolor
package
Flutter and Dart have a very rich ecosystem consisting of thousands of packages created by the dev community.
As you might have guessed, there is also one for the exact purpose: converting a hex color string to a Color
class.
The package has the descriptive name hexcolor.
So if you need the flexibility of our implementation but don’t want to mess with own code, you might go for this package.
It has the same API as our implementation (uses a HexColor
class).
Pros / Cons
Using this package has the usual pros and cons of using a package. It produces very quick results but is also a dependency which wants to be maintained.
Advantages
- No need to implement anything
Disadvantages
- You need to do dependency management
- No control over changes
MaterialColor
When you create a MaterialApp, you start with an app with a blue primary color.
This is due to this code:
1class MyApp extends StatelessWidget {
2 const MyApp({super.key});
3
4 // This widget is the root of your application.
5 @override
6 Widget build(BuildContext context) {
7 return MaterialApp(
8 title: 'Flutter Demo',
9 theme: ThemeData(
10 primarySwatch: Colors.blue, // defaults to blue in a newly created project
11 ),
12 home: const MyHomePage(title: 'Flutter Clutter Color Demo'),
13 );
14 }
15}
If you want to change this, there is a problem: apart from the predefined colors like Colors.red
, Colors.green
etc. you can’t define one.
That’s because Colors.blue
doesn’t return a Color
but a MaterialColor.
A MaterialColor
does not only contain one color, but different shades of that color being mapped to respective int
values.
Wouldn’t it be nice if we could use our newly created HexColor
class for this purpose as well?
Let’s change our class to add this possibility.
1int toHex() => int.parse(
2 '0xFF'
3 '${alpha.toRadixString(16).padLeft(2, '0')}'
4 '${red.toRadixString(16).padLeft(2, '0')}'
5 '${green.toRadixString(16).padLeft(2, '0')}'
6 '${blue.toRadixString(16).padLeft(2, '0')}',
7);
8
9MaterialColor toMaterialColor() {
10 Map<int, Color> color = {
11 50: Color.fromRGBO(red, green, blue, .1),
12 100: Color.fromRGBO(red, green, blue, .2),
13 200: Color.fromRGBO(red, green, blue, .3),
14 300: Color.fromRGBO(red, green, blue, .4),
15 400: Color.fromRGBO(red, green, blue, .5),
16 500: Color.fromRGBO(red, green, blue, .6),
17 600: Color.fromRGBO(red, green, blue, .7),
18 700: Color.fromRGBO(red, green, blue, .8),
19 800: Color.fromRGBO(red, green, blue, .9),
20 900: Color.fromRGBO(red, green, blue, 1),
21 };
22
23 return MaterialColor(_toHex(), color);
24}
The MaterialColor
constructor expects an int
as the first value and a base color as the second value. In order to get the int
, we need to convert our color back. We do this again by using int.parse()
and concatenating our hex color string.
After that we build up the color map with all the opacity values from .1
to 1
. The rgb values are available because we are inside the Color
class.
Now we can instantiate our MaterialApp
like this:
1MaterialApp(
2 title: 'Flutter Demo',
3 theme: ThemeData(
4 primarySwatch: HexColor('31a8e0').toMaterialColor(),
5 ),
6 home: const MyHomePage(title: 'Flutter Clutter Color Demo'),
7);
Now we can instantly use the defined primary color in our widgets:
Conclusion
Working with hex colors might be a little bit counter-intuitive because the default Dart library does not offer a direct, convenient way to do the conversion.
However, going from String
to int
with a bit of sanitizing and putting all this into a named constructor makes life easier.
Muhammadaziz
Marc
In reply to Muhammadaziz's comment