在 Android 和 Hilt 中限定作用域

1,700 阅读5分钟

将对象 A 的作用域限定到对象 B,指的是对象 B 的整个生命周期内始终持有相同的 A 实例。当涉及到 DI (依赖项注入) 时,限定对象 A 的作用域为一个容器,则意味着该容器在销毁之前始终提供相同的 A 实例。

在 Hilt 中,您可以通过注解将类型的作用域限定在某些容器或组件内。例如,您的应用中有一个处理登录和注销的 UserManager 类型。您可以使用 @Singleton 注解将该类型的作用域限定为 ApplicationComponent (ApplicationComponent 是一个被整个应用的生命周期管理的容器)。被限定作用域的类型在应用组件中沿 组件层次结构 向下传递: 在本案例中,相同的 UserManager 实例将被提供给层次结构内其余的 Hilt 组件。应用中任何依赖于 UserManager 的类型都将获得相同的实例。

注意 : 默认情况下,Hilt 中的绑定都 未限定作用域 。这些绑定不属于任何组件,并且可以在整个项目中被访问。每次被请求都会提供该类型的不同实例。当您将绑定的作用域限定为某个组件时,它会限制您使用该绑定的范围以及该类型可以具有的依赖项。

在 Android 中,您不使用 DI 库也可以通过 Android Framework 来手动限定作用域。让我们看看如何手动限定作用域,以及如何改用 Hilt 来限定作用域。最后,我们将比较使用 Android Framework 手动限定作用域和使用 Hilt 限定作用域的区别。

在 Android 中限定作用域

看了上文的定义,您可能会有这样的异议: 在某个特定类中使用一个类型的实例变量也可以做到限定该变量类型的作用域。没错!不使用 DI 时,您可以执行如下操作:

class ExampleActivity : AppCompatActivity() {

  private val analyticsAdapter = AnalyticsAdapter()
  ...

}

analyticsAdapter 变量的作用域被限定为 MyActivity 的生命周期,这意味着只要 Activity 没有被销毁,该变量就是同一个实例。如果另一个类出于某种原因需要访问这个被限定了作用域的变量,每次访问也会获得相同实例。当新的 MyActivity 实例被创建时 (如系统设置改变),一个新的 AnalyticsAdapter 实例将会被创建。

使用 Hilt,等效代码如下:

@ActivityScoped
class AnalyticsAdapter @Inject constructor() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

 @Inject lateinit var analyticsAdapter: AnalyticsAdapter

}

每次创建的 MyActivity 都会持有一个 ActivityComponent DI 容器的新实例,在 Activity 被销毁之前,该实例将向 组件层次结构 下的依赖项提供相同的 AnalyticsAdapter 实例。

更改系统设置后,您将获得一个新的 AnalyticsAdapter 和 MainActivity 实例

通过 ViewModel 限定作用域

然而,我们可能希望 AnalyticsAdapter 可以在系统设置更改后留存!或者说,我们希望直到用户离开 Activity 之前,都限定该实例的作用域为 Activity。

为此,您可以使用 组件架构中的 ViewModel,因为它可以在系统设置更改后留存。

不使用依赖项注入时,您可能有如下代码:

class AnalyticsAdapter() { ... }

class ExampleViewModel() : ViewModel() {
  val analyticsAdapter = AnalyticsAdapter()
}

class ExampleActivity : AppCompatActivity() {

  private val viewModel: ExampleViewModel by viewModels()
  private val analyticsAdapter = viewModel.analyticsAdapter

}

通过这种方式,您将 AnalyticsAdapter 的作用域限定为 ViewModel。因为 Activity 具有 ViewModel 的访问权限,所以在该 Activity 中可以始终获得相同的 AnalyticsAdapter 实例。

通过使用 Hilt,您可以通过限定 AnalyticsAdapter 的作用域为 ActivityRetainedComponent 来实现相同的行为,因为 ActivityRetainedComponent 也可以在系统设置更改后留存。

@ActivityRetainedScoped

class AnalyticsAdapter @Inject constructor() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

@Inject lateinit var analyticsAdapter: AnalyticsAdapter

}

通过使用 ViewModel 或者 Hilt 中的 ActivityRetainedScope 注解,您可以在系统设置更改后获得相同的实例

如果您希望在遵循良好的 DI 实践的同时,保留 ViewModel 用于处理视图逻辑,您可以使用 @ViewModelInject 提供 ViewModel 的依赖项,该注解的详细描述请参见: 文档 | 使用 Hilt 注入 ViewModel 对象。这样一来,AnalyticsAdapter 的作用域就无需被限定为 ActivityRetainedComponent,因为此时它的作用域被手动限定为 ViewModel:

class AnalyticsAdapter @Inject constructor() { ... }

class ExampleViewModel @ViewModelInject constructor(
  private val analyticsAdapter: AnalyticsAdapter
) : ViewModel() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  private val viewModel: ExampleViewModel by viewModels()
  private val analyticsAdapter = viewModel.analyticsAdapter

}

我们刚才所看到的内容,可以应用到任何由 Android Framework 生命周期类管理的 Hilt 组件中。点击查看 全部可用作用域。回到我们最初的示例,将作用域限定为 ApplicationComponent,等同于不使用 DI 框架时在 Application 类中持有该实例。

对比 Hilt 及 ViewModel 限定作用域

使用 Hilt 限定作用域,优势为您可在 Hilt 组件层次结构中使用被限定的类型;而对于 ViewModel,则必须通过 ViewModel 手动访问被限定作用域的类型。

使用 ViewModel 限定作用域,优势为您可以在应用中任何 LifecyclerOwner 对象中持有 ViewModel。例如,如果您使用了 Jetpack Navigation 库,则可以将 ViewModel 绑定到 NavGraph 上。

Hilt 提供的作用域数量有限。可能没有符合您特定使用场景的作用域。例如嵌套 Fragment,对于这种情况,您可以退一步使用 ViewModel 限定作用域。

使用 Hilt 注入 ViewModel

如上文所述,您可以使用 @ViewModelInject 向 ViewModel 注入依赖项。其原理是这些绑定关系保存在 ActivityRetainedComponent 中,这也是为什么您只能注入未限定作用域的类型,或者是限定作用域为 ActivityRetainedComponent 以及 ApplicationComponent 的类型。

如果 Activity 或 Fragment 被 @AndroidEntryPoint 注解修饰,就可以通过 getDefaultViewModelProviderFactory() 方法获取 Hilt 生成的 ViewModel 工厂了。由于可以在 ViewModelProvider 中使用这些 ViewModel 工厂,使您获取 ViewModel 的方式变得更加灵活。例如: 将作用域限定为 BackStackEntry 的 ViewModel。

限定作用域会有一些代价,因为提供的对象在持有者被销毁之前将一直保留在内存中。请在应用中慎重地考虑使用限定作用域的对象。如果对象的内部状态要求使用同一实例,对象需要同步,或者对象的创建成本很高,那么限定作用域是恰当的做法。

当然,当您需要限定作用域时,您可以使用 Hilt 中的作用域注解,也可以直接使用 Android Framework。