8

Running Android P, using androidx 1.0.0 (minSdkVersion 17). From my MainActivity I open my PreferenceActivity. There I change the UI theme, and also re-create the activity to pick up the changes:

AppCompatDelegate.setDefaultNightMode(nightMode);
recreate();

After updating the theme, I return to MainActivity. There the theme is successfully updated. Then I re-open the PreferenceActivity and change the theme again.

So far so good!

Finally, I return to the MainActivity again. The theme is NOT updated, and it will not update if you repeat the steps!

Thus, the steps to reproduce seem to be:

  1. From activity A, open activity B.
  2. In B, call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES) and then recreate(). Theme is updated!
  3. Return to A. Theme is updated!
  4. Open activity B again.
  5. In B, call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO) and then recreate(). Theme is updated!
  6. Return to A. Theme is NOT updated and will NOT update if steps 3-6 are repeated!

I tried calling recreate() when returing from the PreferenceActivity but that yields another problem when the library does react on the theme change:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (...) {
        recreate();
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

That works when the library does NOT react on the updated theme. Otherwise the activity is recreated twice (possibly more when debugging), which kills performance etc:

D/MainActivity: onActivityResult(): instance 1
D/MainActivity: onResume(): instance 1
D/MainActivity: onPause(): instance 1
D/MainActivity: onDestroy(): instance 1

D/MainActivity: onCreate(): instance 2
D/MainActivity: onResume(): instance 2
D/MainActivity: onPause(): instance 2
D/MainActivity: onDestroy(): instance 2

D/MainActivity: onCreate(): instance 3
D/MainActivity: onResume(): instance 3

Q: What is going on with the setDefaultNightMode() API? And more importantly, how can I successfully update all running activities without the risk of re-creating them multiple times?

UPDATE

There is a sample project demonstrating the issue here: https://issuetracker.google.com/issues/119757688

l33t
  • 18,692
  • 16
  • 103
  • 180

2 Answers2

7

When you change night Mode, store the mode value to shared preference.

AppCompatDelegate.setDefaultNightMode(nightMode);
recreate(); //only recreate setting activity 
...//store mode value, these lines are omitted,please complete yourself

In other activity onCreate() method:

...//get mode from share preference, these lines are omitted.
AppCompatDelegate.setDefaultNightMode(mode)//must place before super.onCreate();
super.onCreate(savedInstanceState);
navylover
  • 12,383
  • 5
  • 28
  • 41
  • 2
    When returning from settings activity, `onCreate` is not called (unless I recreate the activity). The `setDefaultNightMode` method is called on Application startup. Everything works, except that the theme update mechanism seems to break after it has run once. I believe [this flag](https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV14.java#47) in the Android source could be the culprit. – l33t Nov 18 '18 at 20:13
2

I found a quite simple solution to this problem. It works in both cases; when AppCompatActivity successfully recreates itself and when it fails to do so. Recall that activity A calls activity B where the theme is changed, then we return to A where the theme is not always updated.

Activity B

In activity B - i.e. Preferences - we keep track of theme changes:

private boolean mThemeChanged;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mThemeChanged = getIntent().getBooleanExtra(EXTRA_THEME_CHANGED, false);
}

private void onNightModeChanged() {
    int nightMode = getNightModeFromPreferences();
    if (AppCompatDelegate.getDefaultNightMode() != nightMode) {
        AppCompatDelegate.setDefaultNightMode(nightMode);

        getIntent().putExtra(EXTRA_THEME_CHANGED, true);
        getDelegate().applyDayNight();
    }
}

And we provide this piece of information to the calling activity, i.e. Main:

@Override
public void finish() {
    Intent data = new Intent();
    data.putExtra(EXTRA_THEME_CHANGED, mThemeChanged);
    setResult(RESULT_OK, data);

    super.finish();
}

Activity A

Then in activity A we use this piece of information:

private boolean mShouldRecreateActivity;

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == ActivityResults.OPEN_SETTINGS_RESULT) {
        if (data != null && data.getBooleanExtra(SetPreferenceActivity.EXTRA_THEME_CHANGED, false)) {
            mShouldRecreateActivity = true;
        }
    }
}

@Override
protected void onResume() {
    super.onResume();

    if (mShouldRecreateActivity) {
        recreate();
        return; // No need to continue resuming!
    }
}

@Override
public void recreate() {
    super.recreate();

    mShouldRecreateActivity = false;
}

In the rare case (usually the first time) the AppCompatActivity correctly calls recreate() our flag will be reset, avoiding an additional recreation of the activity when we reach onResume(). Thus, this code should be future-proof. Though, I really hope the issue is fixed in the next version of androidx, allowing us to get rid of the workaround.

UPDATE

Looks like this was fixed in AppCompat 1.1.0. I no longer need this workaround to get the desired behavior.

l33t
  • 18,692
  • 16
  • 103
  • 180