Mathieu de Brito
Mathieu de Brito

Reputation: 2696

ContextCompat.getColor() ignore NightMode

TL,DR;

ContextCompat.getColor() does not use the night colors (values-night/colors.xml) though it should when night mode is enabled.

Here is the problem:

Hi everyone,

So I'm implementing a dark theme for my Android app, I call this to enable it : AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);

I have set colors in values/colors.xml and there dark version in values-night/colors.xml. The colors changes well depending on the nightMode, BUT :

when I use ContextCompat.getColor(getApplicationContext(), R.id.myColor), it uses the normal colors (values/colors.xml) and not the night colors (values-night/colors.xml).

In my build.gradle, I have set these :

implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0-beta01'

Could someone please tell me what am I doing wrong ?

PS : I already looked at the following question and it does not answer this problem https://stackoverflow.com/questions/57779661/contextcompat-getcolor-method-ignores-night-mode

Upvotes: 20

Views: 3563

Answers (4)

Романыч
Романыч

Reputation: 287

For those who find it later. I've tweaked the solution a bit @ygngy

This answer solves the configuration problem at runtime and leaves the actual context on every call. It could also be used with a dagger so as not to generate a million objects each time.

in some cases, this is necessary when the color needs to be taken at runtime, for example, in a mapper when receiving data from the server, and so on

interface ThemeContextProvider {
    fun getColor(@ColorRes id: Int): Int
    fun getDrawable(@DrawableRes id: Int): Drawable?
}

class ThemeContextProviderImpl @Inject constructor(
    private val application: Context
) : ThemedContext {

    private val res: Resources = application.resources
    private val filter by lazy {
        application.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
    }
    private lateinit var themedContext: Context
    private var configuration = Configuration(res.configuration)

    init {
        getCurrentContext()
    }

    private fun getCurrentContext() {
        configuration.uiMode = when (AppCompatDelegate.getDefaultNightMode()) {
            AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO or filter
            AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES or filter
            else -> res.configuration.uiMode
        }
        themedContext = application.createConfigurationContext(configuration)
    }

    override fun getColor(@ColorRes id: Int): Int {
        getCurrentContext()
        return ContextCompat.getColor(themedContext, id)
    }

    override fun getDrawable(@DrawableRes id: Int): Drawable? {
        getCurrentContext()
        return ContextCompat.getDrawable(themedContext, id)
    }
}

Upvotes: 1

Amani
Amani

Reputation: 3979

Application context don't know anything about current theme or day/night so if you get a resource from application context, you will get the resource in default app theme. A solution to this problem is to use activity/fragment context but in some situations you may not have activity or fragment and only have application context. I have created a context wrapper class to add Day & Night theme to Application context:

import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat

class ThemedContext(application: Context) {

    private val themedContext: Context

    init {
        val res: Resources = application.resources
        val configuration = Configuration(res.configuration)
        val filter = res.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()

        configuration.uiMode = when (AppCompatDelegate.getDefaultNightMode()) {
            AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO or filter
            AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES or filter
            else -> res.configuration.uiMode
        }

        themedContext = application.createConfigurationContext(configuration)
    }

    fun getColor(@ColorRes id: Int) = ContextCompat.getColor(themedContext, id)
    fun getDrawable(@DrawableRes id: Int) = ContextCompat.getDrawable(themedContext, id)
    //todo Add other getter methods as needed...
}

This code has been tested and worked.

Upvotes: 6

xh jia
xh jia

Reputation: 21

I faced similar question as you, and I also found the core of the issue is that Application don't have a theme wrapper like Activity. so W0rmH0le's answer can resolve this problem. but for me, there are lots of code can't fetch an activity or view's context. so I thought why don't we create one singleton Context wrapper nightMode theme. there is the code, and it works fine for me.

Resources res = getApplication().getResources();
Configuration configuration = new Configuration(res.getConfiguration());
int nightNode = AppCompatDelegate.getDefaultNightMode();
if (nightNode == AppCompatDelegate.MODE_NIGHT_NO) {
    configuration.uiMode = Configuration.UI_MODE_NIGHT_NO | (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
} else if (nightNode == AppCompatDelegate.MODE_NIGHT_YES) {
    configuration.uiMode = Configuration.UI_MODE_NIGHT_YES| (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
} else {
    configuration.uiMode = res.getConfiguration().uiMode;
}
mThemeContext = getApplication().createConfigurationContext(configuration);

then you can use mThemeContext replace Application, and you can found the right color followed night mode.

Upvotes: 2

guipivoto
guipivoto

Reputation: 18677

I faced similar issues with night mode. Some screens were fine but others were keeping the regular theme. In the end, I found out that I was instantiating some views using the Application's context instead of the current's activity context. For some reason, Application's context does not track this kind of information.

So, update your code to use current's activity context instead of the application context.

For reference for other users. Avoid:

ContextCompat.getColor(getApplicationContext(), R.id.myColor)

And use:

// In a Activity
ContextCompat.getColor(this, R.id.myColor)

// In a View
ContextCompat.getColor(getContext(), R.id.myColor)

// In a Fragment (check against null because getContext can trigger a NPE
Context context = getContext()
if (context != null) {
    ContextCompat.getColor(context, R.id.myColor)
}

Upvotes: 29

Related Questions