React-native: Appearance module does not behave correctly: addChangeListener does not react to changes, useColorScheme() does not react to changes.

Created on 4 May 2020  路  29Comments  路  Source: facebook/react-native

Description

The subscription of useColorScheme() and Appearance.addChangeListener() wont work. The hook returns an initial value only on component mount. Changes to the devices color scheme on Android does not provide the hook with a new value. I therefore tried to manually replicate this hook with useEffect and the Appearance module and found that Appearance.addChangeListener() when called returns "undefined", not type EventSubscription. The listener function provided to Appearance.addChangeListener() never gets called.

I have only tested this on Android so I am not aware if this is also an issue on iOS.

React Native version:

System:
    OS: Windows 10 10.0.17763
    CPU: (16) x64 Intel(R) Core(TM) i7-5960X CPU @ 3.00GHz
    Memory: 15.05 GB / 31.92 GB
  Binaries:
    Node: 10.16.2 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.19.2 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 6.14.4 - C:\Program Files\nodejs\npm.CMD
    Watchman: Not Found
  SDKs:
    Android SDK:
      API Levels: 28, 29
      Build Tools: 28.0.3, 29.0.2, 29.0.3
      System Images: android-28 | Intel x86 Atom_64, android-R | Google APIs Intel x86 Atom
      Android NDK: Not Found
  IDEs:
    Android Studio: Version  3.6.0.0 AI-192.7142.36.36.6308749
  Languages:
    Java: 1.8.0_211 - C:\Program Files\Java\jdk1.8.0_211\bin\javac.EXE
    Python: 2.7.18 - C:\Python27\python.EXE
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.11.0 => 16.11.0
    react-native: 0.62.2 => 0.62.2
  npmGlobalPackages:
    *react-native*: Not Found

Steps To Reproduce

Provide a detailed list of steps that reproduce the issue.

  1. run npx react-native init TestDark --template react-native-template-typescript
  2. Import Appearance & useColorScheme from 'react-native'
  3. copy paste the code snippet below above the return in <App /> or use the useColorScheme hook and console.log its output.
  4. Change the color scheme of the Android device and check the console for the correct values.

Expected Results

The useColorScheme hook should return the correct colorScheme when the devices colorScheme changes.
The Appearance.addChangeListener() should return type EventSubscription not undefined.
The listener callback provided to Appearance.addChangeListener(cb) should be called.

Snack, code example, screenshot, or link to a repository:

const App = () => {
  const colorScheme = useColorScheme(); // Only gives initial colorScheme, does not return new colorScheme on change.
  const [mode1, setMode1] = useState('light');
  const mode = colorScheme || 'light';

  React.useEffect(() => {
    const initialColorScheme = Appearance.getColorScheme(); // Works as expected
    setMode1(initialColorScheme || 'light');

    const listener = () => {
      console.log('called'); // Never fires
      const colorScheme1 = Appearance.getColorScheme();
      setMode1(colorScheme1 || 'light');
    };

    const subscription = Appearance.addChangeListener(listener);
    console.log('App -> subscription', subscription); // logs as undefined
    return () => {
      Appearance.removeChangeListener(listener);
    };
  }, []);
  return (
   // jsx
  )
}

