Our last post focused on optimizing UI system implementation by breaking down design into smaller pieces, identifying the UI elements and components, and using protocol-oriented programming in iOS. As I mentioned here.
This time around, we’ll give Android the spotlight by building a sample app that can switch themes using respective dark mode. We’ll also discuss approaches that can make Android UI development more efficient.
Theme and Style in Android #
Generally we follow themes and styles as much as possible for consistency. Before diving into the sample app, let’s see how the Android developer guide defines themes and styles:
- A style is a collection of attributes that specify the appearance for a single
View
. A style can specify attributes such as font color, font size, background color, and much more. - A theme is a type of style that’s applied to an entire app, activity, or view hierarchy — not just an individual view. When you apply your style as a theme, every view in the app or activity applies each style attribute that it supports. Themes can also apply styles to non-view elements, such as the status bar and window background.
The bottom line? Applying a style to a view impacts only that view and not its children. However, if you apply a theme to a view it will be applied to all view/non-view elements.
Dark Theme #
Starting with Android Q, users can switch the device into dark theme via system settings -> Display -> Dark theme. To support the dark theme functionality, you must set the app’s theme to inherit from a DayNight
theme.
Google recommends updating the app theme to inherit from the Theme.MaterialComponents.DayNight
(or one of its descendants). For example:
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<!-- Customize your theme here -->
</style>
In most cases, switching to dark theme doesn’t just about inverting the white into black:
There are several principles we need to follow:
- Darken with grey.
- Apply limited color accents in dark theme.
- Meeting the accessibility color contrast standards.
So we also need to define separate theme attributes within each theme, which can be inherited from style name="AppTheme" parent="Theme.MaterialComponents.Light"
in res/values/themes.xml
file:
<style name="AppTheme" parent="Theme.MaterialComponents.Light">
<!-- Customize your light theme here -->
</style>
Then define your dark theme in res/values-night/themes.xml
:
<style name="AppTheme" parent="Theme.MaterialComponents">
<!-- Customize your dark theme here -->
</style>
For most apps that use AppCompat
themes, if you want to incrementally add material components you have to add the theme attributes to the existing app theme. You can find all new attributes here.
From Android 10 onwards you can set android:forceDarkAllowed="true"
in the activity’s theme, so that even the app without DayNight
can automatically apply dark theme. Just make sure everything looks right before you turn it on.
Multiple themes with dark mode
The sample we’re going to build is a bit different. In this case, we’re planning on several themes.
This means having several DayNight
themes. Let’s define our basic theme first:
<style name="AppTheme" parent="Theme.AppCompat.Light">
<item name="colorControlDisabled">@color/grey_200</item>
<item name="colorControlDisabledDark">@color/grey_300</item>
</style>
<style name="AppTheme.Basic">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
colorControlDisabled
and colorControlDisableDark
are custom theme attributes.
You can define custom theme attributes in res/values/attrs.xml
like this:
<resources>
<attr name="themePrimary" format="reference" />
<attr name="themeOnPrimary" format="reference" />
<attr name="themePrimaryDark" format="reference" />
<attr name="themeOnPrimaryDark" format="reference"/>
<attr name="themeAccent" format="reference" />
<attr name="themeSecondary" format="reference"/>
<attr name="themeSecondaryDark" format="reference"/>
<attr name="themeOnSecondary" format="reference"/>
<attr name="colorControlDisabledDark" format="reference"/>
<attr name="colorControlDisabled" format="reference"/>
<attr name="themeColorControlHighlight" format="reference"/>
<attr name="themeColorControlHighlightDark" format="reference"/>
<attr name="themeBackground" format="reference"/>
<attr name="themeOnBackground" format="reference"/>
<attr name="themeSurface" format="reference"/>
<attr name="themeOnSurface" format="reference"/>
</resources>
Now define the red theme:
<style name="AppTheme.Basic.Red">
<item name="colorPrimary">@color/red_500</item>
<item name="colorPrimaryDark">@color/red_800</item>
<item name="colorAccent">@color/red_900</item>
<item name="themePrimary">@color/red_500</item>
<item name="themePrimaryDark">@color/red_800</item>
<item name="themeAccent">@color/red_900</item>
<item name="themeOnPrimary">@color/white</item>
<item name="themeSecondary">@color/red_100</item>
<item name="themeSecondaryDark">@color/red_200</item>
<item name="themeOnSecondary">@color/white</item>
<item name="themeBackground">@color/white</item>
<item name="themeOnBackground">@color/black</item>
<item name="themeSurface">@color/white</item>
<item name="themeOnSurface">@color/black</item>
<item name="themeColorControlHighlight">@color/red_800</item>
<item name="themeColorControlHighlightDark">@color/red_900</item>
</style>
Next, we define dark mode for the red theme:
<style name="AppTheme.Basic.Red.Dark">
<item name="colorPrimary">@color/grey_700</item>
<item name="colorPrimaryDark">@color/black</item>
<item name="themeSurface">@color/grey_700</item>
<item name="themeOnSurface">@color/white</item>
<item name="android:colorBackground">@color/black</item>
</style>
We can see AppTheme.Basic.Red.Dark
inherits from AppTheme.Basic.Red
, which overrides the attributes that need to change in dark mode:
<!-- The primary branding color for the app. By default, this is the color
applied to the action bar background. -->
<attr name="colorPrimary" format="color" />
<!-- Dark variant of the primary branding color. By default, this is the color
applied to the status bar (via statusBarColor) and navigation bar
(via navigationBarColor). -->
<attr name="colorPrimaryDark" format="color" />
<!-- The background used by framework controls. -->
<attr name="controlBackground" format="reference" />
If you’re looking to dig deeper, here are all the original attributes in AppCompat theme.
Use this same approach to define other color themes and their dark modes for “orange”, “blue”, “green”, “purple”, “teal”, or whatever you decide to name it.
Next, we need to style our views. For consistency, carry out themes and styles as much as possible, and keep Android’s style hierarchy in mind. The list is ordered from highest precedence to lowest to help you determine which attributes to apply:
- Applying character- or paragraph-level styling via text spans to
TextView
-derived classes - Applying attributes programmatically
- Applying individual attributes directly to a view
- Applying a style to a view
- Default styling
- Applying a theme to a collection of views, an activity, or your entire app
- Applying certain view-specific styling, such as setting a
TextAppearance
on aTextView
So, wherever we need to change the theme in our sample app, we need to apply the theme attributes. For example, using a gradient drawable gradient_background.xml as the background, will look like this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="?colorPrimary"
android:endColor="?colorPrimaryDark"
android:angle="0"/>
</shape>
Now a card view with a background changed by theme:
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_margin"
app:cardBackgroundColor="?attr/themeSurface"
>
You can then define a textView like this:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:text="@string/app_title"
android:background="?colorPrimary"
android:textColor="@color/pink_600"
android:textAppearance="@style/MyAppTextAppearance.Header1"
/>
The text color will always be pink_600 no matter what text color is set in textAppearance or what theme you use. For things you never want to change, apply individual attributes directly.
Okay, now that our themes and views are set up, we can call SetTheme in MainActivity.kt
. Here is the code snippet:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (getPreferences(Activity.MODE_PRIVATE).getBoolean("isNightMode", false)) {
when(getSavedTheme()) {
R.style.AppTheme_Basic_Orange -> setTheme(R.style.AppTheme_Basic_Orange_Dark)
R.style.AppTheme_Basic_Red -> setTheme(R.style.AppTheme_Basic_Red_Dark)
R.style.AppTheme_Basic_Blue -> setTheme(R.style.AppTheme_Basic_Blue_Dark)
R.style.AppTheme_Basic_Green -> setTheme(R.style.AppTheme_Basic_Green_Dark)
R.style.AppTheme_Basic_Purple -> setTheme(R.style.AppTheme_Basic_Purple_Dark)
R.style.AppTheme_Basic_Teal -> setTheme(R.style.AppTheme_Basic_Teal_Dark)
}
} else {
setTheme(getSavedTheme())
}
// …
}
Save/get the theme to user preference:
private fun saveTheme(value: String) {
val pref = getPreferences(Activity.MODE_PRIVATE)
pref.edit().putString("theme", value).apply()
recreate()
}
private fun getSavedTheme(): Int {
val theme = getPreferences(Activity.MODE_PRIVATE).getString("theme", BLUE)
when (theme) {
ORANGE -> return R.style.AppTheme_Basic_Orange
RED -> return R.style.AppTheme_Basic_Red
BLUE -> return R.style.AppTheme_Basic_Blue
GREEN -> return R.style.AppTheme_Basic_Green
PURPLE -> return R.style.AppTheme_Basic_Purple
TEAL -> return R.style.AppTheme_Basic_Teal
else -> return R.style.AppTheme_Basic_Orange
}
}
After a somewhat tedious process, our app now supports multiple themes with respective dark mode.
In app development, requirements, design, and implementation (especially UI) are prone to change. The first thing we need to keep in mind is to avoid creating styles with hardcoded values. Separating UI code by first defining theme, styles, and custom view components results in a powerful central control and reduces the amount of code and improve reusability and testability.
In the long run understanding how style/view hierarchy works within a specific operating system helps to create a clear hierarchy implementation, while ultimately making code more reusable.
Next up in our How to Build a Better App UI Architecture series: Creating a custom view in Android. We can’t wait to share our insights.
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
A 3-part framework for getting your software project approved internally
September 25, 2024Explore this strategic approach to securing internal buy-in for your custom software projects. The framework emphasizes starting with a lean business case, engaging key stakeholders across the organization to align economic, operational, and technical considerations, and embracing an iterative learning process to make informed decisions.
Read moreBusiness Model Canvas: Helping business leaders create meaningful digital products
January 17, 2024When it comes to custom software, it’s easy to feel stuck between needing to deliver value quickly, while also ensuring meaningful, long-lasting results. Learn why the Business Model Canvas can help you identify the right digital product for your business needs.
Read moreTo RFP or not to RFP?
January 19, 2024Selecting the right digital product partner is not just about ticking boxes in a request for proposal (RFP). It’s more important to engage in a meaningful discovery process to find a collaborator who can turn your vision into a successful digital reality. Here are three things to watch out for as you search for the perfect digital collaborator.
Read more