Preparing for Dark Mode
How we restructured our theming functionality to allow us to bring dark mode to Passenger apps.
6th Mar 2020
At Passenger, our products are shaped by user needs. This can mean we are working on ideas that are brand new, undocumented or simply that no-one else has tackled yet. When it came to the rise in popularity of dark mode, this was no exception. We knew users would want it, so we had to make it happen.
With dark mode for apps finally becoming the norm and more native support being added to various operating systems (macOS Mojave, iOS 13, Android 10, Windows 10), it’s an important consideration for mobile app teams. Benefits include increasing battery life, reducing eye strain and making users feel safer when travelling at night – and it was something we wanted to implement ASAP.
Where we began
When I joined Passenger over a year ago and started getting familiar with the codebase, I found it to be well structured and maintained. However, it soon became apparent that the theming would need to be refactored to allow us to create a dark theme.
Within Android, there are many ways you can approach building your UI. My preference is to use the amazing resource framework built in XML. Using this is pretty standard (though some people opt to build their UI in code) and gives a lot of control over how things are displayed (for different devices/screen sizes/OS versions etc) using resource qualifiers.
Within this system, you can create layouts and various supporting resources such as images, colour references, styles, and themes.
Colour references
The Android Passenger apps had been structured using the resource framework – with styling utilising only the Style/colour references system. Within the app, most things had a colour reference with a semi-standard naming convention (that somewhat matched the iOS version). For example:
- Accent
- AccentDark
- Primary
- PrimaryDark
- Text accent
- Accent text
- Light text
- Dark text
- Base 1
- Base 2
- Base 3
- Base 4
- Base 5
As a newcomer to the codebase none of these made immediate sense, and became problematic when creating an app for a new operator. This was compounded if an operator had darker colours in their palette requiring me to override certain style references to point to something else.
Style references
Style references are almost always a must when creating layouts via the XML interface. Each view uses a style reference (such as @style/MyView) that contains a list of standard properties and values to apply rather than defining the properties over and over. This is very useful as it reduces boilerplate and helps standardise the look and feel of your app.
The codebase utilised style references pretty well, but to design and implement dark theme would require duplication of every style reference (to change the colour references within them to something that would work in dark mode). This would also cause huge duplication of code, especially for references that are independent of each theme (such as text sizes, font weights, paddings etc).
For example:
<style name="PassengerText" parent="android:Widget.TextView">
<item name="android:textColor">@color/lightText</item>
</style>
Would require an additional style added to values-night/styles.xml
for us to change the android:textColor
property.
<style name="PassengerText" parent="android:Widget.TextView">
<item name="android:textColor">@color/darkText</item>
</style>
Multiply this by dozens of style definitions and it results in an unmanageable state of styles.
Android attributes
Within the Android framework there is a list of standard attribute references. You may have seen them or used them before in your codebases. Things like
style="?android:attr/progressBarStyleHorizontal"
Or
android:background="?attr/selectableItemBackground"
These attribute references (denoted by ?attr/
or ?android:attr/
) have been around since Android 1, and were woefully under-documented. With the rise of more complex theming (such as dark theme) and the introduction of Material Design, there are now a lot more properties available for devs to implement for free that will (mostly) Just Work™.
Unless you’ve dug through library or AOSP source code, these attribute references are not very obvious, but they include names like:
- colorPrimary
- colorSecondary
- colorAccent
- colorControlNormal
You can find a good list on Google’s Android dev blog. These attributes will work for most out-of-the-box UI components.
Going dark
The first step to implementing a dark theme was to create a standardised style/colour naming convention. I drafted an extensive document that would ultimately replace all the colour and style references in the app, then created a backing document explaining each colour/reference – what it is and where it’s used.
I opted to use custom attributes rather than just the Android/Material attributes because we already had most of the references in the codebase (making refactoring much easier). This styling refactor would need to be replicated on iOS, and possibly even the web front-end in the future, so having a platform agnostic naming convention that worked for our theming requirements was very important.
I took the previous list of colours, filtered them down to a handful of colours that are used within the app, and standardised the naming.
“Accent”, “Primary”, “Base1”, “Base2”, “Base3” stayed the same as these colours were used in around 80% of the app. All the other colours such as “Base4”, “Base5”, “PrimaryDark”, “AccentDark” were removed (the latter two were re-purposed).
When implementing an operator’s branding in their app, knowing which text colour is appropriate to display on the above colours is crucial. The most important additions to the styling structure were “variants” and “textual” colours. With the new standardisation, each colour reference has a corresponding “text” colour (accent + text_accent, primary + text_primary) and this ensures that text is always legible on its surface.
The next priority was retaining the statefulness of each colour. Before, we had a “normal”, “selected”, and “disabled” reference for some colours, and “primary”, “secondary”, “tertiary” for the rest. I standardised using the naming convention of primary, secondary, and tertiary, so each colour has three variants. This allows colours to be slightly more generic but still work. For example, buttons use the primary colour as the normal state, the secondary as the pressed state, and tertiary as the disabled state; or backgrounds may implement primary, secondary, and tertiary as varying brightnesses or opacities.
Lastly came implementation of any colours/references that did not exist before i.e.
- Default window background colour
- Empty list state background colour
- Error state background colour
- “success/warning/failure” colours for things like Snackbar messages.
The great refactor
Once the documentation guidelines were drafted, the refactor to update all the old references to the new references was underway. This was by far the biggest chunk of work in this project, changing 895 files over 5 months (just on Android!).
The first thing we needed to do was create the attribute references.
(This is a filtered down version of our attribute definitions using primary as our example. All other references as defined in the documentation will follow the same pattern)
<!--
Colour documentation:
<what>_<type>_<variant>
What is what the colour is used for, default is "generic/background" and is omitted
Type describes the colour, example "accent", "primary"
Variant is a variant of the colour, this varies between colours but generally is used for normal/selected/disabled
-->
<resources>
<!--
Colour definitions. {@link primary} (and similar) will reference a drawable. This can not be used for text colours, or colours
that expect a colour reference. Instead use the `<attr>_color` variant.
If you need to reference a specific colour, default to `<attr>_primary` reference.
-->
<!-- Primary colour, main brand colour as a drawable state list -->
<attr name="primary" format="reference" />
<attr name="primary_primary" format="reference|color" />
<attr name="primary_secondary" format="reference|color" />
<attr name="primary_tertiary" format="reference|color" />
... other definitions ...
<!--
Text colour definitions. {@link text_primary} (and similar) will reference a colour state list. For references that expect
These colours should work on their respective colour reference (e.g {@link text_window} will work with {@link window})
If you need to reference a specific colour, default to `<attr>_primary` reference.
-->
<!-- Primary text colour, should work on primary colour -->
<attr name="text_primary" format="color" />
<attr name="text_primary_primary" format="reference|color" />
<attr name="text_primary_secondary" format="reference|color" />
<attr name="text_primary_tertiary" format="reference|color" />
... other definitions ...
</resources>
Now we have our list of available references, we should create the colour references we plan on using for the attributes.
<!-- base/surface colours -->
<color name="passenger_primary_primary">#355063</color>
<color name="passenger_primary_secondary">#2d4454</color>
<color name="passenger_primary_tertiary">#536a7a</color>
<!-- text -->
<color name="passenger_text_primary_primary">#ffffff</color>
<color name="passenger_text_primary_secondary">#ffffff</color>
<color name="passenger_text_primary_tertiary">#ffffff</color>
Next we assign them colours in our default Application theme. For the sake of management, these attributes resolve to a colour reference.
<!-- Base application theme. -->
<style name="PassengerTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- passenger theme attributes -->
<item name="primary_primary">@color/passenger_primary_primary</item>
<item name="primary_secondary">@color/passenger_primary_secondary</item>
<item name="primary_tertiary">@color/passenger_primary_tertiary</item>
<item name="text_primary_primary">@color/passenger_text_primary_primary</item>
<item name="text_primary_secondary">@color/passenger_text_primary_secondary</item>
<item name="text_primary_tertiary">@color/passenger_text_primary_tertiary</item>
</resources>
Whilst refactoring, I took the opportunity to clean up the layout files (and there are a lot of layout files) and to upgrade the overall look of the apps using Google’s Material components library. This is a great library that gives you access to loads of backwards compatible views and styles – such as the infamous rounded button with ripple press effect.
Dark theme
Now that the app references had been implemented, UI components changed and paddings fixed – dark theme was in a position to be implemented. As big of an update this was, the dark theme ended up being around 100 lines of attribute re-definitions (for example, changing primary_primary from @color/passenger_primary_primary
to @color/dark_passenger_primary_primary
) All other styles are left untouched.
We can create our dark theme in the values-night/
resource folder. This will automatically apply the override styles when dark mode is enabled.
<!-- Base application theme. -->
<style name="PassengerTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- passenger theme attributes -->
<item name="primary_primary">@color/dark_passenger_primary_primary</item>
<item name="primary_secondary">@color/dark_passenger_primary_secondary</item>
<item name="primary_tertiary">@color/dark_passenger_primary_tertiary</item>
<item name="text_primary_primary">@color/dark_passenger_text_primary_primary</item>
<item name="text_primary_secondary">@color/dark_passenger_text_primary_secondary</item>
<item name="text_primary_tertiary">@color/dark_passenger_text_primary_tertiary</item>
</resources>
As we’re using @style/PassengerTheme
in our Application
default theme, using the same style name will mean the reference is the same, but the resolution will depend on which resource query handles it (i.e styles.xml
from res/values/
will be used normally, but styles.xml
from res/values-night/
will be used if dark mode is enabled)
Finally
In the final stages, our iOS team incorporated some of the new naming conventions, meaning we could better align styles for operators. As an additional bonus, having a solid spec for our design team to follow helps us implement new features or operator styles. After perfecting the final colours and updating assets originally designed for the lighter theme, we launched the update – 27.0.Francis – to the world.
Newsletter
We care about protecting your data. Here’s our Privacy Policy.
Related news
Start your journey with Passenger
If you want to learn more, request a demo or talk to someone who can help you take the next step forwards, just drop us a line.