处理配置更改

应用运行期间,某些设备配置可能会发生变化。这些包括但不限于:

  • 应用显示大小
  • 屏幕方向
  • 字体大小和粗细
  • 语言环境
  • 深色模式与浅色模式
  • 键盘可用性

大多数这些配置更改是由于某些用户交互造成的。例如,旋转或折叠设备会改变应用可用的屏幕空间量。同样,更改设备设置(如字体大小、语言或首选主题)也会更改Configuration 对象中相应的值。

这些参数通常需要对应用程序的 UI 进行足够大的更改,以至于 Android 平台为此类更改提供了一种专门构建的机制。此机制是Activity 重新创建

Activity 重新创建

发生配置更改时,系统会重新创建Activity。为此,系统会调用onDestroy() 并销毁现有的Activity 实例。然后,它使用onCreate() 创建一个新实例,并且此新的Activity 实例将使用新的、更新的配置进行初始化。这也意味着系统还会使用新配置重新创建 UI。

重新创建行为通过自动使用与新设备配置匹配的替代资源重新加载应用程序来帮助您的应用程序适应新的配置。

重新创建示例

考虑一个使用android:text="@string/title"(如在布局 XML 文件中定义)显示静态标题的TextView。创建视图时,它会根据当前语言只设置一次文本。如果语言发生更改,系统会重新创建 activity。因此,系统还会重新创建视图并根据新语言将其初始化为正确的值。

重新创建还会清除作为Activity 或其包含的任何FragmentView 或其他对象中的字段保留的任何状态。这是因为Activity 重新创建会创建Activity 和 UI 的全新实例。此外,旧的Activity 不再可见或有效,因此对它或其包含对象的任何剩余引用都是过时的。它们会导致错误、内存泄漏和崩溃。

用户期望

应用程序用户期望状态能够保持不变。如果用户正在填写表单,并在多窗口模式下打开另一个应用程序以参考信息,那么如果他们返回时表单已清空或跳转到应用程序的其他位置,则用户体验很差。作为开发者,您必须在配置更改和活动重建过程中提供一致的用户体验。

要验证您的应用程序中是否保留了状态,您可以执行在应用程序处于前台和后台时都会导致配置更改的操作。这些操作包括

  • 旋转设备
  • 进入多窗口模式
  • 在多窗口模式或自由窗格模式下调整应用程序大小
  • 折叠具有多个显示屏的可折叠设备
  • 更改系统主题,例如深色模式与浅色模式
  • 更改字体大小
  • 更改系统或应用程序语言
  • 连接或断开硬件键盘
  • 连接或断开扩展坞

您可以采用三种主要方法来在Activity重建过程中保留相关状态。使用哪种方法取决于您要保留的状态类型。

  • 本地持久化 用于处理复杂或大型数据的进程死亡。持久性本地存储包括数据库或DataStore
  • 保留的对象,例如ViewModel 实例,用于在用户积极使用应用程序时在内存中处理与 UI 相关的状态。
  • 保存的实例状态 用于处理系统启动的进程死亡并保持依赖于用户输入或导航的瞬态状态。

要详细了解每个 API,以及何时使用每个 API 是合适的,请参阅保存 UI 状态

限制 Activity 重建

您可以阻止某些配置更改自动重建 Activity。Activity重建会导致重新创建整个 UI 和任何从Activity派生的对象。您可能有充分的理由避免这种情况。例如,您的应用程序可能不需要在特定配置更改期间更新资源,或者您可能有性能限制。在这种情况下,您可以声明您的 Activity 自行处理配置更改,并阻止系统重新启动您的 Activity。

要为特定配置更改禁用 Activity 重建,请将配置类型添加到AndroidManifest.xml 文件中<activity> 条目中的android:configChanges。可能的取值出现在android:configChanges 属性的文档中。

以下清单代码在屏幕方向和键盘可用性更改时禁用MyActivityActivity重建。

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

某些配置更改总是会导致 Activity 重新启动。您无法禁用它们。例如,您无法禁用 Android 12L(API 级别 32)中引入的动态颜色更改

在 View 系统中响应配置更改

View系统中,当发生您已禁用Activity重建的配置更改时,Activity 会收到对Activity.onConfigurationChanged()的调用。任何附加的视图也会收到对View.onConfigurationChanged()的调用。对于您未添加到android:configChanges的配置更改,系统会照常重新创建 Activity。

onConfigurationChanged()回调方法会接收一个Configuration对象,该对象指定新的设备配置。读取Configuration对象中的字段以确定您的新配置是什么。要进行后续更改,请更新您在界面中使用的资源。当系统调用此方法时,您的 Activity 的Resources对象会更新为基于新配置返回资源。这使您可以重置 UI 的元素,而无需系统重新启动您的 Activity。

例如,以下onConfigurationChanged()实现检查键盘是否可用

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
    } else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
    }
}

Java

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
    } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
    }
}

如果您不需要根据这些配置更改更新您的应用程序,则可以不实现onConfigurationChanged()。在这种情况下,配置更改前使用的所有资源仍然被使用,您只是避免了 Activity 的重新启动。例如,电视应用程序可能不希望在连接或断开蓝牙键盘时做出反应。

保留状态

