Android

Building Better App UI Architecture- Android Themes

January 13, 2020
Building Better App UI Architecture- Android Themes

Our last post focused on opti­miz­ing UI sys­tem imple­men­ta­tion by break­ing down design into small­er pieces, iden­ti­fy­ing the UI ele­ments and com­po­nents, and using pro­to­col-ori­ent­ed pro­gram­ming in iOS. As I men­tioned here.

This time around, we’ll give Android the spot­light by build­ing a sam­ple app that can switch themes using respec­tive dark mode. We’ll also dis­cuss approach­es that can make Android UI devel­op­ment more efficient.

Theme and Style in Android #

Gen­er­al­ly we fol­low themes and styles as much as pos­si­ble for con­sis­ten­cy. Before div­ing into the sam­ple app, let’s see how the Android devel­op­er guide defines themes and styles:

  • A style is a col­lec­tion of attrib­ut­es that spec­i­fy the appear­ance for a sin­gle View. A style can spec­i­fy attrib­ut­es such as font col­or, font size, back­ground col­or, and much more.
  • A theme is a type of style that’s applied to an entire app, activ­i­ty, or view hier­ar­chy — not just an indi­vid­ual view. When you apply your style as a theme, every view in the app or activ­i­ty applies each style attribute that it sup­ports. Themes can also apply styles to non-view ele­ments, such as the sta­tus bar and win­dow background.

The bot­tom line? Apply­ing a style to a view impacts only that view and not its chil­dren. How­ev­er, if you apply a theme to a view it will be applied to all view/non-view elements.

Dark Theme #

Start­ing with Android Q, users can switch the device into dark theme via sys­tem set­tings -> Dis­play -> Dark theme. To sup­port the dark theme func­tion­al­i­ty, you must set the app’s theme to inher­it from a DayNight theme.

Google rec­om­mends updat­ing the app theme to inher­it from the Theme.MaterialComponents.DayNight (or one of its descen­dants). For example:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
    <!-- Customize your theme here -->
</style>

In most cas­es, switch­ing to dark theme doesn’t just about invert­ing the white into black:

There are sev­er­al prin­ci­ples we need to follow:

  • Dark­en with grey.
  • Apply lim­it­ed col­or accents in dark theme.
  • Meet­ing the acces­si­bil­i­ty col­or con­trast standards.

So we also need to define sep­a­rate theme attrib­ut­es with­in each theme, which can be inher­it­ed 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 incre­men­tal­ly add mate­r­i­al com­po­nents you have to add the theme attrib­ut­es to the exist­ing app theme. You can find all new attrib­ut­es here.

From Android 10 onwards you can set android:forceDarkAllowed="true" in the activity’s theme, so that even the app with­out DayNight can auto­mat­i­cal­ly apply dark theme. Just make sure every­thing looks right before you turn it on.

Mul­ti­ple themes with dark mode

The sam­ple we’re going to build is a bit dif­fer­ent. In this case, we’re plan­ning on sev­er­al themes.

This means hav­ing sev­er­al 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 cus­tom theme attributes. 

You can define cus­tom theme attrib­ut­es 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 inher­its from AppTheme.Basic.Red, which over­rides the attrib­ut­es 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 look­ing to dig deep­er, here are all the orig­i­nal attrib­ut­es in App­Com­pat theme.

Use this same approach to define oth­er col­or themes and their dark modes for ​“orange”, ​“blue”, ​“green”, ​“pur­ple”, ​“teal”, or what­ev­er you decide to name it.

Next, we need to style our views. For con­sis­ten­cy, car­ry out themes and styles as much as pos­si­ble, and keep Android’s style hier­ar­chy in mind. The list is ordered from high­est prece­dence to low­est to help you deter­mine which attrib­ut­es to apply:

  • Apply­ing char­ac­ter- or para­graph-lev­el styling via text spans to TextView-derived class­es
  • Apply­ing attrib­ut­es programmatically 
  • Apply­ing indi­vid­ual attrib­ut­es direct­ly to a view
  • Apply­ing a style to a view
  • Default styling
  • Apply­ing a theme to a col­lec­tion of views, an activ­i­ty, or your entire app
  • Apply­ing cer­tain view-spe­cif­ic styling, such as set­ting a TextAppearance on a TextView

So, wher­ev­er we need to change the theme in our sam­ple app, we need to apply the theme attrib­ut­es. For exam­ple, using a gra­di­ent draw­able gradient_background.xml as the back­ground, 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 back­ground 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 col­or will always be pink_​600 no mat­ter what text col­or is set in tex­tAp­pear­ance or what theme you use. For things you nev­er want to change, apply indi­vid­ual attrib­ut­es directly.

Okay, now that our themes and views are set up, we can call Set­Theme 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 some­what tedious process, our app now sup­ports mul­ti­ple themes with respec­tive dark mode.

In app devel­op­ment, require­ments, design, and imple­men­ta­tion (espe­cial­ly UI) are prone to change. The first thing we need to keep in mind is to avoid cre­at­ing styles with hard­cod­ed val­ues. Sep­a­rat­ing UI code by first defin­ing theme, styles, and cus­tom view com­po­nents results in a pow­er­ful cen­tral con­trol and reduces the amount of code and improve reusabil­i­ty and testability. 

In the long run under­stand­ing how style/​view hier­ar­chy works with­in a spe­cif­ic oper­at­ing sys­tem helps to cre­ate a clear hier­ar­chy imple­men­ta­tion, while ulti­mate­ly mak­ing code more reusable.

Next up in our How to Build a Bet­ter App UI Archi­tec­ture series: Cre­at­ing a cus­tom view in Android. We can’t wait to share our insights.

Mei Huang
Mei Huang
Software Developer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

How to Prepare for our Associate Software Developer Position
Team

How to Prepare for our Associate Software Developer Position

June 30, 2023

Tips for applying to MichiganLab's Associate Software Developer program

Read more
Why I use NextJS
Development Web

Why I use NextJS

December 21, 2022

Is NextJS right for your next project? In this post, David discusses three core functionalities that NextJS excels at, so that you can make a well-informed decision on your project’s major framework.

Read more
User research: The heartbeat of successful development
Business Design Process

User research: The heartbeat of successful development

July 15, 2024

User research in software development is essential for success. Learn how consistently engaging in methods like user interviews, usability testing, and field studies throughout the product lifecycle, helps ensure your solutions align closely with user needs.

Read more
View more articles