Working with hexadecimal color strings

Working with hexadecimal color strings
No time to read?
tl;dr
  • The easiest and quickest way to convert a hex String to a Color is by using the unnamed constructor and prepending 0xFF 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
Tip
This is contrary to what it’s like in CSS. There you have an RGBA model, making the last two digits determine the alpha and not the first two.

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 use const. So if we know the color at compile-time, we should always use the const 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:

Example app utilizing our new `HexCode` class to define the primary color
The blue color is defined using our HexCode class

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.

Comments (2) ✍️

Muhammadaziz

Awesome tutorial! Also, the website feels so good and aesthetic! Thank you for your passion!
Reply to Muhammadaziz

Marc
In reply to Muhammadaziz's comment

Thank you for your appreciation. Feels good to read this! 🙂
Reply to Marc

Comment this 🤌

You are replying to 's commentRemove reference