Limiting the scope of object A to object B means that object B holds the same instance of A throughout its life. When it comes to DI (dependency injection), scoping object A to A container means that the container always provides the same instance of A until it is destroyed.

In Hilt, you can limit the scope of a type to certain containers or components through annotations. For example, your application has a UserManager type that handles login and logout. You can use the @Singleton annotation to limit the scope of this type to ApplicationComponent (ApplicationComponent is a container managed by the entire application lifecycle). Scoped types are passed down the component hierarchy in the application component: in this case, the same UserManager instance will be provided to the remaining Hilt components within the hierarchy. Any type in the application that depends on UserManager will get the same instance.

Note: By default, bindings in Hilt are not scoped. These bindings do not belong to any component and can be accessed throughout the project. A different instance of the type is provided each time it is requested. When you limit the scope of a binding to a component, it limits the scope you can use the binding and the dependencies that type can have.

In Android, you can manually qualify the scope through the Android Framework without using the DI library. Let’s look at how to scope manually and how to use Hilt to scope. Finally, we’ll compare the difference between manually scoping with the Android Framework and scoping with Hilt.

Scoping in Android

Looking at the above definition, you might argue that using an instance variable of a type in a particular class can also limit the scope of that variable type. That’s right! If DI is not used, you can perform the following operations:

class ExampleActivity : AppCompatActivity() {

  private val analyticsAdapter = AnalyticsAdapter()
  ...

}
Copy the code

The scope of the analyticsAdapter variable is limited to the life cycle of the MyActivity, which means that as long as the Activity is not destroyed, the variable is the same instance. If another class needs to access the scoped variable for some reason, it will get the same instance every time it accesses it. When a new Instance of MyActivity is created (if system Settings change), a new Instance of AnalyticsAdapter will be created.

Using Hilt, the equivalent code is as follows:

@ActivityScoped
class AnalyticsAdapter @Inject constructor() {... }@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

 @Inject lateinit var analyticsAdapter: AnalyticsAdapter

}
Copy the code

Each MyActivity created holds a new instance of the ActivityComponent DI container, which provides the same AnalyticsAdapter instance to dependencies in the component hierarchy until the Activity is destroyed.

After you change your system Settings, you get a new AnalyticsAdapter and MainActivity instance

Scope is defined by the ViewModel

However, we might want AnalyticsAdapter to persist after system Settings change! In other words, we want the instance to be scoped to Activity until the user leaves the Activity.

To do this, you can use the ViewModel in the component architecture, because it can persist after system Settings change.

Without dependency injection, you might have code like this:

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

class ExampleActivity : AppCompatActivity() {

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

}
Copy the code

In this way, you limit the scope of the AnalyticsAdapter to the ViewModel. Because the Activity has access to the ViewModel, you can always get the same AnalyticsAdapter instance in that Activity.

Through the use of Hilt, you can limit the scope for ActivityRetainedComponent AnalyticsAdapter to achieve the same behavior, because ActivityRetainedComponent can also be retained after the system is set to change.

@ActivityRetainedScoped

class AnalyticsAdapter @Inject constructor() {... }@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

@Inject lateinit var analyticsAdapter: AnalyticsAdapter

}
Copy the code

By using the ActivityRetainedScope annotation in the ViewModel or Hilt, you can get the same instance after the system Settings change

If you want to preserve ViewModel for handling view logic while following good DI practices, you can use @ViewModelInject to provide ViewModel dependencies. See the annotations for a detailed description: Document | use a Hilt ViewModel object. Thus, AnalyticsAdapter that will not be limited to the scope of the ActivityRetainedComponent, because its scope is limited to manually 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

}
Copy the code

What we’ve just seen can be applied to any Hilt component managed by the Android Framework lifecycle classes. Click to see all available scopes. Going back to our original example, scoping to ApplicationComponent is equivalent to holding the instance in the Application class without using the DI framework.

Contrast Hilt with ViewModel scoping

Using Hilt scoping has the advantage that you can use qualified types in a Hilt component hierarchy; In the case of the ViewModel, the scoped type must be manually accessed through the ViewModel.

Using ViewModel scoping has the advantage that you can hold the ViewModel in any LifecyclerOwner object in your application. For example, if you use the Jetpack Navigation library, you can bind the ViewModel to NavGraph.

Hilt provides a limited number of scopes. There may not be a scope that fits your particular usage scenario. In nested fragments, for example, you can take a step back and use ViewModel scoping.

Inject the ViewModel using Hilt

As mentioned above, you can inject dependencies into the ViewModel using @ViewModelInject. Its principle is that the binding relationship stored in ActivityRetainedComponent, that is why you can only type was injected scoped, Or is scoped ActivityRetainedComponent and ApplicationComponent type.

If the Activity or fragments is decorated @ AndroidEntryPoint annotations, can be used getDefaultViewModelProviderFactory () method to get the ViewModel to generate the Hilt of the factory. The fact that you can use these ViewModel factories in the ViewModelProvider makes the way you get the ViewModel more flexible. For example: ViewModel scoped to BackStackEntry.

There are costs to scoping, because supplied objects remain in memory until the owner is destroyed. Think carefully about using scoped objects in your applications. Scoping is appropriate if the internal state of the object requires the same instance, the object needs to be synchronized, or the object is expensive to create.

Of course, when you need to scope, you can use the scope annotations in Hilt, or you can use the Android Framework directly.