Here is a GIF of the app created with npx react-native init NAME --template react-native-template-typescript command. Look for Mode: dark (useColorScheme() and Mode_1 (useEffect & Appearance module). https://gyazo.com/d4ddb35bf689b3c8926be2f613714f73

Android PR Submitted

Most helpful comment

In terms of getting Android to react to the system theme using the React Native core API, I've got it working by doing the following...

// in MainActivity.java
@Override
public void onConfigurationChanged(Configuration newConfig) {
  super.onConfigurationChanged(newConfig);
  getReactInstanceManager().onConfigurationChanged(this, newConfig);
}

It appears this is some missed implementation in ReactActivity.java and ReactActivityDelegate.java.

All 29 comments

Having the same issue here, but it works fine on iOS. For some reason the changeListener never gets called on Android when changing the system color scheme

Having issues with useColorScheme as well, same as described by the OP on Android. On iOS it always returns dark

In my case, On iOS it always returns light.. and useColorScheme subscription doesn't work and Appearance.addChangeListener doesn't call its callback on both ios and android

Having the same issue here, but it works fine on iOS. For some reason the changeListener never gets called on Android when changing the system color scheme

Same issue here, works well on iOS but not in Android, changeListener is never called.

Oh finally I solved the issue for Android.

Based on the documentation of the react-native-appearance package I found a configuration for Android that solve this issue.

It has to modify the MainActivity.java with the following code

import android.content.Intent; // <--- import
import android.content.res.Configuration; // <--- import

public class MainActivity extends ReactActivity {
  ......

  // copy these lines
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Intent intent = new Intent("onConfigurationChanged");
    intent.putExtra("newConfig", newConfig);
    sendBroadcast(intent);
  }

  ......
}

@franciscocobas that didn't work for me :/. I create a brand new project and added that method, but it still didn't work.

Also @rarira make sure that for iOS, you don't have UIUserInterfaceStyle set in your Info.plist file. That was my mistake (I had it set to Dark, hence why useColorScheme was always returning dark).

Oh finally I solved the issue for Android.

Based on the documentation of the react-native-appearance package I found a configuration for Android that solve this issue.

It has to modify the MainActivity.java with the following code

import android.content.Intent; // <--- import
import android.content.res.Configuration; // <--- import

public class MainActivity extends ReactActivity {
  ......

  // copy these lines
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Intent intent = new Intent("onConfigurationChanged");
    intent.putExtra("newConfig", newConfig);
    sendBroadcast(intent);
  }

  ......
}

This does seem to fix the issue, make sure uiMode is added in android:configChanges in AndroidManifest.xml also. Thank you for the quick solution :)

Edit: This only works when using react-native-appearance. The issue of react-native's Appearance API is still there.

@franciscocobas that didn't work for me :/. I create a brand new project and added that method, but it still didn't work.

Also @rarira make sure that for iOS, you don't have UIUserInterfaceStyle set in your Info.plist file. That was my mistake (I had it set to Dark, hence why useColorScheme was always returning dark).

Thank you @josmithua ... I also have UIUserInterfaceStyle in my info.plist :)

Oh finally I solved the issue for Android.
Based on the documentation of the react-native-appearance package I found a configuration for Android that solve this issue.
It has to modify the MainActivity.java with the following code

import android.content.Intent; // <--- import
import android.content.res.Configuration; // <--- import

public class MainActivity extends ReactActivity {
  ......

  // copy these lines
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Intent intent = new Intent("onConfigurationChanged");
    intent.putExtra("newConfig", newConfig);
    sendBroadcast(intent);
  }

  ......
}

This does seem to fix the issue, make sure uiMode is added in android:configChanges in AndroidManifest.xml also. Thank you for the quick solution :)

Edit: This only works when using react-native-appearance. The issue of react-native's Appearance API is still there.

Not really, I'm using the react-native's Appearance API.
I have a working implementation of that, check this repository https://github.com/franciscocobas/ReactNativeDarkMode

I was having the same problem with the hook only returning 'light', no matter how much I'd toggle the appearance in the simulator.

After a lot of digging into code, it turned out the reason it was doing this was due to - the debugger! If you're using a Chrome-based debugger (either in browser or using the standalone RN debugger) the Appearance module will always return light, due to the debugger not being able to handle async functions (or something like that, it was a few days ago!).

I can't say I understand the idiosyncracies as to why it does this, but the upshot of it all is I stopped using the Chrome debugger and moved to flipper - and it worked! I'm now able to swap between dark and light mode using the useColorTheme hook as expected.

Hope this helps someone else, it was a head scratcher for me for quite a while!

Oh finally I solved the issue for Android.
Based on the documentation of the react-native-appearance package I found a configuration for Android that solve this issue.
It has to modify the MainActivity.java with the following code

