Handling user selectable themes in Jetpack Compose

Recent versions of Android offer both Light and Dark themes, which the user can set globally via the device settings. Most apps nowadays follow this setting and change their theme based on the global setting, but it is also good practice to offer users an app option to set the theme regardless of what the global setting is. In this article we’ll find out how to do that with Jetpack Compose.

Storing the user preference

The first thing we need to do is persist the user setting; we will use SharedPreferences for this, though you may want to consider using the new DataStore — that’s just an implementation detail so we’ll use preferences here.

We will offer the user 3 options, “Always light”, “Always dark” and “Auto”, which will follow the global setting. As this is a discreet set, it makes sense to define an enum to represent these values:

Now that we’ve defined out values, it’s time to create our preferences class. We need to offer a means to read and writethe value, as well as a means to inform interested parties of changes to the value. A Flow is a good fit for this, and more specifically a StateFlow as a value is always present and we want to share a single underlying flow to all clients. We will also follow clean architecture principles and define an interface that clients can get injected and a concrete implementation for that interface. With all that said, we can define our user settings interface and implementation as seen below:

Note how we used a property delegate to back the app theme. We initialize our StateFlow with the current value we read from the preferences when the class is initialized, and whenever the value is updated (in setValue) we publish the new theme setting to our flow so that any listeners get notified.

Creating the radio buttons

The next step is to create the composable that will show the 3 options to the user; for this we will use the selectableGroup modifier. Each entry will be its own composable, and we will also define a data class to represent the content of each entry. Let’s look at our radio group entry composables first

Each of our entries is a Row consisting of a radio button and a text, with an 8dp spacer separating them. The important thing to note is that this composable is selectable with a role of RadioButton and accepts a lambda that will be called when the user taps on the composable, which will let us know which option the user clicked on by virtue of the id in our RadioButtonItem. We are also passing in as argument a boolean that indicates if this option is currently selected, which we use to update the radio button composable. This is how the compose preview pane indicates our composable will render

Next we want to create the radio group that will host our 3 entries and ensure that only one is selectable at any given time. Our radio group composable is shown below

This is fairly straightforward, we just have a column and we display our children composable items within. The important thing to note is the use of modifier.selectableGroup and passing to each child composables a boolean that indicates if it is selected or not. The preview pane shows how our radio group will render

Radio group preview
Radio group preview

Putting it all together

Now that we have all the pieces we need it’s time to put it all together. For this simple example we will have the Activity listen for the app theme stream directly, but in a more realistic scenario this would be handled by a ViewModel and exposed as a state that the Activity ‘s composable would observe. Let’s have a look at the code first and we’ll go over it in detail in a moment

For the Activity, we can see that

  • we get our user settings class injected.
  • when we set the composable content, we observe the user setting for the app theme stream as a state, using the collectAsState extension function. This ensures that whenever the value changes any composables observing this value will be recomposed.
  • when we set our app theme, we specify if we are in light or dark theme based on the current value we received from the stream. If the value is Auto we default to the global setting, otherwise we enforce the value, either Light or Dark theme.
  • we display our main screen composable, providing the current value and a lambda to trigger when the user changes the app theme.
  • in the lambda we update the app theme in our persistent storage, which will trigger an update on the state flow and in turn trigger a recomposition of our screen.

For our main screen, we have

  • the currently selected theme and click lambda are received as arguments.
  • we first build our list of radio group items, 3 entries with an ID and a label each. We use the enum’s ordinal as our ids.
  • our composable is a column where we display a label and our RadioGroup, using the items we just defined.
  • we provide the selected item id to the RadioGroup and in the click lambda we convert the id back to an enum and then pass this information back to our caller.
  • we add a button and some lorem ipsum text to aid in visualizing the theme changes.

With all this in place we now have a reactive solution where updating the theme in our storage triggers an update on our state which refreshes the screen and applies the newly selected theme.

App theme chooser screenshot
App theme chooser screenshot

An important difference with the view system is that the Activity is not recreated when the user selected theme changes, the content is recomposed and redraw based on the new theme; in the view system to change the theme would require recreating the Activity for the change to take place, as themes are immutable and get applied only on Activity creation and can’t be changed afterwards.

The solution shown here only deals with Light and Dark themes, but it can easily be extended to handle multiple theme options, where the user can select the colors that will be applied to the app. Instead of a light and dark theme we would have a collection of light and dark themes using different colors each, and we would apply the selected one in our theme class in Theme.kt. Making those changes is left as an exercise to the reader.

The sample project is available here: App Theme Sample.

Senior Android Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store