WSBT
WSBT

Reputation: 36323

How to switch themes (night mode) without restarting the activity?

I have made a few apps that support multiple themes, but I always had to restart the app when user switches theme, because setTheme() needs to be called before setContentView().

I was okay with it, until I discovered this app. It can seamlessly switch between two themes, and with transitions/animations too!

enter image description here

Please give me some hints on how this was implemented (and animations too). Thanks!

Upvotes: 77

Views: 38780

Answers (7)

GKA
GKA

Reputation: 1356

This other answer also does the same thing using finish()/startActivity().

Anyway, I would do exactly what he described in terms of styles.

<style name="AppThemeLight" parent="Theme.AppCompat.Light">
    <!-- Customize your theme here. -->
    <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item>
</style>
<style name="AppThemeDark" parent="Theme.AppCompat">
    <!-- Customize your theme here. -->
    <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item>
</style>
<!-- This will set the fade in animation on all your activities by default -->
<style name="WindowAnimationTransition">
    <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
    <item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>

But instead of finish/start with new intent:

Intent intent = new Intent(this, <yourclass>.class);
startActivity(intent);
finish();

I would do:

@Override
protected void onCreate(Bundle savedInstanceState) {

    // MUST do this before super call or setContentView(...)
    // pick which theme DAY or NIGHT from settings
    setTheme(someSettings.get(PREFFERED_THEME) ? R.style.AppThemeLight : R.style.AppThemeDark);

    super.onCreate(savedInstanceState);
}

// Somewhere in your activity where the button switches the theme
btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

        // decide which theme to use DAY or NIGHT and save it
        someSettings.save(PREFFERED_THEME, isDay());

        Activity.this.recreate();
    }
});

The effect is as shown in the video...

Upvotes: 64

Hoang Nguyen Huu
Hoang Nguyen Huu

Reputation: 1252

All above answers are not really prevent activity from being recreated! The correct solution should be like this:

  1. Add "uiMode" into configChanges field in AndroidManifest.xml to avoid Activity recreating when dark mode is toggled.

android:configChanges="uiMode"

  1. Update all your views color manually when dark mode is toggled in onConfigureChanged() function in Activity.

You can refer this article for more detail: https://proandroiddev.com/daynight-applying-dark-mode-without-recreating-your-app-c8a62d51092d

Upvotes: 3

Ali Nawaz
Ali Nawaz

Reputation: 2500

Simply efficient one liner in fragment:

requireActivity().recreate();

For activity:

recreate();

Upvotes: 2

Iman Dolatkia
Iman Dolatkia

Reputation: 226

setTheme() before super.onCreate(savedInstanceState) in GKA answer is perfect approach and work well, thanks to GKA.

but it creates new instances for all resources again, including activities, fragments, and recycler views. I think it may be heavy work and cause to loss of some saved data like local variables.

accourding to google document: https://developer.android.com/reference/android/app/Activity#recreate()

Cause this Activity to be recreated with a new instance. This results in essentially the same flow as when the Activity is created due to a configuration change -- the current instance will go through its lifecycle to onDestroy() and a new instance then created after it.

there is another approach that you can change the theme programmatically with code (Java or Kotlin), in this approach you don't need to recreate all resources, and also you can use custom animation like ripple.

check my GitHub library: https://github.com/imandolatkia/Android-Animated-Theme-Manager

in this library, you can create your custom themes and change them dynamically with ripple animation without recreating any resources.

enter image description here

Upvotes: 5

Asad
Asad

Reputation: 1439

for those who are trying to find solution for android version 10 or updated.

to set dark/light mode use this:

AppCompatDelegate.setDefaultNightMode(state) //state can be AppCompatDelegate.MODE_NIGHT_YES or AppCompatDelegate.MODE_NIGHT_NO

it will change the display of your app but with a flicker

to avoid the activity recreation flicker (for smooth transition), in your activity add the below method

@Override
    public void recreate() {
        finish();
        overridePendingTransition(R.anim.anime_fade_in,
                                  R.anim.anime_fade_out);
        startActivity(getIntent());
        overridePendingTransition(R.anim.anime_fade_in,
                                  R.anim.anime_fade_out);
    }

Upvotes: 6

Alexander Hanssen
Alexander Hanssen

Reputation: 290

The transition/animation makes the theme change seamless when you restart the activity, and this can be done by adding the items "android:windowanimationStyle" to your themes, and then referencing a style where you specifiy how the Activity should animate when it enters and exits. Note that this makes the animation apply on all activities with that theme.

<style name="AppThemeLight" parent="Theme.AppCompat.Light">
    <!-- Customize your theme here. -->
    <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item>
</style>
<style name="AppThemeDark" parent="Theme.AppCompat">
    <!-- Customize your theme here. -->
    <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item>
</style>
<!-- This will set the fade in animation on all your activities by default -->
<style name="WindowAnimationTransition">
    <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
    <item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>

Then, when you want to change theme you could do this when clicking a button:

AppSettings settings = AppSettings.getInstance(this);
settings.set(AppSettings.Key.USE_DARK_THEME,
!settings.getBoolean(AppSettings.Key.USE_DARK_THEME));
Intent intent = new Intent(this, <yourclass>.class);
startActivity(intent);
finish();

Then in your onCreate method, use the setTheme() to apply the theme that is currently set in AppSettings like this:

AppSettings settings = AppSettings.getInstance(this);
setTheme(settings.getBoolean(AppSettings.Key.USE_DARK_THEME) ? R.style.AppThemeDark : R.style.AppThemeLight);
super.onCreate(savedInstanceState);
setContentView(<yourlayouthere>);

Check out this gist for reference: https://gist.github.com/alphamu/f2469c28e17b24114fe5

Upvotes: 17

David Wasser
David Wasser

Reputation: 95578

There isn't anything preventing you from calling setTheme() and then setContentView() again. You'll just need to restructure your app a bit so that, if you change the theme, you need to reinitialize any member variables you might have that are holding references to View objects.

Upvotes: 1

Related Questions