Android

Building Better App UI Architecture- Android Themes

January 13, 2020

010 0 U0 A1655 squashed

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.

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com