Modify the record Modify the time
new 2021.01.09

Quick location and repair

How to call when a problem occurs:

public class I18nBaseActivity extends AppCompatActivity {
    @Override
    protected void attachBaseContext(Context newBase) {
      	// Switch languages, then overwrite the newly generated context with attachBaseContext()
        Context context = MultiLanguageUtils.changeContextLocale(newBase);
        super.attachBaseContext(context); }}Copy the code

Solutions:

Androidx(AppCompat :1.2.0) wraps a layer of ContextThemeWrapper with attachBaseContext(), but it is this layer of logic that causes the multilanguage switch to be delayed. So let’s wrap it up manually

public class I18nBaseActivity extends AppCompatActivity {
    @Override
    protected void attachBaseContext(Context newBase) {
      	// Switch languages, then overwrite the newly generated context with attachBaseContext()
        Context context = MultiLanguageUtils.changeContextLocale(newBase);
       // Language switching failure after compatibility with AppCompat 1.2.0
        final Configuration configuration = context.getResources().getConfiguration();
        final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(context,
                R.style.Base_Theme_AppCompat_Empty) {
            @Override
            public void applyOverrideConfiguration(Configuration overrideConfiguration) {
                if(overrideConfiguration ! =null) {
                    overrideConfiguration.setTo(configuration);
                }
                super.applyOverrideConfiguration(overrideConfiguration); }};super.attachBaseContext(wrappedContext); }}Copy the code

encapsulation

The above only explains how to solve the problem, not the implementation of multilanguage switch. So I’ve packaged a library (essentially a utility class) that’s already adapted to the problem, so you can copy it out and use it

Making: github.com/StefanShan/…

Detailed troubleshooting process and principles

Recently, the project was upgraded to Androidx, and the previous multilingual switch failed. After a little way out, found is due to rise to the project after Androidx Androidx is introduced. The appcompat: appcompat: 1.2.0 v7 bag instead of before. Let’s see why, according to the principle of multi-language switching.

Working principle of multi-language switchover: Modify the Locale configuration of a context and set the newly generated context to attachBaseContext to replace the Locale configuration.

Take a look at the source code for AppCompatActivity# attachBaseContext() on AndroidX

@Override
protected void attachBaseContext(Context newBase) {
  super.attachBaseContext(getDelegate().attachBaseContext2(newBase));
}
Copy the code

There is a proxy class that handles the incoming context. Look at attachBaseContext2() of getDelegate().

/ * * *@return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate(a) {
  if (mDelegate == null) {
    mDelegate = AppCompatDelegate.create(this.this);	// Proxy objects are built on AppCompatDelegate create, so read on
  }
  return mDelegate;
}
Copy the code
// Look directly at the AppCompatDelegateImpl class, which is an implementation of the AppCompatDelegate class

