Changing the color scheme at run-time for an Android app (Theme hack?).
Tags: android, kotlin, themes
Published: 2020-02-27 10:17:05

Righto. Have you ever tried to change the defined theme for your android app at run-time? Perhaps, in response to user input or some user setting you've described in your app?

If you have, then I feel a wee bit bad for you, because like me, you probably found out real quick that, android themes? Those little buggers are [immutable](https://stackoverflow.com/a/26511725).

If you haven't attempted to do that before and this post here is your first foray into changing android themes, then, welcome, you've come to the right place.

But, if themes are immutable and cannot be changed at run-time, then what are we doing here?

![Just what in the world?](https://media.giphy.com/media/zLXBAnyOqmTHa/giphy.gif)

Well, we are here, because of ThemeOverlays. ThemeOverlays are basically themes that [change specific attributes](https://medium.com/androiddevelopers/theming-with-appcompat-1a292b754b35) in already defined parent themes, i.e. they change only the items you determine.

Theme overlays, just like normal themes are immutable and cannot be changed at run-time, but since they only change specific attributes (like colorPrimary) and leave all others as they were, we can define multiple overlays and then apply them (like patches) to different activities depending on user input or setting.

To get started, we'll need a default android theme. The default one created for any new project is fine. It can be found in the `styles.xml` file and usually looks like this:

```xml
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    
```

In this tutorial, we'll be editing the `colorPrimary`, `colorPrimaryDark` and `colorAccent` attributes (in effect, the title and status bars and things like FABs).

Now, to begin changing themes, we need a couple of colors, we can very well write the color codes in our `styles.xml` file but I prefer a cleaner approach, so let's define the colors we want to use in `colors.xml`. I have chosen these:

```xml
    <color name="md_red_500">#970606</color>
    <color name="md_red_700">#770404</color>
    <color name="md_yellow_A400">#FBC02D</color>

    <color name="md_lime_500">#6D9904</color>
    <color name="md_lime_700">#587C02</color>
    <color name="md_amber_A400">#F55E0C</color>

    <color name="md_green_500">#047204</color>
    <color name="md_green_700">#035203</color>
    <color name="md_pink_A400">#D32F2F</color>

    <color name="md_blue_500">#070786</color>
    <color name="md_blue_700">#04045E</color>
    <color name="md_turquoise_A400">#00796B</color>
```

Twelve colors. Four sets. In each set, one color for `colorPrimary`, another for `colorPrimaryDark` and the last for `accentColor`. Perfect for four different themeOverlays.

![Perfectamento](https://media.giphy.com/media/3q3QK6KyDVUBzih7hB/giphy.gif)

Next, we'll create our actual overlays and use these colors in them.

If you're following along, jump over to `styles.xml` and create the themeOverlays you need. I have created mine in this fashion:

```xml
    <style name="OverlayThemeLime" parent="AppTheme">
        <item name="colorPrimary">@color/md_lime_500</item>
        <item name="colorPrimaryDark">@color/md_lime_700</item>
        <item name="colorAccent">@color/md_amber_A400</item>
    </style>

    <style name="OverlayThemeRed" parent="AppTheme">
        <item name="colorPrimary">@color/md_red_500</item>
        <item name="colorPrimaryDark">@color/md_red_700</item>
        <item name="colorAccent">@color/md_yellow_A400</item>
    </style>

    <style name="OverlayThemeGreen" parent="AppTheme">
        <item name="colorPrimary">@color/md_green_500</item>
        <item name="colorPrimaryDark">@color/md_green_700</item>
        <item name="colorAccent">@color/md_pink_A400</item>
    </style>

    <style name="OverlayThemeBlue" parent="AppTheme">
        <item name="colorPrimary">@color/md_blue_500</item>
        <item name="colorPrimaryDark">@color/md_blue_700</item>
        <item name="colorAccent">@color/md_turquoise_A400</item>
    </style>
```

Notice how I've made them all inherit the default `AppTheme`? This is incredibly useful if you have other attributes set in the AppTheme style that you want all your overlays to inherit.

Now, we have all our colors and styles set up. How do we use them in activities and such?

Simple! We do that with [Theme.applyStyle](https://developer.android.com/reference/android/content/res/Resources.Theme.html#applyStyle(int,%20boolean)).

![That's right](https://media.giphy.com/media/wOY3kEK96Te7e/giphy.gif)

There's a caveat, however. We can only do `applyTheme` _before_ we set the content view (i.e. before the layout is inflated).

So, for now, just add the line for applying a theme before `setContentView` in your activity. The `onCreate` method should then look like so:

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

//      getTheme()applyStyle(R.style.OverlayThemeLime, true) in Java 
        theme.applyStyle(R.style.OverlayThemeLime, true)
        setContentView(R.layout.activity_main)
    }
```

The second `boolean` parameter means to force apply the style.

If you run the app now, you should see a beautiful lime color (or whatever scheme you chose) as the app's theme.

But...how do we change that if we have to set the theme before setting content view?!??!

![How? HOW???!](https://media.giphy.com/media/QjIz1AqkGTszK/giphy.gif)

To do that, we can simply...restart the activity. Yeah, sorry if that's not very elegant but I promise it works.

Before we go about that though, we'll set up a way to keep track of state so that when the activity restarts, the state is preserved and used to set the last selected theme. For simplicity, let's use SharedPreferences.

Create a `SharedPreferences` object and a key for the value we're storing just above the `onCreate` method like so:

```kotlin
    lateinit var sharedPreferences: SharedPreferences
    val themeKey = "currentTheme"
```

Next, we'll initialize the sharedPreferences object so our `onCreate` method looks like this:

```kotlin
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //New
        sharedPreferences = getSharedPreferences(
            "ThemePref",
            Context.MODE_PRIVATE
        )

//        getTheme()applyStyle(R.style.OverlayThemeLime, true) in Java
        theme.applyStyle(R.style.OverlayThemeLime, true)
        setContentView(R.layout.activity_main)

    }
```

Now, we'll need four buttons to trigger each of the style changes. Nothing fancy, just normal buttons with a click handler. I've created mine in the activity xml file:
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    <Button
            android:text="Blue"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/blue" app:layout_constraintStart_toStartOf="parent"
            android:textAllCaps="false"
            app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="24dp"
            app:layout_constraintEnd_toEndOf="parent" android:onClick="onClick"/>
    <Button
            android:text="Green"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/green"
            android:textAllCaps="false"
            app:layout_constraintStart_toStartOf="@+id/blue" android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/blue" android:onClick="onClick"/>
    <Button
            android:text="Red"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/red"
            android:textAllCaps="false"
            app:layout_constraintStart_toStartOf="@+id/green" android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/green" android:onClick="onClick"/>
    <Button
            android:text="Lime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/lime"
            android:textAllCaps="false"
            app:layout_constraintStart_toStartOf="@+id/red" android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toTopOf="@+id/red" android:onClick="onClick"/>
</androidx.constraintlayout.widget.ConstraintLayout>
```

If you copy-pasted that, you'll probably have a tiny little error. That's because we haven't created our click handler in the activity yet. So, let's do that next.

We'll create an `onClick` handler for all our buttons. This handler will take care of setting the `sharedPreferences` value restarting our activity. For now, all it does is set our value and it should look something like this:
```kotlin
fun onClick(view: View) {
        when(view.id) {
            R.id.lime -> {
                sharedPreferences.edit().putString(themeKey, "lime").apply()
            }

            R.id.red -> {
                sharedPreferences.edit().putString(themeKey, "red").apply()
            }

            R.id.green-> {
                sharedPreferences.edit().putString(themeKey, "green").apply()
            }

            R.id.blue -> {
                sharedPreferences.edit().putString(themeKey, "blue").apply()
            }

        }

    }
```

All we've done here, is use a `when` statement (switch in Java) to cater for each button's click event using their id. And in the event that a button is clicked, we set the shared preferences value to reflect that.

Next, we restart the activity.

<!-- To do this, we simply add one more statement to our `onClick` function, below the `when` statement. We add `recreate()` -->

<!-- We add `startActivity(intent)`. This gets the current intent (with getIntent) and starts an activity with it. However, since the intent we're passing to `startActivity` is the current intent, the effect is just of restarting our activity. -->

To do this, we simply add a few more lines at the bottom of our `onClick` function. Like so:

Our `onClick` function should now look like so:
```kotlin
fun onClick(view: View) {
        when(view.id) {
            R.id.lime -> {
                sharedPreferences.edit().putString(themeKey, "lime").apply()
            }
            
            R.id.red -> {
                sharedPreferences.edit().putString(themeKey, "red").apply()
            }

            R.id.green-> {
                sharedPreferences.edit().putString(themeKey, "green").apply()
            }

            R.id.blue -> {
                sharedPreferences.edit().putString(themeKey, "blue").apply()
            }
            
        }
        
        val intent = intent // from getIntent()
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
        finish()
        startActivity(intent)
    }
```

What we've done is simply getting the current intent and restarting it. I've added a `no-animation` flag so the change is instant and this would hide all animations if we had other activities in our backstack (does not exactly work here because we have just one activity so the entire app is, in effect, getting restarted)

**Disclaimer**: Rather than going this route, we could use [`Activity.recreate()`](https://stackoverflow.com/a/16467733), but I don't particularly like the flicker effect I get with that. (Again, things might be different with a backstack, but feel free to experiment and see which works best for you.)

![Experiment, you scientist, you](https://media.giphy.com/media/QU1pSfyEynvgY/giphy.gif)

At this point, we're 90% done.

![Almost there gif](https://media.giphy.com/media/VFHWGT96hbfsjDJ57S/giphy.gif)

The only thing left is to set the theme when our activity restarts based on the current `sharedPreferences` value. So, we go back to our `onCreate` method and replace the explicit line to set theme with a conditional. Doing this correctly, our onCreate method looks somewhat like this:

```kotlin
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sharedPreferences = getSharedPreferences(
            "ThemePref",
            Context.MODE_PRIVATE
        )

//        getTheme()applyStyle(R.style.OverlayThemeLime, true) in Java
//        theme.applyStyle(R.style.OverlayThemeBlue, true) // -> Replaced
        when (sharedPreferences.getString(themeKey, "red")) {
            "lime" ->  theme.applyStyle(R.style.OverlayThemeLime, true)
            "red" ->  theme.applyStyle(R.style.OverlayThemeRed, true)
            "green" ->  theme.applyStyle(R.style.OverlayThemeGreen, true)
            "blue" ->  theme.applyStyle(R.style.OverlayThemeBlue, true)
        }

        setContentView(R.layout.activity_main)

    }
```

**Bonus**: We can take advantage of the `colorAccent` attribute of our overlays in any `xml` files. To demonstrate this, we'll set the background color of our layout to the `colorAccent` attribute of the currently applied theme by simply adding this line to layout code:
```xml
            android:background="?colorAccent"
```

The results of all our trouble so far looks like this:

![Results](https://docs.google.com/uc?export=download&id=13IedEptw-Gm8Ol0Z2s4Htt1pCg_Agxag)

That's it, I hope this was helpful. I've created an android project with all of the code on [GitHub here](https://github.com/emkayDauda/CustomThemingExample).

Sources include [this](https://stackoverflow.com/a/37905131) StackOverflow answer and the Android Developer [docs](https://developer.android.com).