Dagger Hilt: Basics, Architecture, Concerns

栏目: IT技术 · 发布时间: 4年前

内容简介:Google has just released the first alpha version of Dagger Hilt.Dagger Hilt is a library which constitutes a thin wrapper around Dagger 2 dependency injection framework. The goals of this library is to simplify and standardize integration of Dagger into An

Google has just released the first alpha version of Dagger Hilt.

Dagger Hilt is a library which constitutes a thin wrapper around Dagger 2 dependency injection framework. The goals of this library is to simplify and standardize integration of Dagger into Android projects, and to reduce the amount of “boilerplate” involved.

In this article, I’ll show you how to use Hilt and share my opinion about this new addition to Android development ecosystem.

Dagger Hilt is in Early Alpha

It’s important to keep in mind that, as of this writing, Dagger Hilt in alpha. In my opinion, it would be irresponsible to use this library in professional projects at this stage. Unless, of course, you don’t mind doing a bit of QA.

I write this post out of personal curiosity and as a service to my students and readers. Please don’t interpret it as an endorsement or a call to action.

Dagger Hilt Mode of Operation

Just like vanilla Dagger, Hilt uses annotation processing and code generation to do its magic. In essence, it’s just one more annotation processor in your app which realizes additional conventions. However, it also supports reflection in tests. This reduces the overhead incurred when you run your test suite and allows for runtime adjustments of objects graph.

There is also one aspect of Hilt operation that is based on Gradle plugin. It’s not mandatory, but Google recommends using it.

Dagger Hilt Fundamental Assumption

The fundamental assumption in Dagger Hilt is that dependencies are provided from multiple Component classes which are associated with specific Android constructs. These Component classes form this hierarchy:

Dagger Hilt: Basics, Architecture, Concerns

The association of a specific class of Component with a specific Android construct means that a new instance of a Component will be created for each instance of Android construct. For example, each Activity will have a dedicated instance of ActivityComponent, each Fragment will have FragmentComponent, etc. Naturally, since there is just one instance of Application, there will be just one instance of the associated ApplicationComponent. Therefore, ApplicationComponent is “global”.

It’s important to note that instances of specific Components will originate from the same Component classes. Therefore, all Android constructs of a specific type will have access to the same set of dependencies. For example, if ActivityA needs DependencyX and you provide it from ActivityComponent, any other Activity in the app will also get access to DependencyX (if needed).

The tree-shaped hierarchical structure of Hilt’s Components is required to ensure that dependencies provided in higher-level Components will be available in lower-level Components as well. For example, going back to the example of DependencyX provided from ActivityComponent, FragmentComponent and ViewComponent will “inherit” this dependency from ActivityComponent. Therefore, you’ll automatically get access to DependencyX in your Fragments and Views (if needed).

Comparison to Dagger-Android

Readers who took my Dagger 2 or Android Architecture courses might be a bit confused now: “What’s all the fuss about? Isn’t the hierarchy of Hilt Components just natural?”.

Well, for me and you Hilt’s hierarchy of components might feel natural. I usually end up with ApplicationComponent, ActivityComponent, ControllerComponent and, if needed, ServiceComponent. That’s also the approach I taught in my courses. Hilt’s hierarchy is just an expanded version of this same structure.

However, Hilt is a 180 degree turn on dagger-android ‘s approach, which basically advocated for dedicated Component and Module classes for each Activity and Fragment in your app. I always said that this idea was the biggest fundamental flaw in dagger-android which made it very poor choice for DI ( like in this notoriously heated argument with Jake ). Happy to see Google finally promoting better practices.

I’m sure that the above two paragraphs read a bit self-congratulatory. Well, not every day I get a confirmation that my recommendations spared the devs who followed them long detours into non-optimal architecture and tens, or even hundreds of thousands of man-hours of cumulative effort.

Injecting Into Application

Since ApplicationComponent sits at the root of Hilt’s hierarchy, it’s mandatory. Therefore, if you want to use Hilt, then, after you set up all the required Gradle dependencies, you annotate your custom Application class (which you must have if you want to use Hilt) with @HiltAndroidApp annotation:

@HiltAndroidApp
public class MyApplication {
    …
}

Then, if you want to inject anything into your custom application, you’ll need to provide that dependency.

Hilt reuses Dagger’s Modules, but inverts the direction of (compile-time) dependencies between Components and Modules. In Hilt, instead of specifying which Modules a Component will use, you specify in each Module in which Components it’ll be installed:

@Module
@InstallIn(ApplicationComponent.class)
public class ApplicationModule {

    @Provides
    Logger logger() {
        return new Logger();
    }
    
}

