DEV Community

Maaruf Dauda
Maaruf Dauda

Posted on • Edited on

Changing the color scheme at run-time for an Android app (Theme hack?).

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.

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?

Well, we are here, because of ThemeOverlays. ThemeOverlays are basically themes that change specific attributes 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:



    <!-- 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>



Enter fullscreen mode Exit fullscreen mode

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:



    <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>


Enter fullscreen mode Exit fullscreen mode

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

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:



    <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>


Enter fullscreen mode Exit fullscreen mode

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.

That's right

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:



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)
    }


Enter fullscreen mode Exit fullscreen mode

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???!

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:



    lateinit var sharedPreferences: SharedPreferences
    val themeKey = "currentTheme"


Enter fullscreen mode Exit fullscreen mode

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



 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)

    }


Enter fullscreen mode Exit fullscreen mode

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 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>


Enter fullscreen mode Exit fullscreen mode

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:



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()
            }

        }

    }


Enter fullscreen mode Exit fullscreen mode

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 a few more lines at the bottom of our onClick function. Like so:

Our onClick function should now look like so:



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)
    }


Enter fullscreen mode Exit fullscreen mode

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(), 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

At this point, we're 90% done.

Almost there 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:



     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)

    }


Enter fullscreen mode Exit fullscreen mode

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:



            android:background="?colorAccent"


Enter fullscreen mode Exit fullscreen mode

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

Results

That's it, I hope this was helpful. I've created an android project with all of the code on GitHub here.

Sources include this StackOverflow answer and the Android Developer docs.

Top comments (0)