import android.content.Intent; // <--- import
import android.content.res.Configuration; // <--- import

public class MainActivity extends ReactActivity {
  ......

  // copy these lines
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Intent intent = new Intent("onConfigurationChanged");
    intent.putExtra("newConfig", newConfig);
    sendBroadcast(intent);
  }

  ......
}

This does seem to fix the issue, make sure uiMode is added in android:configChanges in AndroidManifest.xml also. Thank you for the quick solution :)
Edit: This only works when using react-native-appearance. The issue of react-native's Appearance API is still there.

Not really, I'm using the react-native's Appearance API.
I have a working implementation of that, check this repository https://github.com/franciscocobas/ReactNativeDarkMode

@mvrdrew I use flipper not chrome debugger, but it doesn't work either.

@franciscocobas I tried your repo and it works as you mentioned... and you only used react native's API in your code, But I found that in your package.json you have 'react-native-appear' in your dependancies which I don't have.
So I also tried it, I mean just 'yarn add react-native-appearance' .... and it works!!
Of course, I need those lines in MainActivity.java

While that might be a work around, this issue is to do with React Native's own Appearance module not working correctly.

To add on to the list of issues. For iOS I've noticed some strange behavior when overriding the Window UserInterfaceStyle natively.

From the above gif you can see that setting the system theme works perfect, both the useColorScheme hook and change listener trigger.

But if you call window.overrideUserInterfaceStyle = .dark from native iOS only the change listener reacts and updates.

Then when you call window.overrideUserInterfaceStyle = .light neither the hook or change listener get called.

^^ this is using the Appearance API from React Native core.

If I use the react-native-appearance library this behavior works fine...

Also noticed an issue on the Android side (using react-native-appearance). If you try and override the system theme natively by calling AppCompatDelegate.setDefaultNightMode the useColorScheme hook or update listener aren't called.

e.g. Calling AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); from native android should trigger an update by the Appearance API on the JavaScript side.

I've raised a PR to address this in react-native-appearance but thought I should mention it here so that the same fix can be brought into React Native core if needed.

https://github.com/expo/react-native-appearance/pull/50

In terms of getting Android to react to the system theme using the React Native core API, I've got it working by doing the following...

// in MainActivity.java
@Override
public void onConfigurationChanged(Configuration newConfig) {
  super.onConfigurationChanged(newConfig);
  getReactInstanceManager().onConfigurationChanged(this, newConfig);
}

It appears this is some missed implementation in ReactActivity.java and ReactActivityDelegate.java.

Thanks @mrbrentkelly, I can confirm your Android fix works.

In my case, On iOS it always returns light.. and useColorScheme subscription doesn't work and Appearance.addChangeListener doesn't call its callback on both ios and android

i have same issue, have you solved the problem?

Having issues with useColorScheme as well, same as described by the OP on Android. On iOS it always returns dark

On my iOS it always returnslight

It doesn't subscribe to changes still.
I only get the initial value - and that is not always reported correctly.

For me this works on an ios device with debug mode off. For all other cases (ios device debug mode on, ios simulator, android simulator, android device) this does not seem to work.

Thanks, @mrbrentkelly! I've confirmed that this manual fix works for me while using the official Appearance API. 馃コ

In terms of getting Android to react to the system theme using the React Native core API, I've got it working by doing the following...

// in MainActivity.java
@Override
public void onConfigurationChanged(Configuration newConfig) {
  super.onConfigurationChanged(newConfig);
  getReactInstanceManager().onConfigurationChanged(this, newConfig);
}

It appears this is some missed implementation in ReactActivity.java and ReactActivityDelegate.java.

Awesome find! Feel free to submit a PR

Feel free to submit a PR

I already did a few weeks back :) https://github.com/facebook/react-native/pull/29106

MainActivity.java

