将Google I / O应用迁移到Hilt

Hilt是一个基于Dagger的库,旨在简化Android应用的依赖注入。通过将Google I/O应用(iosched)迁移到Hilt,我们发现它可以显著减少代码量,将2000行DI代码减少到500行。Hilt通过提供预定义的组件和作用域,减少了手动配置,例如自动处理Activity和Fragment的注入。此外,Hilt与Android Architecture Components的集成使得ViewModel和WorkManager Worker的注入更加方便。在测试方面,Hilt简化了测试运行器的设置,但测试类的配置仍需注意避免重复绑定错误。
摘要由CSDN通过智能技术生成

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附带了一堆预定义的组件 ,包括ApplicationComponentActivityComponentFragmentComponent 。 当然,您仍然可以创建自己的应用,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通过扩展DaggerAppCompatActivityDaggerFragment调用AndroidInjection.inject (此),从而简化了此过程。 除此之外,我们还有一个模块( ActivityBindingModule ),该模块将使用@ContributesAndroidInjector对Activity和Fragments定义dagger.android应该创建哪些子组件,它们的范围以及其中包括的所有模块:

ActivityBindingModule.kt:

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

Additionally, each fragment had its own subcomponent, also generated with @ContributesAndroidInjector in their own module:

此外,每个片段都有自己的子组件,这些子组件也由@ContributesAndroidInjector在其各自的模块中生成:

OnboardingModule.kt

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

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绑定(稍后将对此进行更多介绍)。 现在,您只需将片段注释为入口点:

OnboardingFragment.kt:

OnboardingFragment.kt

@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对其进行注释。

SessionViewPoolModule.kt:

SessionViewPoolModule.kt

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

Scoping works as you’d expect with familiar predefined scopes like ActivityScoped, FragmentScoped, ServiceScoped, etc.

使用熟悉的预定义范围(例如ActivityScopedFragmentScopedServiceScoped等),可以按预期使用作用域

SessionViewPoolModule.kt:

SessionViewPoolModule.kt

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

在每个模块中,您将按以下方式提供它:

SessionDetailModule.kt:

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

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

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

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我们更换CoroutinesModuleTestCoroutinesModule ,以展执行,以便它是同步的,可重复的和一致的。 将此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.

但是,由于两个模块( CoroutinesModuleTestCoroutinesModule )可以提供相同的依赖关系,所以此时将出现“重复绑定”错误。 为了解决这个问题,我们只需使用@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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值