Конфигурация контекста приложения Android не обновляется с учетом новой локали

#android #locale

#Android #локаль

Вопрос:

Мне нужно получить доступ к локализованным строкам приложения, когда я не использую Context связанные классы (Activites, Fragments, Services, Views и т.д.) В ViewModel или специально созданных классах, где я сохраняю всю свою бизнес-логику. Например, отправка String в UI с помощью LiveData :

 toastMessageLiveData.value = buildLastLoginToastMessage()

fun buildLastLoginToastMessage() {
   val lastLoginDiff = (System.currentMillis() - getLastLogin()) / DateUtils.HOURS_IN_MILLIS

   return MyApp.instance.getString(R.string.last_login_txt, lastLoginDiff)
}
  

Теперь в Activity я наблюдаю за LiveData изменением, и я показываю всплывающее сообщение.

Это работает нормально, но проблема возникает, если пользователь меняет язык приложения. Макеты обновлены, и я вижу, что используется язык новой локали, но MyApp.instance.getString(R.string....) функция по-прежнему использует старую локаль. Если я принудительно завершу работу приложения и перезапущу его, оно будет работать, потому что приложение attachBaseContext вызывается снова и применяется новая локаль. Однако я не хочу принудительно завершать работу приложения, мне нужно решение для обновления конфигурации приложения.

Служебная функция для создания или обновления Context

 fun buildLocalizedContext(context: Context): Context {
   // From preferences, load the saved Language and Country codes
   val language = getSavedLanguage()
   val country = getSavedCountry()

   // Create the new locale
   val locale = Locale(language, country)
   Locale.setDefault(locale)

   val resources = context.resources
   val configuration = Configuration(resources.configuration)

   // Create a new configration or update the existing one if the API is less than 17
   if (Build.VERSION.SDK_INT >= 17) {
      configuration.setLocale(locale)
      return context.createConfigurationContext(configuration)
   } else {
      configuration.locale = locale
      resources.updateConfiguration(configuration, resources.getDisplayMetrics())
   }

   return context
}
  

При запуске приложения примените новую локаль

 class MyApp: Application {
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(LocaleHelper.instance.buildLocalizedContext(base))
    }
}
  

BaseActivity Класс, который используется другими активностями:

 class BaseActivity: AppCompatActivity {
    override fun attachBaseContext(newBase: Context) {
        super.attachBaseContext(LocaleHelper.instance.buildLocalizedContext(newBase))
    }
}
  

Это то, что мне удалось сделать до сих пор, однако это тоже не работает:

 fun changeLocale(locale: Locale, app: Application) {
    Locale.setDefault(locale)

    val resources = app.resources
    val configuration = resources.configuration

    if (Build.VERSION.SDK_INT >= 17) {
        configuration.setLocale(locale)
    }
    else {
        configuration.locale = locale
        resources.updateConfiguration(configuration, resources.displayMetrics)
    }
}
  

Приведенный выше фрагмент кода не работает, он не изменяет локаль приложения, только если я принудительно перезапущу приложение.

Комментарии:

1. «Это работает нормально, но проблема возникает, если пользователь меняет язык приложения» — у приложений на самом деле нет языков. Ваш setLocale() подход всегда был чем-то вроде взлома с неполной поддержкой в Android. При этом, есть ли какое-либо решение, которое предполагает, что вы обращаетесь к Application не напрямую, а через некоторые ContextWrapper , где вы можете изменить язык, используемый ContextWrapper ?

2. @Commons — Одним из решений было бы отправить идентификаторы строковых ресурсов в Live Data и получить строки из Activity или Fragment , но это нарушает принцип MVVM, перенося бизнес-логику в пользовательский интерфейс. В приведенном выше примере вы можете видеть, что бизнес-логика вычисления последнего входа в систему и построения строки выполнена в ViewModel . Проблема возникает при вызове MyApp.instance.getString(R.string....) функции внутри ViewModel , потому что Locale внутри MyApp.instance.configuration.locale не обновляется с новой локализацией, только если я перезапускаю приложение.

3. Я хочу сказать, что вместо вызова MyApp.instance.getString() , возможно, вы можете использовать что-то другое, чем MyApp.instance (например, ContextWrapper around MyApp.instance ), которое по-прежнему является глобальным по объему, но может быть заменено во время выполнения. Или, возможно, строка создается путем вызова функции, в которой вызывающий объект предоставляет Activity .

4. @CommonsWare — спасибо за ваши быстрые ответы. Если бы вы могли привести мне пример, как я мог бы использовать или ContextWrapper обойти MyApp.instance , или как я мог бы получить доступ к Activity из ViewModel , не нарушая MVVM принципа.

5. Что касается последнего, fun buildLastLoginToastMessage(ctxt: Context) в приведенном выше фрагменте кода, где эта функция вызывается вашим действием или фрагментом и передается в действии, и вы используете ctxt для своего getString() вызова. Что касается первого, я просто предлагаю путь для вашего собственного исследования. Лично я бы никогда не написал приложение, которое использовало setLocale() .

Ответ №1:

Как вы уже испытали, язык и несколько других конфигураций применяются только к контекстам просмотра, а не к контексту приложения. Похоже, вы находитесь в странной ситуации. Возможно, вам потребуется пересмотреть свою архитектуру. Обычно вы хотите получить строку внутри вашего представления и передать ее в вашу ViewModel для обработки. Однако, если вы хотите, чтобы это работало так, как есть, возможно, вы можете сохранить слабую ссылку на представление внутри вашего объекта приложения и использовать это для получения локализованных строк. Вы также можете сохранить жесткую ссылку на представление, но тогда вам нужно будет обязательно очистить ее, чтобы избежать утечки памяти. Лучшим решением было бы пересмотреть вашу архитектуру, но это тоже подходящее решение.

 class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        (application as App).activity = WeakReference<Activity>(this)
        }
}

class App : Application {
var activity : WeakReference<Activity>? = null
}