Hilt is the new library built on top of Dagger that simplifies Dependency Injection (DI) in Android apps. But, how much does it simplify it? We migrated the Google I/O app (iosched) to find out, which already used Dagger with dagger.android.
Hilt是建立在Dagger之上的新库,可简化Android应用程序中的依赖注入(DI)。 但是,它简化了多少呢? 我们迁移了Google I / O应用程序( iosched )以进行查找,该应用程序已将 Dagger与dagger.android一起使用 。
In this article I’ll go through our experience migrating this particular app. For proper and comprehensive instructions, check out the Hilt Migration Guide.
在本文中,我将介绍我们迁移此特定应用程序的经验。 有关正确和全面的说明,请参阅《 迁移指南》 。
-2000,+ 500 (-2000, +500)
We replaced 2000 lines of DI code with just 500. This is not the only success metric, but it’s promising!
我们仅用500行替换了2000行DI代码 。 这不是唯一的成功指标,但很有希望!
How is this reduction possible? We were using dagger.android which also promised some boilerplate reduction in Android. The difference is that Hilt is much more opinionated. It already implements some concepts that work well with Android apps.
如何减少? 我们使用的是dagger.android,它也承诺会在Android中减少样板。 不同之处在于,Hilt更自以为是 。 它已经实现了一些可与Android应用完美配合的概念。
For example, you don’t need to define an AppComponent. Hilt comes with a bunch of predefined components, including the ApplicationComponent
, ActivityComponent
, or FragmentComponent
. You can still create your own of course, Hilt is just a wrapper on top of Dagger.
例如,您不需要定义AppComponent。 Hilt附带了一堆预定义的组件 ,包括ApplicationComponent
, ActivityComponent
或FragmentComponent
。 当然,您仍然可以创建自己的应用,Hilt只是Dagger之上的包装。
Let’s dig into the details:
让我们深入研究细节:
Android组件和作用域 (Android components and scoping)
A problem for dependency injection in Android (actually, a general annoyance) is that components, like Activities, are created by the framework. So, in order to inject dependencies, you have to somehow do it after creation. dagger.android
simplified this process by letting you call AndroidInjection.inject
(this) which we did by extending DaggerAppCompatActivity
or DaggerFragment
. Apart from this we had a module (ActivityBindingModule
) that would define which subcomponents dagger.android should create, their scope and all modules included in them using @ContributesAndroidInjector
for both Activity and Fragments:
Android中依赖项注入的一个问题(实际上是一个普遍的烦恼)是由框架创建的组件(例如Activity)。 因此,为了注入依赖关系,您必须在创建后以某种方式进行处理。 dagger.android
通过扩展DaggerAppCompatActivity
或DaggerFragment
调用AndroidInjection.inject
(此),从而简化了此过程。 除此之外,我们还有一个模块( ActivityBindingModule
),该模块将使用@ContributesAndroidInjector
对Activity和Fragments定义dagger.android应该创建哪些子组件,它们的范围以及其中包括的所有模块:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With dagger.android
@ActivityScoped
@ContributesAndroidInjector(
modules = [
OnboardingModule::class,
SignInDialogModule::class
]
)
internal abstract fun onboardingActivity(): OnboardingActivity
Additionally, each fragment had its own subcomponent, also generated with @ContributesAndroidInjector
in their own module:
此外,每个片段都有自己的子组件,这些子组件也由@ContributesAndroidInjector
在其各自的模块中生成:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With dagger.android
@FragmentScoped
@ContributesAndroidInjector
internal abstract fun contributeOnboardingFragment(): OnboardingFragment
@FragmentScoped
@ContributesAndroidInjector
internal abstract fun contributeWelcomePreConferenceFragment(): WelcomePreConferenceFragment
If you look at different dagger.android projects out there, they all have similar boilerplate.
如果您查看其他的dagger.android项目,它们都有相似的样板。
With Hilt, you just have to remove the @ContributesAndroidInjector
bindings and add the @AndroidEntryPoint
annotation to all Android Components (activities, fragments, views, services, and broadcast receivers) that require injection of dependencies.
使用Hilt,您只需删除@ContributesAndroidInjector
绑定,并将@AndroidEntryPoint
批注添加到所有需要注入依赖项的Android组件(活动,片段,视图,服务和广播接收器)。
Most of the modules we had, more than 20, could be removed, as they just contained @ContributesAndroidInjector
(like OnboardingModule) and ViewModel bindings (more on this later) . Now you just need to annotate the fragments as entry points:
我们拥有的大多数模块(超过20个)可以删除,因为它们仅包含@ContributesAndroidInjector
(如OnboardingModule )和ViewModel绑定(稍后将对此进行更多介绍)。 现在,您只需将片段注释为入口点:
@AndroidEntryPoint
class OnboardingFragment : Fragment() {...
For other types of bindings, we still use modules to define them and they need to be annotated with @InstallIn
.
对于其他类型的绑定,我们仍然使用模块来定义它们,并且需要使用@InstallIn
对其进行注释。
@InstallIn(FragmentComponent::class)
@Module
internal class SessionViewPoolModule {
Scoping works as you’d expect with familiar predefined scopes like ActivityScoped
, FragmentScoped
, ServiceScoped
, etc.
使用熟悉的预定义范围(例如ActivityScoped
, FragmentScoped
, ServiceScoped
等),可以按预期使用作用域 。
@FragmentScoped
@Provides
@Named("sessionViewPool")
fun providesSessionViewPool(): RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool()
Another boilerplate remover is the predefined qualifiers, like @ApplicationContext
or @ActivityContext
which saves you from having to create the same bindings in all apps.
另一个样板删除程序是预定义的限定符,例如@ApplicationContext
或@ActivityContext
,可让您不必在所有应用程序中创建相同的绑定。
@Singleton
@Provides
fun providePreferenceStorage(
@ApplicationContext context: Context
): PreferenceStorage = SharedPreferenceStorage(context)
Android体系结构组件 (Android Architecture Components)
Where Hilt really shines is with its integration with Architecture Components. It supports injection of ViewModels and WorkManager Workers.
Hilt真正令人瞩目的地方在于其与Architecture Components的集成。 它支持注入的ViewModels和WorkManager的工人 。
Before Hilt, doing this required a deep understanding of Dagger (or good copy-paste skills, as most projects had the same setup).
在Hilt之前,执行此操作需要对Dagger有深入的了解(或具有良好的复制粘贴技能,因为大多数项目具有相同的设置)。
First, we provided a ViewModel factory for fragments and activities to obtain ViewModels via ViewModelProviders:
首先,我们为片段和活动提供了一个ViewModel工厂,以便通过ViewModelProviders获得ViewModel:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With Dagger2 or dagger.android
class SessionDetailFragment : ... {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var sessionDetailViewModel: SessionDetailViewModel
…
override fun onCreateView(...) {
// Helper method that obtains ViewModel with the injected factory
sessionDetailViewModel = viewModelProvider(viewModelFactory)
}
}
With Hilt we can obtain ViewModels in fragments with a single line:
使用Hilt,我们可以用单行获取片段中的ViewModels:
private val viewModel: AgendaViewModel by viewModels()
or, if you want to scope to the parent activity:
或者,如果您想限制父活动的范围:
private val mainActivityViewModel: MainActivityViewModel by activityViewModels()
Secondly, before Hilt, using injected dependencies inside the ViewModels required a complicated multibindings setup using a ViewModelKey
:
其次,希尔特前使用的ViewModels内注入依赖使用需要复杂的设置multibindings ViewModelKey
:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With Dagger2 or dagger.android
@Target(
AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
And in each module you would provide it like so:
在每个模块中,您将按以下方式提供它:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With Dagger2 or dagger.android
@Binds
@IntoMap
@ViewModelKey(SessionDetailViewModel::class)
abstract fun bindSessionDetailFragmentViewModel(viewModel: SessionDetailViewModel): ViewModel
With Hilt, we add the @ViewModelInject
annotation to the ViewModel’s constructor. That’s it. No need to define them in modules or add them to a magical map.
使用Hilt,我们将@ViewModelInject
批注添加到ViewModel的构造函数中。 而已。 无需在模块中定义它们或将它们添加到神奇的地图中。
class SessionDetailViewModel @ViewModelInject constructor(...) { … }
Note that this is part of Hilt and Jetpack integrations and you need to define extra dependencies to use them.
请注意,这是Hilt和Jetpack集成的一部分,您需要定义其他依赖项才能使用它们。
测试中 (Testing)
单元测试 (Unit testing)
Unit testing doesn’t change. Your architecture should allow for testing your classes independently of how you create your object graph.
单元测试不变。 您的体系结构应允许独立于创建对象图的方式来测试类。
仪器化测试-测试运行器设置 (Instrumented tests — test runner setup)
Using Instrumented tests with Hilt changes a bit with respect to Dagger. It all starts with a custom test runner that lets you define a different test application:
与Hilt一起使用仪器化测试会相对于Dagger有所变化。 这一切都始于自定义测试运行程序,可让您定义其他测试应用程序:
Before:
之前:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With Dagger2 or dagger.android
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, MainTestApplication::class.java.name, context)
}
}
之后 :
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
@CustomTestApplication(MainTestApplication::class)
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, CustomTestRunner_Application::class.java.name, context)
}
}
Instead of returning a test application with a different Dagger graph for tests in the newApplication
method, we need to return CustomTestRunner_Application
. The actual test application is defined in the @CustomTestApplication
annotation. You only need this class if there’s some important initialization to do. In our case it was AndroidThreeTen and we added Timber as well.
我们需要返回CustomTestRunner_Application
,而不是使用带有不同Dagger图的测试应用程序来返回newApplication
方法中的测试。 实际的测试应用程序在@CustomTestApplication
批注中定义。 仅当需要执行一些重要的初始化时才需要此类。 在我们的例子中是AndroidThreeTen,我们还添加了Timber。
Before, we had to tell Dagger which AndroidInjector
to use and we could extend the main application:
之前,我们必须告诉Dagger使用哪个AndroidInjector
,然后才能扩展主应用程序:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// With Dagger2 or dagger.android
class MainTestApplication : MainApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerTestAppComponent.builder().create(this)
}
}
With Hilt, the MainTestApplication
can’t extend your existing application because it’s already annotated with @HiltAndroidApp
. We need to create a new Application
and define the important initialization steps here:
使用Hilt, MainTestApplication
无法扩展您现有的应用程序,因为它已经使用@HiltAndroidApp
了注释。 我们需要创建一个新的Application
并在此处定义重要的初始化步骤:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
open class MainTestApplication : Application() {
override fun onCreate() {
// ThreeTenBP for times and dates, called before super to be available for objects
AndroidThreeTen.init(this)
Timber.plant(Timber.DebugTree())
super.onCreate()
}
}
That’s it for testing. This Application will replace the MainApplication
when running instrumented tests.
就是测试了。 运行已测试的测试时,此应用程序将替换MainApplication
。
仪器测试-测试类别 (Instrumented tests — test classes)
The actual test classes vary as well. Since we don’t have an AppComponent
(or TestAppComponent
) anymore, all modules and dependencies installed in the predefined ApplicationComponent
are going to be available at test time. Oftentimes, however, you want to replace some of those modules.
实际的测试类别也有所不同。 由于我们不再具有AppComponent
(或TestAppComponent
),因此预定义的ApplicationComponent
中安装的所有模块和依赖项都将在测试时可用。 但是,通常您想替换其中一些模块。
For example, in iosched we replace the CoroutinesModule
for a TestCoroutinesModule
that flattens execution so it’s synchronous, repeatable and consistent. This TestCoroutinesModule
is simply added to the androidTest
directory and it’s installed in the ApplicationComponent
normally:
例如,在iosched我们更换CoroutinesModule
的TestCoroutinesModule
,以展执行,以便它是同步的,可重复的和一致的。 将此TestCoroutinesModule
简单地添加到androidTest
目录中,并且通常将其安装在ApplicationComponent
:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
@InstallIn(ApplicationComponent::class)
@Module
object TestCoroutinesModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher =
AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
...
}
However, at this time we would have “duplicated bindings” errors because two modules (CoroutinesModule
and TestCoroutinesModule
) can provide the same dependencies. To solve this, we simply uninstall the production module in the test class using the @UninstallModules
annotation.
但是,由于两个模块( CoroutinesModule
和TestCoroutinesModule
)可以提供相同的依赖关系,所以此时将出现“重复绑定”错误。 为了解决这个问题,我们只需使用@UninstallModules
批注在测试类中卸载生产模块。
@HiltAndroidTest@UninstallModules(CoroutinesModule::class)@RunWith(AndroidJUnit4::class)
class AgendaTest {...
Also, we need to add the @HiltAndroidTest
annotation and the @HiltAndroidRule
JUnit rule to the test class. There’ s one thing you have to take into account though:
另外,我们需要向测试类添加@HiltAndroidTest
批注和@HiltAndroidRule
JUnit规则。 但是,您必须考虑一件事:
HiltAndroidRule命令 (HiltAndroidRule order)
One important thing to note is that the HiltAndroidRule
must be processed before the activity is launched. It’s probably a good idea to run it before any other rule.
需要注意的重要一件事是必须在启动活动之前处理HiltAndroidRule
。 在任何其他规则之前运行它可能是一个好主意。
Before JUnit 4.13 you can use RuleChain
to define the order but personally I’ve never liked the outer/inner rule concept. In 4.13 a simple order
parameter was added to the @Rule
annotations, making them much more readable:
在JUnit 4.13之前,您可以使用RuleChain
定义顺序,但是就我个人而言,我从来不喜欢外部/内部规则概念。 在4.13中,将一个简单的order
参数添加到@Rule
批注中,使它们更具可读性:
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
@HiltAndroidTest
@UninstallModules(CoroutinesModule::class)
@RunWith(AndroidJUnit4::class)
class AgendaTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
// Executes tasks in a synchronous [TaskScheduler]
@get:Rule(order = 1)
var syncTaskExecutorRule = SyncTaskExecutorRule()
// Sets the preferences so no welcome screens are shown
@get:Rule(order = 1)
var preferencesRule = SetPreferencesRule()
@get:Rule(order = 2)
var activityRule = MainActivityTestRule(R.id.navigation_agenda)
@Test
fun agenda_basicViewsDisplayed() {
// Title
onView(allOf(instanceOf(TextView::class.java), withParent(withId(R.id.toolbar))))
.check(matches(withText(R.string.agenda)))
// One of the blocks
onView(withText("Breakfast")).check(matches(isDisplayed()))
}
}
Remember that if you don’t define the order you’ll be introducing a race condition and a subtle bug that will show up, probably, at the worst time.
请记住,如果您未定义顺序,则可能会引入竞争条件和可能在最坏的时间出现的细微错误。
您应该使用Hilt吗? (Should you use Hilt?)
Like everything released in Jetpack, Hilt is a way for you to code Android applications faster, but it’s still optional. If you are confident in your Dagger skills, there’s probably no reason for you to migrate. However, if you work with a diverse team where not everyone eats multibindings for breakfast, you should consider simplifying your codebase using Hilt. Build times are similar and the dex method count is similar to dagger.android’s.
与Jetpack中发布的所有内容一样,Hilt是让您更快地编写Android应用程序的一种方法,但是它仍然是可选的。 如果您对Dagger的技能充满信心,则可能没有理由迁移。 但是,如果您与一个并非每个人都吃早餐的多重绑定的多元化团队一起工作,则应考虑使用Hilt简化代码库。 构建时间相似,而dex方法的计数与dagger.android相似。
Also, if you don’t use a DI framework, now is the time. I recommend starting with the codelab, which doesn’t assume any Dagger knowledge!
另外,如果您不使用DI框架,那么现在是时候了。 我建议从不带任何Dagger知识的codelab开始!
翻译自: https://medium.com/androiddevelopers/migrating-the-google-i-o-app-to-hilt-f3edf03affe5