[Android翻译]将谷歌I/O应用迁移到Hilt

1,086 阅读7分钟

原文地址:medium.com/androiddeve…

原文作者:medium.com/@JoseAlcerr…

发布时间: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自带了一堆预定义的组件,包括ApplicationComponentActivityComponentFragmentComponent。当然,你仍然可以创建自己的组件,Hilt只是Dagger之上的一个包装器。

让我们来深入了解一下细节:

Android组件和范围化

在Android中,依赖注入的一个问题(实际上是一个普遍的烦恼)是,像Activities这样的组件是由框架创建的。dagger.android简化了这个过程,让你调用AndroidInjection.injection(this),我们通过扩展DaggerAppCompatActivityDaggerFragment来实现。除此之外,我们还有一个模块(ActivityBindingModule),它将定义dagger.android应该创建哪些子组件,它们的范围,以及使用@ContributesAndroidInjector为Activity和Fragments创建的所有模块:

ActivityBindingModule.kt

<!-- 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生成的:

OnboardingModule.kt

<!-- 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绑定(稍后会有更多说明)。现在你只需要将这些片段注释为入口点:

OnboardingFragment.kt

@AndroidEntryPoint
class OnboardingFragment : Fragment() {...

对于其他类型的绑定,我们仍然使用模块来定义,并且需要用@InstallIn来注释。

SessionViewPoolModule.kt

@InstallIn(FragmentComponent::class)
@Module
internal class SessionViewPoolModule {

Scoping的工作原理与你所熟悉的预定义作用域如ActivityScoped、FragmentScoped、ServiceScoped等一样。

SessionViewPoolModule.kt:

    @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>)

而在每个模块中,你都会提供这样的设置:

SessionDetailModule.kt:

<!-- 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()

    ...
}

但是,此时我们会出现 "重复绑定 "的错误,因为两个模块(CoroutinesModuleTestCoroutinesModule)可以提供相同的依赖关系。为了解决这个问题,我们只要在测试类中使用@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 )(免费版)翻译