Once you install a Module into ApplicationComponent, you’ll be able to inject dependencies provided from that Module:

@HiltAndroidApp
public class MyApplication {
    @Inject Logger mLogger;
    ...
}

The injection itself implicitly happens during the invocation of superclass’ onCreate() method. Therefore, if you override it, don’t forget to call through to super.onCreate() :

@HiltAndroidApp
public class MyApplication {

    @Inject Logger mLogger;
    
    @Override
    public void onCreate() {
        super.onCreate();
	...
    }

    ...
}

Injecting Into Activities

When you need to inject dependencies into Activities, you annotate that Activity with @AndroidEntryPoint annotation:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject Logger mLogger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }
    
    ...
}

Just like with Application, don’t forget to call through to super.onCreate() .

Where will an instance of Logger come from? Well, remember I wrote that Hilt’s Components are organized as an hierarchy? Since ActivityComponent is a descendant of ApplicationComponent, it “inherits” the ability to inject Logger .

If you want to add Activity-specific dependencies to objects graph, you’ll need to install additional modules into ActivityComponent:

@Module
@InstallIn(ActivityComponent.class)
public class ActivityModule {

    @Provides
    FragmentManager fragmentManager(Activity activity) {
        return ((AppCompatActivity) activity).getSupportFragmentManager();
    }

    @Provides
    DialogsNavigator dialogsNavigator(FragmentManager fragmentManager) {
        return new DialogsNavigator(fragmentManager);
    }

}

Then you’ll be able to inject DialogsNavigator in your Activity:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject Logger mLogger;
    @Inject DialogsNavigator mDialogsNavigator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    ...
}

You might’ve noticed that ActivityModule uses Activity, but I don’t provide it anywhere myself. Turns out Hilt automatically provides a small set of implicit dependencies in specific components. Activity in ActivityComponent is one of them. You can find the full list of these dependencies here .

Dagger Hilt Scopes

So far you saw how Dagger Hilt reuses Dagger’s Components and Modules. It establishes a strict convention, which is often referred to as being “opinionated”. Similarly, Hilt reuses Scopes.

Let’s say that DialogsNavigator is a stateful object which must be shared among all the clients within the scope of a single Activity. For example, imagine single-Activity app with many Fragments, all of which must use the same instance of DialogsNavigator.

To implement this requirement in Hilt, you need to annotate that dependency provided form ActivityModule with @ActivityScoped annotation:

@Module
@InstallIn(ActivityComponent.class)
public class ActivityModule {

    @Provides
    FragmentManager fragmentManager(Activity activity) {
        return ((AppCompatActivity) activity).getSupportFragmentManager();
    }

    @Provides
    @ActivityScoped
    DialogsNavigator dialogsNavigator(FragmentManager fragmentManager) {
        return new DialogsNavigator(fragmentManager);
    }

}

Then in each individual Fragment shown in that Activity you do:

@AndroidEntryPoint
public class MyFragment extends Fragment {

    @Inject DialogsNavigator mDialogsNavigator;

}

It’s guaranteed now that the Activity and all the Fragments within the scope of that Activity will get a reference to the same instance of DialogsNavigator.

However, if you change to a different Activity, different instance will be created for it and its own Fragments.

Advanced Dagger Hilt

So far, I covered the basics of Hilt. It should get you started with Hilt and its official documentation, but it wasn’t an exhaustive list of Hilt’s features. Stuff that I didn’t cover: ViewModel injection, custom components, custom entry points, scope aliases, optional injection, extensions, testing, and, probably, some more.

As far as I can tell, many apps won’t even need to deal with the above “advanced” concepts because the basics seem to provide sane defaults. In addition, the official documentation is surprisingly good, so if you’ll be evaluating Hilt for real production use, just go over the docs first.

Automated Testing with Dagger Hilt

One very interesting aspect of Hilt is its support for automated testing.

Let’s be clear about one thing right away: if you write unit tests for your code, these tests shouldn’t know anything about dependency injection frameworks. Hilt’s testing features should be restricted to integration, end-to-end and UI tests (whether you use real device, emulator, or Robolectric).

Automated testing has always been Dagger’s weak spot because it makes it very hard to replace dependencies with test-doubles in tests. This is an inherent limitation stemming from Dagger being an annotation processor and a code generator. To mitigate this limitation, Hilt makes use of reflection in tests. This is an interesting approach, but, still, looks like you can’t just replace individual dependencies and need to nuke out entire modules.

It’s definitely too early to assess Hilt’s utility in automated testing, but the fact that this aspect got a bit of attention and love is encouraging.

Migration to Dagger Hilt