使用此技术时,您仍然必须在正常的 Activity 生命周期中保留状态。这是因为以下原因:

  • 不可避免的更改:您无法阻止的配置更改可能会重新启动您的应用程序。
  • 进程死亡:您的应用程序必须能够处理系统启动的进程死亡。如果用户离开您的应用程序并且应用程序转到后台,系统可能会销毁该应用程序。

在 Jetpack Compose 中响应配置更改

Jetpack Compose 使您的应用程序更容易响应配置更改。但是,如果您为所有可以禁用 Activity 重建的配置更改禁用了 Activity 重建,您的应用程序仍然必须正确处理配置更改。

Configuration对象可通过LocalConfiguration组合局部对象在 Compose UI 层次结构中使用。每当它发生变化时,从LocalConfiguration.current读取的可组合函数都会重新组合。有关组合局部对象的工作方式的信息,请参阅使用 CompositionLocal 进行局部作用域数据

示例

在以下示例中,可组合项会以特定格式显示日期。可组合项通过使用LocalConfiguration.current调用ConfigurationCompat.getLocales()来响应系统区域设置配置更改。

@Composable
fun DateText(year: Int, dayOfYear: Int) {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(
        "MMM dd",
        ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
    )
    Text(
        dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
    )
}

为了避免在区域设置更改时重建Activity,托管 Compose 代码的Activity需要选择退出区域设置配置更改。为此,您需要将android:configChanges设置为locale|layoutDirection

配置更改:关键概念和最佳实践

在处理配置更改时,您需要了解以下关键概念:

  • 配置:设备配置定义了 UI 如何显示给用户,例如应用程序显示大小、区域设置或系统主题。
  • 配置更改:配置通过用户交互发生更改。例如,用户可能会更改设备设置或他们与设备的物理交互方式。无法阻止配置更改。
  • Activity重建:配置更改默认情况下会导致Activity重建。这是一种内置机制,用于针对新配置重新初始化应用程序状态。
  • Activity销毁:Activity重建会导致系统销毁旧的Activity实例并创建一个新的实例来代替它。旧实例现在已过时。对它的任何剩余引用都会导致内存泄漏、错误或崩溃。
  • 状态:Activity实例中的状态不会出现在新的Activity实例中,因为它们是两个不同的对象实例。请按照保存 UI 状态中的说明保留应用程序和用户的状态。
  • 选择退出:对于某种配置更改,选择退出活动重建是一种潜在的优化方法。这要求你的应用能够正确响应新的配置。

为了提供良好的用户体验,请遵守以下最佳实践

  • 做好应对频繁配置更改的准备:不要假设配置更改很少发生或根本不会发生,无论 API 级别、外形尺寸或 UI 工具包如何。当用户导致配置更改时,他们期望应用能够更新并继续使用新的配置正常工作。
  • 保留状态:Activity重建发生时,不要丢失用户的状态。请按照保存 UI 状态中所述保留状态。
  • 避免将选择退出作为快速修复:不要将选择退出Activity重建作为避免状态丢失的捷径。选择退出活动重建要求你履行处理更改的承诺,并且你仍然可能由于其他配置更改、进程死亡或关闭应用而导致状态丢失。Activity重建是无法完全禁用的。请按照保存 UI 状态中所述保留状态。
  • 不要避免配置更改:不要对方向、纵横比或可调整大小性设置限制以避免配置更改和Activity重建。这会对希望以自己偏好方式使用你的应用的用户产生负面影响。

处理基于大小的配置更改

基于大小的配置更改随时可能发生,当你的应用在用户可以进入多窗口模式大屏幕设备上运行时,这种情况更常见。他们期望你的应用在这种环境下也能良好运行。

一般有两种大小更改:显著的和不显著的。显著的大小更改是指由于屏幕大小(例如宽度、高度或最小宽度)的差异,导致应用于新配置的一组不同的替代资源发生变化。这些资源包括应用本身定义的资源以及其任何库中的资源。

限制基于大小的配置更改的活动重建

当你禁用基于大小的配置更改的Activity重建时,系统不会重建Activity。相反,它会调用Activity.onConfigurationChanged()。任何附加的视图都会收到对View.onConfigurationChanged()的调用。

当你manifest文件中包含android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout"时,基于大小的配置更改的Activity重建会被禁用。

允许基于大小的配置更改的活动重建

在 Android 7.0(API 级别 24)及更高版本上,只有当大小更改显著时,才会发生基于大小的配置更改的Activity重建。当系统由于大小不足而没有重建Activity时,系统可能会改而调用Activity.onConfigurationChanged()View.onConfigurationChanged()

关于Activity未重建时ActivityView回调,有一些需要注意的事项

  • 在 Android 11(API 级别 30)到 Android 13(API 级别 33)中,不会调用Activity.onConfigurationChanged()
  • 已知存在一个问题,即在 Android 12L(API 级别 32)和早期版本的 Android 13(API 级别 33)中,在某些情况下可能不会调用View.onConfigurationChanged()。有关更多信息,请参阅此公共问题。此问题已在后续的 Android 13 版本和 Android 14 中得到解决。

对于依赖于侦听基于大小的配置更改的代码,我们建议使用具有重写的View.onConfigurationChanged()的实用程序View,而不是依赖于Activity重建或Activity.onConfigurationChanged()