Changing locale inside an Android app was never been easy. With androidx.appcompat:appcompat:1.3.0-alpha02, it seems that changing locale in an application has become much more difficult than I imagined. It appears that activity context and application context behaves very differently. If I change the locale of activities using a common BaseActivity (like below), it will work for the corresponding activity.
BaseActivity.java
public class BaseActivity extends AppCompatActivity {
private Locale currentLocale;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
currentLocale = LangUtils.updateLanguage(this);
super.onCreate(savedInstanceState);
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(LangUtils.attachBaseContext(newBase));
}
@Override
protected void onResume() {
super.onResume();
if (currentLocale != LangUtils.getLocaleByLanguage(this)) recreate();
}
}
But I need to change the locale of application context as well as this is only limited to activities. To do that, I can easily override Application#attachBaseContext() to update locale just as above.
MyApplication.java
public class MyApplication extends Application {
private static MyApplication instance;
@NonNull
public static MyApplication getInstance() {
return instance;
}
@NonNull
public static Context getContext() {
return instance.getBaseContext();
}
@Override
public void onCreate() {
instance = this;
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LangUtils.attachBaseContext(base));
}
}
While this successfully changes the locale of the application context, the activity context no longer respects the custom locale (regardless of whether I extend each activity from BaseActivity or not). Weird.
LangUtils.java
public final class LangUtils {
public static final String LANG_AUTO = "auto";
private static Map<String, Locale> sLocaleMap;
private static Locale sDefaultLocale;
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
sDefaultLocale = LocaleList.getDefault().get(0);
} else sDefaultLocale = Locale.getDefault();
}
public static Locale updateLanguage(@NonNull Context context) {
Resources resources = context.getResources();
Configuration config = resources.getConfiguration();
Locale currentLocale = getLocaleByLanguage(context);
config.setLocale(currentLocale);
DisplayMetrics dm = resources.getDisplayMetrics();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N){
context.getApplicationContext().createConfigurationContext(config);
} else {
resources.updateConfiguration(config, dm);
}
return currentLocale;
}
public static Locale getLocaleByLanguage(Context context) {
// Get language from shared preferences
String language = AppPref.getNewInstance(context).getString(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR);
if (sLocaleMap == null) {
String[] languages = context.getResources().getStringArray(R.array.languages_key);
sLocaleMap = new HashMap<>(languages.length);
for (String lang : languages) {
if (LANG_AUTO.equals(lang)) {
sLocaleMap.put(LANG_AUTO, sDefaultLocale);
} else {
String[] langComponents = lang.split("-", 2);
if (langComponents.length == 1) {
sLocaleMap.put(lang, new Locale(langComponents[0]));
} else if (langComponents.length == 2) {
sLocaleMap.put(lang, new Locale(langComponents[0], langComponents[1]));
} else {
Log.d("LangUtils", "Invalid language: " + lang);
sLocaleMap.put(LANG_AUTO, sDefaultLocale);
}
}
}
}
Locale locale = sLocaleMap.get(language);
return locale != null ? locale : sDefaultLocale;
}
public static Context attachBaseContext(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return updateResources(context);
} else {
return context;
}
}
@TargetApi(Build.VERSION_CODES.N)
private static Context updateResources(@NonNull Context context) {
Resources resources = context.getResources();
Locale locale = getLocaleByLanguage(context);
Configuration configuration = resources.getConfiguration();
configuration.setLocale(locale);
configuration.setLocales(new LocaleList(locale));
return context.createConfigurationContext(configuration);
}
}
Therefore, my conclusions are:
- If locale is set in the application context, regardless of whether you set activity context or not, locale will be set to application context only and not to activity (or any other) context.
- If locale isn't set in the application context but set in the activity context, the locale will be set to the activity context.
The workarounds that I can think of are:
- Set locale in the activity context and use them everywhere. But notifications, etc. will not work if there isn't any opened activity.
- Set locale in the application context and use it everywhere. But it means that you cannot take advantage of
Context#getResources()for an activity.
EDIT(30 Oct 2020): Some people have suggested using a ContextWrapper. I've tried using one (like below) but still the same issue. As soon as I wrap the application context using the context wrapper, locale stops working for activities and fragments. Nothing changes.
public class MyContextWrapper extends ContextWrapper {
public MyContextWrapper(Context base) {
super(base);
}
@NonNull
public static ContextWrapper wrap(@NonNull Context context) {
Resources res = context.getResources();
Configuration configuration = res.getConfiguration();
Locale locale = LangUtils.getLocaleByLanguage(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocale(locale);
LocaleList localeList = new LocaleList(locale);
LocaleList.setDefault(localeList);
configuration.setLocales(localeList);
} else {
configuration.setLocale(locale);
DisplayMetrics dm = res.getDisplayMetrics();
res.updateConfiguration(configuration, dm);
}
configuration.setLayoutDirection(locale);
context = context.createConfigurationContext(configuration);
return new MyContextWrapper(context);
}
}