Dark Mode: three Lint checks to help
Tags: android, darkmode, kotlin, lint
Published: 2020-04-14 10:43:06

---
title: Dark Mode: three Lint checks to help
published: true
description: Three Lint checks to help you developing dark mode on Android
tags: android, darkmode, kotlin, lint
---

Implementing Night Mode in Android is pretty straightforward: you have a theme with attributes and you can just define those attributes in the `values-night` qualifier and that's it. Five minutes and you are done. Well...

Most likely, reality is different: you have a project 4-5 years old, you don't even have a second theme, you have been using hardcoded colors `#FFFFFF` everywhere, maybe at times you felt brave and used instead a reference color `@color/white` . And maybe some other times you had to tint something programmatically with your friend `ContextCompat.getColor(context, R.color.white)`to tint that icon that comes from an API.  
So now your code is polluted with things that will be hard to migrate to night: you have white defined directly, as a reference and in code. Guess what, white is not white in night mode anymore :)

And even if you have a brand new project, how do you make sure someone in future will not do the same, how do you enforce that night mode is implemented in every commit/PR? That's where Lint checks come in to help us!

Most of Android developers are familiar with Lint, but just as a refresh, Lint is a static analysis tool which looks for different types of bugs; you can find out more at [https://developer.android.com/studio/write/lint](https://developer.android.com/studio/write/lint). 

Android comes with some Lint checks already made for you, but you can extend them and add your own! Here I'm going to show you how to add three Lint checks to your project: one to detect the usage of a direct color (eg. `#FFFFFF`), one to detect if you have defined a color and its night mode equivalent, and one to check all those color's name that may be problematic (eg. `white, red, green`).

I’ve already wrote a post about writing a custom Lint rule ([https://dev.to/dbottillo/how-to-write-a-custom-rule-in-lint-23gf](https://dev.to/dbottillo/how-to-write-a-custom-rule-in-lint-23gf)) so I'm going to assume you know how to write one and focus on the specific content of the three rules.

The first two rules are heavily inspired from Saurabh Arora's post: https://proandroiddev.com/making-android-lint-theme-aware-6285737b13bc

## Direct Color Detector

The first rule is very simple: you shouldn't use any hardcoded color values like `#FFFFFF`. 
Let's do a bit of TDD and write a test to validate what we want from the Lint rule:
```kotlin
    @Test
    fun `should report a direct color violation`() {
        val contentFile = """<?xml version="1.0" encoding="utf-8"?>
                <View 
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        android:id="@+id/toolbar"
                        android:background="#453344"
                        android:foreground="#667788"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content" />"""
        TestLintTask.lint()
                .files(TestFiles.xml("res/layout/toolbar.xml", contentFile).indented())
                .issues(DIRECT_COLOR_ISSUE)
                .run()
                .expect("""
                    |res/layout/toolbar.xml:5: Error: Avoid direct use of colors in XML files. This will cause issues with different theme (eg. night) support [DirectColorUse]
                    |                    android:background="#453344"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |res/layout/toolbar.xml:6: Error: Avoid direct use of colors in XML files. This will cause issues with different theme (eg. night) support [DirectColorUse]
                    |                    android:foreground="#667788"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |2 errors, 0 warnings""".trimMargin())
    }
```
That's a lot to get through, let's break it down:
```kotlin
    val contentFile = """<?xml version="1.0" encoding="utf-8"?>
                <View 
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        android:id="@+id/toolbar"
                        android:background="#453344"
                        android:foreground="#667788"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content" />"""
```
So, here we are defining the content of an hypothetic XML file containing a view tag with few attributes, both `background` and `foreground` contain an hardcoded color so we should expect two violations for both of them.
Next, we can build a test lint task, which is a class lint provides to test your test files:
```kotlin
    TestLintTask.lint()
                .files(TestFiles.xml("res/layout/toolbar.xml", contentFile).indented())
```
With `.files()` you can simulate to pass a number of files to lint. Think of them like all your project's file, in this case I'm just passing a fake `res/layout/toolbar.xml` whose `contentFile` is the xml we defined in the previous section.
```kotlin
    TestLintTask.lint()
    .files(TestFiles.xml("res/layout/toolbar.xml", contentFile).indented())
    .issues(DIRECT_COLOR_ISSUE)
```
With `.issues` you can pass a number of issues lint will use to perform the detection, in this case:
```kotlin
    val DIRECT_COLOR_ISSUE = Issue.create("DirectColorUse",
            "Direct color used",
            "Avoid direct use of colors in XML files. This will cause issues with different theme (eg. night) support",
            CORRECTNESS,
            6,
            Severity.ERROR,
            Implementation(DirectColorDetector::class.java, Scope.RESOURCE_FILE_SCOPE)
    )
```
So this is how you can create a new issue in Lint: `DirectColorUse` is the id of the issue, then there is a brief description and an explanation, followed by category, priority, severity and the implementation. The `DirectColorDetector.kt` file is the one responsible for detection. If you try to run the test of course it will fail since this file doesn't exist yet. Let’s create an empty one so we can run the test:
```kotlin
    class DirectColorDetector : ResourceXmlDetector() {
    }
```
Finally at the end of the test we have:
```kotlin
    TestLintTask.lint()
    .files(TestFiles.xml("res/layout/toolbar.xml", contentFile).indented())
    .issues(DIRECT_COLOR_ISSUE)
    .run()
    .expect("""
                    |res/layout/toolbar.xml:5: Error: Avoid direct use of colors in XML files. This will cause issues with different theme (eg. night) support [DirectColorUse]
                    |                    android:background="#453344"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |res/layout/toolbar.xml:6: Error: Avoid direct use of colors in XML files. This will cause issues with different theme (eg. night) support [DirectColorUse]
                    |                    android:foreground="#667788"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |2 errors, 0 warnings""".trimMargin())
```
`run()` makes the Lint task running and `expect` is a convenient method of the `TestLintTask` to assert what's the outcome of running Lint on those files. Here we are expecting those two violations about the background and the foreground values. Of course if you run this test it will fail since we haven't actually implemented `DirectColorDetector` yet. Let's do that:
```kotlin
    class DirectColorDetector : ResourceXmlDetector() {
    
        override fun getApplicableAttributes(): Collection<String>? = listOf(
                "background", "foreground", "src", "textColor", "tint", "color",
                "textColorHighlight", "textColorHint", "textColorLink", 
                "shadowColor", "srcCompat")
    
        override fun visitAttribute(context: XmlContext, attribute: Attr) {
            if (attribute.value.startsWith("#")) {
                context.report(
                        DIRECT_COLOR_ISSUE,
                        context.getLocation(attribute),
                        DIRECT_COLOR_ISSUE.getExplanation(TextFormat.RAW))
            }
        }
    }
```
Extending `ResourceXmlDetector` means we have a chance to do something when Lint is going through  `XML` files, and through overriding some methods we can perform our check. Here we are using `getApplicableAttributes()` to notify Lint that the detector should be running on those attributes of any XML file. In the `visitAttribute` we have a chance to look at the attribute value and if it starts with `#` it means we can report a violation. You can do that by using the `context` (which is NOT the Android context but the Lint context) and call `report` on it with the values of the `DIRECT_COLOR_ISSUE` defined before.

Here we go! Now if you run the test it will be green. 

## Missing Night Color

This check is about having a color defined without a night version. And as we did before, let's first write a test to validate our assumption:
```kotlin
    @Test
    fun `should report a missing night color violation`() {
        val colorFile = TestFiles.xml("res/values/colors.xml",
                """<?xml version="1.0" encoding="utf-8"?>
                <resources> 
                    <color name="color_primary">#00a7f7</color>
                    <color name="color_secondary">#0193e8</color>
                </resources>""").indented()
        val colorNightFile = TestFiles.xml("res/values-night/colors.xml",
                """<?xml version="1.0" encoding="utf-8"?>
                <resources> 
                    <color name="color_primary">#224411</color>
                </resources>""").indented()
        TestLintTask.lint()
                .files(colorFile, colorNightFile)
                .issues(MISSING_NIGHT_COLOR_ISSUE)
                .run()
                .expect("""
                    |res/values/colors.xml:4: Error: Night color value for this color resource seems to be missing. If your app supports night theme, then you should add an equivalent color resource for it in the night values folder. [MissingNightColor]
                    |                <color name="color_secondary">#0193e8</color>
                    |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |1 errors, 0 warnings""".trimMargin())
    }
```
Pretty straightforward, the color `color_secondary` doesn't have a night definition so it should be reported. Let's have a look at the new Issue:
```kotlin
    val MISSING_NIGHT_COLOR_ISSUE = Issue.create("MissingNightColor",
            "Night color missing",
            "Night color value for this color resource seems to be missing. If your app supports night theme, then you should add an" +
                    " equivalent color resource for it in the night values folder.",
            CORRECTNESS,
            6,
            Severity.ERROR,
            Implementation(MissingNightColorDetector::class.java, Scope.RESOURCE_FILE_SCOPE)
    )
```
This is similar to the previous one, of course we have a different id, description and explanation. I'm re-using the same category, priority and severity but feel free to define your own based on your needs. Last is the implementation that points to a new detector called `MissingNightColorDetector`:
```kotlin
    class MissingNightColorDetector : ResourceXmlDetector() {
    
        private val nightModeColors = mutableListOf<String>()
        private val regularColors = mutableMapOf<String, Location>()
    
        override fun appliesTo(folderType: ResourceFolderType): Boolean {
            return folderType == ResourceFolderType.VALUES
        }
    
        override fun getApplicableElements(): Collection<String>? {
            return listOf("color")
        }
    
        override fun afterCheckEachProject(context: Context) {
            regularColors.forEach { (color, location) ->
                if (!nightModeColors.contains(color))
                    context.report(
                            MISSING_NIGHT_COLOR_ISSUE,
                            location,
                            MISSING_NIGHT_COLOR_ISSUE.getExplanation(TextFormat.RAW)
                    )
            }
        }
    
        override fun visitElement(context: XmlContext, element: Element) {
            if (context.getFolderConfiguration()!!.isDefault)
                regularColors[element.getAttribute("name")] = context.getLocation(element)
            else if (context.getFolderConfiguration()!!.nightModeQualifier.isValid)
                nightModeColors.add(element.getAttribute("name"))
        }
    }
```
Ok, this is way more complicated than the previous one! The reason is that for this specific detection, we can't report violations straight away: Lint is going through all project's files as if it was a tree. That means that while it's visiting the `res/values/colors` folder we don't know anything yet about the `res/values-night/colors` which is probably going to be visited later on. So, in this case I'm memorising all the `res/values/colors` in a map of color name and location (we need the location to know where the violation is in the file) and all the night colors, then throw a detection at the end of the project evaluation if they don't match. Let's do it step by step:
```kotlin
    private val nightModeColors = mutableListOf<String>()
    private val regularColors = mutableMapOf<String, Location>()
    
    override fun appliesTo(folderType: ResourceFolderType): Boolean {
        return folderType == ResourceFolderType.VALUES
    }
    
    override fun getApplicableElements(): Collection<String>? {
        return listOf("color")
    }
```
The first two are just internal variables to store the list of night mode colors and the map of colors/location for the regular colors. `appliesTo` lets you specify that, among all the xml files, you are only interested in the `ResourceFolderType.VALUES` so it will skip drawables, layouts, etc. Finally in the `getApplicableElements()` we are telling Lint to call this rule only for the tag `color` since we don't care about `style`, `bool`, etc..
```kotlin
    override fun visitElement(context: XmlContext, element: Element) {
        if (context.getFolderConfiguration()!!.isDefault)
            regularColors[element.getAttribute("name")] = context.getLocation(element)
        else if (context.getFolderConfiguration()!!.nightModeQualifier.isValid)
            nightModeColors.add(element.getAttribute("name"))
    }
```
`visitElement` is the method that we can use when the `color` tag is visited, here it first checks the folder configuration (eg. `default` or `night`). If it's default we add name and location to the map of regular colors, if it's night we just save the color name in the night mode colors list.
```kotlin
    override fun afterCheckEachProject(context: Context) {
        regularColors.forEach { (color, location) ->
            if (!nightModeColors.contains(color))
                context.report(
                        MISSING_NIGHT_COLOR_ISSUE,
                        location,
                        MISSING_NIGHT_COLOR_ISSUE.getExplanation(TextFormat.RAW)
                )
        }
    }
```
Finally with the `afterCheckProject` we have another chance to report a violation after the whole project has been visited, here we can loop through all the regular colors and if we don't find the equivalent in the night mode colors list, we can report a violation. Please pay attention to the location parameter in the `report` method: we are basically telling Lint where the violation occurred in the file.

## Non Semantic Color

The third check is a bit more specific based on the project, but the idea here is that color names like `white` or `red` shouldn't be used. There is a high probability that `white` is not white in night and that `red` is maybe red but not that specific red in night. It would be better to use semantic color: instead of `red` use maybe `error` and instead of `white` use `surface`. So let's write a new test:
```kotlin
    @Test
    fun `should report a non semantic color violation`() {
        val contentFile = """<?xml version="1.0" encoding="utf-8"?>
                <View 
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        android:id="@+id/toolbar"
                        android:background="@color/white"
                        android:foreground="@color/red"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content" />"""
        TestLintTask.lint()
                .files(TestFiles.xml("res/layout/toolbar.xml", contentFile).indented())
                .issues(NON_SEMANTIC_COLOR_ISSUE)
                .run()
                .expect("""
                    |res/layout/toolbar.xml:5: Error: Avoid non semantic use of colors in XML files. This will cause issues with different theme (eg. night) support. For example, use primary instead of black. [NonSemanticColorUse]
                    |                    android:background="@color/white"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |res/layout/toolbar.xml:6: Error: Avoid non semantic use of colors in XML files. This will cause issues with different theme (eg. night) support. For example, use primary instead of black. [NonSemanticColorUse]
                    |                    android:foreground="@color/red"
                    |                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    |2 errors, 0 warnings""".trimMargin())
    }
```
This is very similar to the first test that we wrote, so I'm not going through the details, we are expecting two violations because background is using `@color/white` and foreground is using `@color/red`.
```kotlin
    val NON_SEMANTIC_COLOR_ISSUE = Issue.create("NonSemanticColorUse",
            "Non semantic color used",
            "Avoid non semantic use of colors in XML files. This will cause issues with different theme (eg. night) support. " +
                    "For example, use primary instead of black.",
            CORRECTNESS,
            6,
            Severity.ERROR,
            Implementation(NonSemanticColorDetector::class.java, Scope.RESOURCE_FILE_SCOPE)
    )
```
Again, nothing new here, it's just a new issue definition for `NonSemanticColorUse`, let's jump to the detector:
```kotlin
    class NonSemanticColorDetector : ResourceXmlDetector() {
    
        override fun getApplicableAttributes(): Collection<String>? = listOf(
                "background", "foreground", "src", "textColor", "tint", "color",
                "textColorHighlight", "textColorHint", "textColorLink", 
                "shadowColor", "srcCompat")
    
        override fun visitAttribute(context: XmlContext, attribute: Attr) {
            if (checkName(attribute.value)) {
                context.report(
                        NON_SEMANTIC_COLOR_ISSUE,
                        context.getLocation(attribute),
                        NON_SEMANTIC_COLOR_ISSUE.getExplanation(TextFormat.RAW))
            }
        }
    
        private fun checkName(input: String): Boolean {
            return listOf("black", "blue", "green", "orange", 
                          "teal", "white", "orange", "red").any {
                input.contains(it)
            }
        }
    }
```
This detector is very similar to the first detector about hardcoded colors: we just define which attributes we are interested in and with the `visitAttribute` you can check the attribute value and if it contains any of `black,blue,green,etc..` we report the violation. I also want to mention that this will work for images as well: if you have something like `app:srcCompat="@drawable/ic_repeat_black_24dp"` it will report and for a good reason! If you don't tint that image then it may not work in night.

## Conclusion

With the three new lint checks if you now run Lint you will find them in the reports:

![Lint checks report](https://dev-to-uploads.s3.amazonaws.com/i/33idft1r7wybnway9odm.png)

Of course, this is a starting point, you can decide to go through them and fix all of them in one go or just have a sanity check on how far you are to fix all the potential issue while implementing night mode. My suggestion is to make them not an error in the beginning, otherwise it will fail all your builds, and turning them into error only when you have fixed all of them. Turning them into error helps prevent new code and feature to be added to the project without night mode in mind preventing that to go into the build.

Tip: you can suppress them as you do with any other lint rule:
```xml
    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:srcCompat="@drawable/ic_repeat_black_24dp"
            tools:ignore="NonSemanticColorUse" />
```
Finally, you can find a full implementation on one of my app here: [https://github.com/dbottillo/MTGCardsInfo/commit/9bfd1e051f5c264cb7b5316efd50c6af9723b922](https://github.com/dbottillo/MTGCardsInfo/commit/9bfd1e051f5c264cb7b5316efd50c6af9723b922)

Happy coding!