I have an old project that supports multi-languages. I want to upgrade support library and target platform, Before migrating to Androidx
everything works fine but now change language not work!
I use this code to change default locale of App
private static Context updateResources(Context context, String language) { Locale locale = new Locale(language); Locale.setDefault(locale); Configuration configuration = context.getResources().getConfiguration(); configuration.setLocale(locale); return context.createConfigurationContext(configuration); }
And call this method on each activity by override attachBaseContext
like this:
@Override protected void attachBaseContext(Context newBase) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String language = preferences.getString(SELECTED_LANGUAGE, "fa"); super.attachBaseContext(updateResources(newBase, language)); }
I try other method to get string and I noticed that getActivity().getBaseContext().getString
work and getActivity().getString
not work. Even the following code does not work and always show app_name
vlaue in default resource string.xml.
<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/app_name"/>
I share a sample code in https://github.com/Freydoonk/LanguageTest
Also getActivity()..getResources().getIdentifier
not work and always return 0!
UPDATE Aug 21 2020:
AppCompat 1.2.0 was finally released. If you're not using a ContextWrapper
or ContextThemeWrapper
at all, there should be nothing else to do and you should be able remove any workarounds you had from 1.1.0!
If you DO use a ContextWrapper
or ContextThemeWrapper
inside attachBaseContext
, locale changes will break, because when you pass your wrapped context to super,
AppCompatActivity
makes internal calls which wrap your ContextWrapper
in another ContextThemeWrapper
,ContextThemeWrapper
, overrides its configuration to a blank one, similar to what happened back in 1.1.0.But the solution is always the same. I've tried multiple other solutions for situation 2, but as pointed out by @Kreiri in the comments (thanks for your investigative help!), the AppCompatDelegateImpl
always ended up stripping away the locale. The big obstacle is that, unlike in 1.1.0, applyOverrideConfiguration
is called on your base context, not your host activity, so you can't just override that method in your activity and fix the locale as you could in 1.1.0. The only working solution I'm aware of is to reverse the wrapping by overriding getDelegate()
to make sure your wrapping and/or locale override comes last. First, you add the class below:
Kotlin sample (please note that the class MUST be inside the androidx.appcompat.app
package because the only existing AppCompatDelegate
constructor is package private)
package androidx.appcompat.app import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.util.AttributeSet import android.view.MenuInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.Toolbar class BaseContextWrappingDelegate(private val superDelegate: AppCompatDelegate) : AppCompatDelegate() { override fun getSupportActionBar() = superDelegate.supportActionBar override fun setSupportActionBar(toolbar: Toolbar?) = superDelegate.setSupportActionBar(toolbar) override fun getMenuInflater(): MenuInflater? = superDelegate.menuInflater override fun onCreate(savedInstanceState: Bundle?) { superDelegate.onCreate(savedInstanceState) removeActivityDelegate(superDelegate) addActiveDelegate(this) } override fun onPostCreate(savedInstanceState: Bundle?) = superDelegate.onPostCreate(savedInstanceState) override fun onConfigurationChanged(newConfig: Configuration?) = superDelegate.onConfigurationChanged(newConfig) override fun onStart() = superDelegate.onStart() override fun onStop() = superDelegate.onStop() override fun onPostResume() = superDelegate.onPostResume() override fun setTheme(themeResId: Int) = superDelegate.setTheme(themeResId) override fun <T : View?> findViewById(id: Int) = superDelegate.findViewById<T>(id) override fun setContentView(v: View?) = superDelegate.setContentView(v) override fun setContentView(resId: Int) = superDelegate.setContentView(resId) override fun setContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.setContentView(v, lp) override fun addContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.addContentView(v, lp) override fun attachBaseContext2(context: Context) = wrap(superDelegate.attachBaseContext2(super.attachBaseContext2(context))) override fun setTitle(title: CharSequence?) = superDelegate.setTitle(title) override fun invalidateOptionsMenu() = superDelegate.invalidateOptionsMenu() override fun onDestroy() { superDelegate.onDestroy() removeActivityDelegate(this) } override fun getDrawerToggleDelegate() = superDelegate.drawerToggleDelegate override fun requestWindowFeature(featureId: Int) = superDelegate.requestWindowFeature(featureId) override fun hasWindowFeature(featureId: Int) = superDelegate.hasWindowFeature(featureId) override fun startSupportActionMode(callback: ActionMode.Callback) = superDelegate.startSupportActionMode(callback) override fun installViewFactory() = superDelegate.installViewFactory() override fun createView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? = superDelegate.createView(parent, name, context, attrs) override fun setHandleNativeActionModesEnabled(enabled: Boolean) { superDelegate.isHandleNativeActionModesEnabled = enabled } override fun isHandleNativeActionModesEnabled() = superDelegate.isHandleNativeActionModesEnabled override fun onSaveInstanceState(outState: Bundle?) = superDelegate.onSaveInstanceState(outState) override fun applyDayNight() = superDelegate.applyDayNight() override fun setLocalNightMode(mode: Int) { superDelegate.localNightMode = mode } override fun getLocalNightMode() = superDelegate.localNightMode private fun wrap(context: Context): Context { TODO("your wrapping implementation here") } }
Then inside our base activity class you remove all your 1.1.0 workarounds and simply add this:
private var baseContextWrappingDelegate: AppCompatDelegate? = null override fun getDelegate() = baseContextWrappingDelegate ?: BaseContextWrappingDelegate(super.getDelegate()).apply { baseContextWrappingDelegate = this }
Depending on the ContextWrapper
implementation you're using, configuration changes might break theming or locale changes. To fix that, additionally add this:
override fun createConfigurationContext(overrideConfiguration: Configuration) : Context { val context = super.createConfigurationContext(overrideConfiguration) TODO("your wrapping implementation here") }
And you're good! You can expect Google to break this again in 1.3.0. I'll be there to fix it ... See you, space cowboy!
OLD ANSWER AND SOLUTION FOR APPCOMPAT 1.1.0:
Basically what's happening in the background is that while you've set the configuration correctly in attachBaseContext
, the AppCompatDelegateImpl
then goes and overrides the configuration to a completely fresh configuration without a locale:
final Configuration conf = new Configuration(); conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK); try { ... ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf); handled = true; } catch (IllegalStateException e) { ... }
In an unreleased commit by Chris Banes this was actually fixed: The new configuration is a deep copy of the base context's configuration.
final Configuration conf = new Configuration(baseConfiguration); conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK); try { ... ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf); handled = true; } catch (IllegalStateException e) { ... }
Until this is released, it's possible to do the exact same thing manually. To continue using version 1.1.0 add this below your attachBaseContext
:
Kotlin solution
override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) { if (overrideConfiguration != null) { val uiMode = overrideConfiguration.uiMode overrideConfiguration.setTo(baseContext.resources.configuration) overrideConfiguration.uiMode = uiMode } super.applyOverrideConfiguration(overrideConfiguration) }
Java solution
@Override public void applyOverrideConfiguration(Configuration overrideConfiguration) { if (overrideConfiguration != null) { int uiMode = overrideConfiguration.uiMode; overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration()); overrideConfiguration.uiMode = uiMode; } super.applyOverrideConfiguration(overrideConfiguration); }
This code does exactly the same what Configuration(baseConfiguration)
does under the hood, but because we are doing it after the AppCompatDelegate
has already set the correct uiMode
, we have to make sure to take the overridden uiMode
over to after we fix it so we don't lose the dark/light mode setting.
Please note that this only works by itself if you don't specify configChanges="uiMode"
inside your manifest. If you do, then there's yet another bug: Inside onConfigurationChanged
the newConfig.uiMode
won't be set by AppCompatDelegateImpl
's onConfigurationChanged
. This can be fixed as well if you copy all the code AppCompatDelegateImpl
uses to calculate the current night mode to your base activity code and then override it before the super.onConfigurationChanged
call. In Kotlin it would look like this:
private var activityHandlesUiMode = false private var activityHandlesUiModeChecked = false private val isActivityManifestHandlingUiMode: Boolean get() { if (!activityHandlesUiModeChecked) { val pm = packageManager ?: return false activityHandlesUiMode = try { val info = pm.getActivityInfo(ComponentName(this, javaClass), 0) info.configChanges and ActivityInfo.CONFIG_UI_MODE != 0 } catch (e: PackageManager.NameNotFoundException) { false } } activityHandlesUiModeChecked = true return activityHandlesUiMode } override fun onConfigurationChanged(newConfig: Configuration) { if (isActivityManifestHandlingUiMode) { val nightMode = if (delegate.localNightMode != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED) delegate.localNightMode else AppCompatDelegate.getDefaultNightMode() val configNightMode = when (nightMode) { AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO else -> applicationContext.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK } newConfig.uiMode = configNightMode or (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) } super.onConfigurationChanged(newConfig) }
Finally, I find the problem in my app. When migrating the project to Androidx
dependencies of my project changed like this:
dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.appcompat:appcompat:1.1.0-alpha03' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.1.0-alpha04' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha02' }
As it is seen, version of androidx.appcompat:appcompat
is 1.1.0-alpha03
when I changed it to the latest stable version, 1.0.2
, my problem is resolved and the change language working properly.
I find the latest stable version of appcompat
library in Maven Repository. I also change other libraries to the latest stable version.
Now my app dependencies section is like bellow:
dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.0.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With