To get my hands dirty, I migrated my “experiments app”, IDoCare , to Dagger Hilt.

As I wrote above, since the fundamental assumptions of Dagger Hilt are aligned with what I’ve been doing for years, it took less than two hours. Therefore, if you’ve been following my recommendations, your app should be ready for Dagger Hilt and you just need to wait for it to reach maturity.

You can review the differences by comparing repo’s master branch with dagger-hilt branch.

Note: IDoCare is not a “clean code” in my opinion. That’s my first real app and I use it today to test ideas and tools, so don’t copy stuff from there blindly.

Dagger Hilt Best Practices

I know it looks a “bit” odd when someone shares best practices for a library that was released two days ago. Literally. However, as I mentioned above, Hilt basically follows the same structure of DI that I used for years and teach in my courses. Therefore, even though the lib itself is new, the underlying architectural approach is not and I have a lot of experience with it. Therefore, even though there is a real possibility that I can be wrong, I feel that my advice can spare much time to others, so here we go.

First, out of all Hilt’s Components, the “core” components are ApplicationComponent, ActivityComponent and ServiceComponent. In some cases, like when you use nested Fragments in some clever way, you might need FragmentComponent as well, but I’d try to avoid that if possible.

Unfortunately, looks like you’ll also need to use ActivityRetainedComponent if you use ViewModels. As far as I understand the docs, this will happen implicitly if you annotate ViewModels’ constructors with @ViewModelInject annotation. That’s yet another unneeded complication brought to you by ViewModel TM. It looks like ActivityRetainedComponent is equivalent to our “old friend”, retained Fragment, in disguise. We quit using it long ago and then Google deprecated the concept itself (IIRC), but now it makes a comeback. I can only recommend being very careful with Hilt if you use ViewModels.

Don’t use ViewComponent and ViewWithFragmentComponent at all, unless you migrate messed-up legacy project to Hilt.

In all the above examples, I explicitly provided dependencies inside Modules using @Provides annotation. You don’t have to do that and using @Inject annotated constructors is also a valid implementation choice. However, for scoped dependencies, I recommend using this explicit approach exclusively. There is real maintenance benefit in being able to open a couple of Modules and get a full picture of scoped dependencies in the app.

Somewhere I read a recommendation to use @Reusable annotation. The best thing you can do with it is to forget about its existence.

There is also a section titled “extensions” in Hilt docs which seem to suggest that Google envisions third-party libraries that will depend on Hilt and integrate into apps. Sounds like a very bad idea to me. Don’t make your libs depend on any DI framework.

And, most importantly, remember that Dependency Injection is a complex architectural pattern that has tremendous effect on the long-term maintainability of your project. DI frameworks provide a template and some conventions, but the quality of your DI doesn’t automatically increase if you use a framework. Therefore, learn the fundamentals and keep the big picture in mind.

My Concerns About Dagger Hilt

Google’s PR and many other resources will assure you that Dagger Hilt will make DI so much simpler and eliminate the boilerplate and everything will be great. I will tell you the truth: there are many potential pitfalls and it’s not even evident at this point that Dagger Hilt justifies its existence.

First of all, I don’t understand Google’s obsession with “boilerplate”. It looks like that’s the main criteria by which they evaluate architectural decisions. Dagger Hilt is not an exception and the current docs state “Reduced boilerplate” as Hilt’s very first benefit:

Dagger Hilt: Basics, Architecture, Concerns

Here is an interesting question: in your estimation, how many lines of code did I eliminate by migrating IDoCare to Dagger Hilt? The answer is that the diff between branches stands at 119 lines of code. However, since I refactored and simplified quite a bit of logic during the migration (e.g. removed entire unneeded ServerSyncComponent, removed unneeded scope annotations, etc.), the actual apples-to-apples diff would probably be around 50 lines of code.

Granted, IDoCare isn’t big project (14 KLOC), but the amount of spared “boilerplate” wouldn’t be much higher even if IDoCare would be ten times in size because most of this code was centralized and reused. This kind of duplication elimination is something that I put special emphasis on in my code. It might look as an overkill on the first sight, but then, at some point, I need to migrate to a different approach and realize that five minutes I invested two years ago spare me hours of work today.

The migration of Google IO (which used dagger-android ) to Hilt resulted in removal of ~600 LOC (that’s 44 KLOC application). Not something to be extasic about either, especially given how verbose and needlessly complicated dagger-android is to begin with.

