Scoping in Android and Hilt

Scoping in Android and Hilt

Scoping object A to object B means that object B always holds the same instance of A throughout its lifetime. 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.

[[339194]]

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

  • Component Hierarchy https://developer.android.google.cn/training/dependency-injection/hilt-android#component-hierarchy

Note: By default, bindings in Hilt are unscoped. They do not belong to any component and are accessible throughout your project. A different instance of the type is provided each time it is requested. When you scope a binding to a component, it limits where you can use the binding and what dependencies the type can have.

In Android, you can manually scope without using a DI library using the Android Framework. Let's look at how to do manual scoping and how to use Hilt instead. Finally, we'll compare manual scoping with the Android Framework and scoping with Hilt.

Scoping in Android

After reading the above definition, you may have such an objection: using an instance variable of a type in a specific class can also limit the scope of the variable type. That's right! Without using DI, you can do the following:

  1. class ExampleActivity : AppCompatActivity() {
  2. private val analyticsAdapter = AnalyticsAdapter()
  3. ...
  4. }

The analyticsAdapter variable is scoped to the lifecycle of MyActivity, which means that as long as the Activity is not destroyed, the variable will be the same instance. If another class needs to access this scoped variable for some reason, it will get the same instance every time it accesses it. When a new MyActivity instance is created (such as when a system setting changes), a new AnalyticsAdapter instance will be created.

Using Hilt, the equivalent code is as follows:

  1. @ActivityScoped
  2. class AnalyticsAdapter @Inject constructor() { ... }
  3.  
  4. @AndroidEntryPoint
  5. class ExampleActivity : AppCompatActivity() {
  6.  
  7. @Inject lateinit var analyticsAdapter: AnalyticsAdapter
  8.  
  9. }

Each creation of MyActivity will hold a new instance of the ActivityComponent DI container, which will provide the same AnalyticsAdapter instance to dependencies down the component hierarchy until the Activity is destroyed.

  • Component Hierarchy https://developer.android.google.cn/training/dependency-injection/hilt-android#component-hierarchy


After changing the system settings, you will get a new AnalyticsAdapter and MainActivity instance

Scoping via ViewModel

However, we may want our AnalyticsAdapter to persist after system settings change! Or, we may want to keep the instance scoped to the Activity until the user leaves the Activity.

To do this, you can use a ViewModel from the component architecture because it can persist after system settings change.

  • ViewModel in component architecture https://developer.android.google.cn/topic/libraries/architecture/viewmodel

Without dependency injection, you might have code like this:

  1. class AnalyticsAdapter() { ... }
  2.  
  3. class ExampleViewModel() : ViewModel() {
  4. val analyticsAdapter = AnalyticsAdapter()
  5. }
  6.  
  7. class ExampleActivity : AppCompatActivity() {
  8.  
  9. private val viewModel: ExampleViewModel by viewModels()
  10. private val analyticsAdapter = viewModel.analyticsAdapter
  11.  
  12. }

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

Using Hilt, you can achieve the same behavior by scoping the AnalyticsAdapter to the ActivityRetainedComponent, since the ActivityRetainedComponent can also be persisted across system setting changes.

  1. @ActivityRetainedScoped
  2. class AnalyticsAdapter @Inject constructor() { ... }
  3.  
  4. @AndroidEntryPoint
  5. class ExampleActivity : AppCompatActivity() {
  6.  
  7. @Inject lateinit var analyticsAdapter: AnalyticsAdapter
  8.  
  9. }

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

If you want to keep the ViewModel for view logic while following good DI practices, you can use @ViewModelInject to provide the ViewModel's dependencies, which is described in detail in: Documentation | Injecting ViewModel objects with Hilt. This way, the AnalyticsAdapter does not need to be scoped to the ActivityRetainedComponent, because it is now manually scoped to the ViewModel:

  • Documentation | Use Hilt to inject ViewModel objects https://developer.android.google.cn/training/dependency-injection/hilt-jetpack#viewmodels
  1. class AnalyticsAdapter @Inject constructor() { ... }
  2.  
  3. class ExampleViewModel @ViewModelInject constructor(
  4. private val analyticsAdapter: AnalyticsAdapter
  5. ) : ViewModel() { ... }
  6.  
  7. @AndroidEntryPoint
  8. class ExampleActivity : AppCompatActivity() {
  9.  
  10. private val viewModel: ExampleViewModel by viewModels()
  11. private val analyticsAdapter = viewModel.analyticsAdapter
  12.  
  13. }

What we have just seen can be applied to any Hilt component managed by the Android Framework lifecycle classes. Click to view all available scopes. Going back to our original example, limiting the scope to ApplicationComponent is equivalent to holding the instance in the Application class when not using a DI framework.

  • All available scopes https://developer.android.google.cn/training/dependency-injection/hilt-android#component-scopes

Comparison of Hilt and ViewModel scope

The advantage of using Hilt scopes is that you can use the scoped types in the Hilt component hierarchy; with ViewModel, you must manually access the scoped types through the ViewModel.

The advantage of using ViewModel scoping is 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 the NavGraph.

  • LifecyclerOwner https://developer.android.google.cn/reference/androidx/lifecycle/LifecycleOwner
  • Jetpack Navigation library https://developer.android.google.com/guide/navigation/navigation-getting-started
  • NavGraphhttps://developer.android.google.cn/reference/androidx/navigation/fragment/NavHostFragment

Hilt provides a limited number of scopes. There may not be a scope that fits your specific use case. For example, nested fragments, for this case, you can fall back to using ViewModel to limit the scope.

Using Hilt to inject ViewModel

As mentioned above, you can use @ViewModelInject to inject dependencies into your ViewModel. The reason for this is that these bindings are stored in the ActivityRetainedComponent, which is why you can only inject unscoped types, or types scoped to ActivityRetainedComponent and ApplicationComponent.

If the Activity or Fragment is modified by the @AndroidEntryPoint annotation, you can get the ViewModel factory generated by Hilt through the getDefaultViewModelProviderFactory() method. Since you can use these ViewModel factories in ViewModelProvider, the way you get ViewModel becomes more flexible. For example: limit the scope to the ViewModel of BackStackEntry.

Scoping has some cost, because the provided object will remain in memory until the holder is destroyed. Carefully consider using scoped objects in your application. Scoping is appropriate if the object's internal state requires the use of the same instance, the object needs to be synchronized, or the object is expensive to create.

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

<<:  Package visibility in Android 11

>>:  Is the 5G replacement trend over? You may have misunderstood!

Recommend

What are the essential factors for Tik Tok promotion?

From Haidilao to Daancha, Douyin has demonstrated...

The 9th Aiti Tribe Technical Clinic

【51CTO.com original article】 [51CTO original arti...

5.5-inch iPhone 6 is not available even if you have money

iPhone 6 and iPhone 6 Plus will soon be launched ...

The Milky Way has a mysterious sister? It can be seen with the naked eye!

Author | Feng Ziyang Review | Dong Chenhui Editor...