发布时间:2020年7月21日 - 5分钟阅读
Hilt是建立在Dagger之上的新库,它简化了Android应用中的依赖注入(DI)。但是,它到底简化了多少呢?我们迁移了Google I/O应用(iosched)来了解一下,它已经使用了Dagger.android的Dagger。
在本文中,我将介绍我们迁移这个特殊应用的经验。要想获得正确而全面的指导,请查看Hilt迁移指南。
-2000, +500
我们只用500行DI代码取代了2000行。这不是唯一的成功指标,但它很有希望!
这种减少是如何实现的?我们使用的是dagger.android,它也承诺在Android中减少一些锅炉板。不同的是,Hilt的意见更大。它已经实现了一些在Android应用中很好的概念。
例如,你不需要定义一个AppComponent。Hilt自带了一堆预定义的组件,包括ApplicationComponent、ActivityComponent或FragmentComponent。当然,你仍然可以创建自己的组件,Hilt只是Dagger之上的一个包装器。
让我们来深入了解一下细节:
Android组件和范围化
在Android中,依赖注入的一个问题(实际上是一个普遍的烦恼)是,像Activities这样的组件是由框架创建的。dagger.android简化了这个过程,让你调用AndroidInjection.injection(this),我们通过扩展DaggerAppCompatActivity或DaggerFragment来实现。除此之外,我们还有一个模块(ActivityBindingModule),它将定义dagger.android应该创建哪些子组件,它们的范围,以及使用@ContributesAndroidInjector为Activity和Fragments创建的所有模块:
<!-- 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
此外,每个fragment都有自己的子组件,也是在自己的模块中用@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
如果你看看外面不同的dagger.android项目,它们都有类似的锅炉模板。
在Hilt中,你只需要去掉@ContributesAndroidInjector绑定,并为所有需要注入依赖关系的Android组件(活动、片段、视图、服务和广播接收器)添加@AndroidEntryPoint注解。
我们拥有的大多数模块,超过20个,可以被删除,因为它们只是包含@ContributesAndroidInjector(像OnboardingModule)和ViewModel绑定(稍后会有更多说明)。现在你只需要将这些片段注释为入口点:
@AndroidEntryPoint
class OnboardingFragment : Fragment() {...
对于其他类型的绑定,我们仍然使用模块来定义,并且需要用@InstallIn来注释。
@InstallIn(FragmentComponent::class)
@Module
internal class SessionViewPoolModule {
Scoping的工作原理与你所熟悉的预定义作用域如ActivityScoped、FragmentScoped、ServiceScoped等一样。
@FragmentScoped
@Provides
@Named("sessionViewPool")
fun providesSessionViewPool(): RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool()
另一个模板去除器是预定义的限定符,如@ApplicationContext或@ActivityContext,它可以节省你在所有应用程序中创建相同的绑定。
@Singleton
@Provides
fun providePreferenceStorage(
@ApplicationContext context: Context
): PreferenceStorage = SharedPreferenceStorage(context)
安卓Architecture Components
Hilt真正出彩的地方在于它与Architecture Components的集成。它支持ViewModels和WorkManager Workers的注入。
在Hilt之前,要做到这一点,需要对Dagger有很深的了解(或者有很好的复制粘贴能力,因为大多数项目都有相同的设置)。
首先,我们为片段和活动提供了一个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)
}
}
通过Hilt,我们可以用一行字来获取片段中的ViewModel。
private val viewModel: AgendaViewModel by viewModels()
或者,如果你想将范围扩展到父活动。
private val mainActivityViewModel: MainActivityViewModel by activityViewModels()
其次,在Hilt之前,在ViewModels中使用注入的依赖关系需要使用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>)
而在每个模块中,你都会提供这样的设置:
<!-- 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
使用Hilt,我们在ViewModel的构造函数中添加@ViewModelInject注解。就这样了。不需要在模块中定义它们,也不需要将它们添加到一个神奇的地图中。
class SessionDetailViewModel @ViewModelInject constructor(...) { … }
请注意,这是Hilt和Jetpack集成的一部分,你需要定义额外的依赖关系才能使用它们。
测试
单元测试
单元测试不会改变。你的架构应该允许独立于创建对象图的方式来测试你的类。
仪器测试--测试运行器设置
使用 Hilt 的 Instrumented 测试与 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)
}
}
After:
<!-- 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)
}
}
在newApplication方法中,我们需要返回CustomTestRunner_Application,而不是返回一个带有不同Dagger图的测试应用程序。实际的测试应用是在@CustomTestApplication注解中定义的。只有当有一些重要的初始化工作要做时,你才需要这个类。在我们的案例中,它是AndroidThreeTen,我们也添加了Timber。
之前,我们必须告诉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)
}
}
有了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()
}
}
测试就到这里了。当运行工具测试时,这个Application将取代MainApplication。
工具化测试--测试类
实际的测试类也有所不同。由于我们没有AppComponent(或TestAppComponent)了,所有安装在预定义ApplicationComponent中的模块和依赖关系都将在测试时可用。然而,很多时候,你想要替换其中的一些模块。
例如,在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()
...
}
但是,此时我们会出现 "重复绑定 "的错误,因为两个模块(CoroutinesModule和TestCoroutinesModule)可以提供相同的依赖关系。为了解决这个问题,我们只要在测试类中使用@UninstallModules注解卸载生产模块即可。
@HiltAndroidTest
@UninstallModules(CoroutinesModule::class)
@RunWith(AndroidJUnit4::class)
class AgendaTest {...
同时,我们需要在测试类中添加@HiltAndroidTest注解和@HiltAndroidRule JUnit规则。有一件事你必须要考虑到。
HiltAndroidRule顺序
需要注意的一件重要事情是,HiltAndroidRule必须在活动启动之前处理。在任何其他规则之前运行它可能是个好主意。
在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()))
}
}
记住,如果你不定义顺序,你会引入一个竞赛条件和一个微妙的bug,可能会在最坏的时候出现。
你应该使用Hilt吗?
就像Jetpack中发布的所有东西一样,Hilt是一种让你更快地编写Android应用程序的方法,但它仍然是可选的。如果你对你的Dagger技能很有信心,你可能没有理由迁移。然而,如果你与一个多样化的团队一起工作,不是每个人都把多绑定当早餐吃,你应该考虑使用Hilt简化你的代码库。构建时间相似,dex方法的数量也与dagger.android的相似。 另外,如果你没有使用DI框架,现在是时候了。我建议从codelab开始,它不假设有任何Dagger知识!
通过( www.DeepL.com/Translator )(免费版)翻译