So, I spent three hours reading docs and another two hours migrating the app to Hilt. Even if Hilt won’t have any downsides (very improbable) and I’ll use it in the next 10 projects, I still won’t get positive ROI from it. This time is basically wasted and Hilt for me, as a developer, will probably remain net negative forever. Don’t worry, that’s not the only criteria, so it’s not that bad. I just want you to understand how nonsensical and amateurish “boilerplate” argument is in this context.

All in all, while Google is finally on track to some sane architectural pattern, they can’t get over their “boilerplate” fetish. In this context, googlers are like miners who found gold in their shaft, but can’t notice it due to their obsession with coal.

The second concern I have about Hilt is increased build times.

Dagger is notoriously bad in context of build times, especially if you use Kotlin and Kapt. I know projects that use Kotlin and Dagger where build times are one of the worst productivity killers. Lyft folks, for example, invested into some kind of black magic to use Dagger without Kapt. It’s a real mess.

It seems to me that Dagger Hilt adds additional layer of annotation processing and code generation on top of standard Dagger. If this addition will increase the build times in a noticeable way, integration of Dagger Hilt would be very questionable move. This article is already too long and I don’t have any time left, but I’d recommend benchmarking IDoCare and IOSched to see the impact of Dagger Hilt on build times (but even then remember that these are small apps).

In addition, Dagger Hilt uses Gradle plugin for a bit or additional black magic. As far as I understand, you can get rid of it, but the examples in docs assume that it’s being used. I didn’t look into this aspect much, but it looks like this plugin is used to spare another couple of lines of code. If that’s the case, how many additional “invalidate caches and restart” would you tolerate due to this plugin instead of writing that code yourself?

My last concern about Dagger Hilt is whether it’s too little, too late. I get a sense that developers start being tired of Dagger and Google’s games. In new Kotlin-only projects, Koin seems to become popular and the overall feedback about it is very positive. Even if Hilt won’t have any disadvantages (again, improbable), maybe it’s not the best investment of our time going forward if the ecosystem is already Kotlin first? However, that’s just my concern and only time will tell whether it’s valid.

Conclusion

I started this post with the intention to share a quick opinion on Dagger Hilt. I failed miserably, once again. On the other hand, I do think that the info in this article can help many developers, so I hope you’ll find it useful.

The good news are that Google finally abandoned the idea behind dagger-android which is a big step forward. I’m sure that someone there had to fight a good fight for this to happen, so hats off. The defaults of Dagger Hilt seem reasonable to me. The documentation is much better than it used to be and I understood what this lib does without much back and forth. Migration guides are also very clear and actually helpful (even though I didn’t need them). New APIs are pretty straightforward to use. ViewModel injection also seems to become easier, but it’s largely irrelevant for me as a developer (but not as an instructor, unfortunately).

I can’t say anything definitive about automated testing story yet.

On the negative side we have a real concern about Hilt’s effect on build times. I’d say that’s the main question to Google right now. Docs still obsessed with “boilerplate”, even though it’s the most irrelevant metric in this context. Not sure why Gradle Plugin is required. Also a bit too many Components and scopes (though I understand that they probably wanted to cover all bases).

In one of my previous posts , which was my triumphant eulogy over dagger-android ‘s coffin, I explained why I’m skeptical about Google’s effort to “improve Dagger” and promised to extend them my apology if they prove my skepticism wrong. After writing this long article and taking into account all my concerns and criticism, I think that, overall, they did a good job and Hilt has a real chance to become that standard of DI that Android needed so much in the past 11 years. It has a long way to go, and it’s not evident that it’ll reach that point, but I still want to congratulate googlers who worked on Hilt. It’s a fair and decent attempt to standardize Dagger integration into Android projects. Googlers, if you read this, I apologize for not believing in you.

Said all that, for some reason, Google already recommends Dagger Hilt as DI solution for Android, two days after the release of the first alpha version. I find it odd and I won’t use Dagger Hilt in professional project any time soon. Especially given the fact it doesn’t provide me any immediate benefits.

As usual, thanks for reading and leave your comments and questions below.

If you liked this post, then you'll surely like my courses


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript编程精解

JavaScript编程精解

Marijn Haverbeke / 徐涛 / 机械工业出版社华章公司 / 2012-10-1 / 49.00元

如果你只想阅读一本关于JavaScript的图书,那么本书应该是你的首选。本书由世界级JavaScript程序员撰写,JavaScript之父和多位JavaScript专家鼎力推荐。本书适合作为系统学习JavaScript的参考书,它在写作思路上几乎与现有的所有同类书都不同,打破常规,将编程原理与运用规则完美地结合在一起,而且将所有知识点与一个又一个经典的编程故事融合在一起,读者可以在轻松的游戏式......一起来看看 《JavaScript编程精解》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具