import com.facebook.react.ReactActivity;
import android.content.Intent; 
import android.content.res.Configuration;
public class MainActivity extends ReactActivity {

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  @Override
  protected String getMainComponentName() {
    return "SQL_PlayGround";
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    getReactInstanceManager().onConfigurationChanged(this, newConfig);
  }
}

The whole code to fix this, hope this gets fixed ASAP

@mrbrentkelly

In terms of getting Android to react to the system theme using the React Native core API, I've got it working by doing the following...

// in MainActivity.java
@Override
public void onConfigurationChanged(Configuration newConfig) {
  super.onConfigurationChanged(newConfig);
  getReactInstanceManager().onConfigurationChanged(this, newConfig);
}

It appears this is some missed implementation in ReactActivity.java and ReactActivityDelegate.java.

Doesn't work for me... I'm using react-native-cli not expo and I have still 'dark' from useColorScheme (from react-native and also from react-native-appearance). Where should be call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO)? In MainActivity or MainApplication? And before or after onCreate method?

@irekrog I've just created a repo that applies the workaround if you want to take a look https://github.com/mrbrentkelly/rn-appearance-workaround.

The only code change required to get useColorScheme to work correctly in fresh RN project is in MainActivity.java

package com.someproject;

import com.facebook.react.ReactActivity;
import android.content.res.Configuration; // <-- Add this import

public class MainActivity extends ReactActivity {

  @Override
  protected String getMainComponentName() {
    return "SomeProject";
  }

  // Add this code to get Appearance API to work in Android

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    getReactInstanceManager().onConfigurationChanged(this, newConfig);
  }
}

Also make sure you're using an android device or emulator that's running Android 10 or above.

Where should be call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO)? In MainActivity or MainApplication? And before or after onCreate method?

This is more of a RN brownfield scenario so you probably don't need to call this at all.

@mrbrentkelly

Ohh thank you for example repo! :)

But now I guess what is wrong because I have a slightly different case. In my app in Settings I have a switcher and I can set dark or light theme for app. For example on my device system theme is dark, but for app I want to change theme to light. Value of theme (dark/light) is saved in SharedPreferences with react-native-shared-preferences library. After restart app in onCreate method in Java I get value from SharedPreferences and if value is light then I call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO) but unfortunately useColorScheme still returns 'dark'... :/

Also I have checked with int currentNightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; what is value after call AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO) and value of currentNightMode is properly because returns Configuration.UI_MODE_NIGHT_NO

Additionaly I need to manipulate theme in Java with setDefaultNightMode because this is needed to change color in Splash Screen (also I created bug in react-native-bootsplash also related with setDefaultNightMode https://github.com/zoontek/react-native-bootsplash/issues/157)

@irekrog there's a bug in the appearance API in RN core which will prevent calls to AppCompatDelegate.setDefaultNightMode() from being respected. I have fixed this bug (and the others mentioned in this issue) here but it hasn't been merged yet.

I also fixed the same bug in the react-native-appearance package. That PR was merged already https://github.com/expo/react-native-appearance/pull/50 but I don't think its been published to NPM yet.

So you could ask them to publish the latest changes from their main branch or add react-native-appearance to your project using a github URL instead of through npm (which I wouldn't recommend).

e.g.

yarn add react-native-appearance@https://github.com/expo/react-native-appearance

Note react-native-appearance has a slightly difference setup configuration so check out their docs https://github.com/expo/react-native-appearance

I just tested this all out by creating a bridge module that calls AppCompatDelegate.setDefaultNightMode and it works for me:

https://github.com/mrbrentkelly/rn-appearance-workaround/compare/BK-WithRNA?expand=1

@mrbrentkelly

Thank you very very much! :) Yeah, your fix is merged but still not publish to NPM so temporary I have used patch-package to patch react-native-appearance using your fix and it works fine 馃憤

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aniss picture aniss  路  3Comments

josev55 picture josev55  路  3Comments

phongyewtong picture phongyewtong  路  3Comments

oney picture oney  路  3Comments

janmonschke picture janmonschke  路  3Comments