@NonNull
@Override
@CallSuper
public Context attachBaseContext2(@NonNull final Context baseContext) {
  / /...

  /** * If the incoming context is wrapped with ContextThemeWrapper, the context configuration will be overwritten directly
  // If the base context is a ContextThemeWrapper (thus not an Application context)
  // and nobody's touched its Resources yet, we can shortcut and directly apply our
  // override configuration.
  if (sCanApplyOverrideConfiguration
      && baseContext instanceof android.view.ContextThemeWrapper) {
    final Configuration config = createOverrideConfigurationForDayNight(
      baseContext, modeToApply, null);
    if (DEBUG) {
      Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                               config.toString()));
    }

    try {
      ContextThemeWrapperCompatApi17Impl.applyOverrideConfiguration(
        (android.view.ContextThemeWrapper) baseContext, config);
      return baseContext;
    } catch (IllegalStateException e) {
      if (DEBUG) {
        Log.d(TAG, "Failed to apply configuration to base context", e); }}}/ /...

  /** * Get the configuration through packageManger and compare it to the context configuration. * The key factor is that the configuration obtained by the packageManager is diff updated with the context configuration, and the DIFF result is assigned to the new configration. After the switchover succeeds, the language configured by the packageManager is the same as that configured by the Context, and the new configration is skipped without assigning a value. Eventually, multilingual switching fails. Similarly, if you set multiple languages from ActivityA, restart ActivityA, and jump from ActivityA to ActivityB, ActivityB multilingualism does not take effect. * /
  // We can't trust the application resources returned from the base context, since they
  // may have been altered by the caller, so instead we'll obtain them directly from the
  // Package Manager.
  final Configuration appConfig;
  try {
    appConfig = baseContext.getPackageManager().getResourcesForApplication(
      baseContext.getApplicationInfo()).getConfiguration();
  } catch (PackageManager.NameNotFoundException e) {
    throw new RuntimeException("Application failed to obtain resources from itself", e);
  }

  // The caller may have directly modified the base configuration, so we'll defensively
  // re-structure their changes as a configuration overlay and merge them with our own
  // night mode changes. Diffing against the application configuration reveals any changes.
  final Configuration baseConfig = baseContext.getResources().getConfiguration();
  final Configuration configOverlay;
  if(! appConfig.equals(baseConfig)) { configOverlay = generateConfigDelta(appConfig, baseConfig);// Here is the key
    if (DEBUG) {
      Log.d(TAG,
            "Application config (" + appConfig + ") does not match base config ("
            + baseConfig + "), using base overlay: "+ configOverlay); }}else {
    configOverlay = null;
    if (DEBUG) {
      Log.d(TAG, "Application config (" + appConfig + ") matches base context "
            + "config, using empty base overlay"); }}final Configuration config = createOverrideConfigurationForDayNight(
    baseContext, modeToApply, configOverlay);
  if (DEBUG) {
    Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
                             + "applyOverrideConfiguration(). Config: %s", config.toString()));
  }

  // Next, we'll wrap the base context to ensure any method overrides or themes are left
  // intact. Since ThemeOverlay.AppCompat theme is empty, we'll get the base context's theme.
  final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(baseContext,
                                                                     R.style.Theme_AppCompat_Empty);
  wrappedContext.applyOverrideConfiguration(config);

  / /...

  return super.attachBaseContext2(wrappedContext);
}
Copy the code
@NonNull
private static Configuration generateConfigDelta(@NonNull Configuration base,
                                                 @Nullable Configuration change) {
  final Configuration delta = new Configuration();
  delta.fontScale = 0;

  / /...
  
  // If the two configurations are equal, the locale of the newly created delta is not assigned.
  if (Build.VERSION.SDK_INT >= 24) {
    ConfigurationImplApi24.generateConfigDelta_locale(base, change, delta); 
  } else {
    if (!ObjectsCompat.equals(base.locale, change.locale)) {	
      delta.locale = change.locale;
    }
  }
  
	/ /...
}
Copy the code

Ok, the comments above are very clear. Here is a brief summary:

The AppCompatActivity# attachBaseContext() method is wrapped on Androidx and is implemented on AppCompatDelegateImpl# attachBaseContext2(). The packaging method implements two sets of logic:

  1. The context passed in is wrapped with ContextThemeWrapper and is overwritten directly with the context configuration (containing the language)
  2. If the context is not wrapped with ContextThemeWrapper, it obtains the configuration (containing language) from PackageManger and compares it with the context configuration (containing language). A new configration object is created. If the two configurations are different, they are assigned to ration. If they are the same, they are skipped. Finally, the new one is overwritten as the configuration result.

The multi-language problem occurs in [2]. If the PackageManager is consistent with a configuration item in the context, it will not assign a value to the new configration. This will cause that when the switch is successful, when the process is started next time, the language configured by the packageManager is the same as that configured by the context, and the language is skipped without assigning a value to the new configration. In the end, the multi-language failure occurs.