Skip to main content

First look on Hilt


onCreate

A new Dependency Injection library called Hilt was presented from the Google team. It was designed on top of Dagger library and provides a simpler, less boilerplate API to handle dependencies in an Android application. As first try, it was a real game changer. Therefore, we will make a short introduction to it, and then discuss about some opinions.

Why was it build?

First of all, Dagger was a little hard to start with, especially for beginners. Second, with the deprecation of Dagger-Android, there was a new library needed to solve DI in Android. It is also less boilerplate than Dagger and makes testing simpler.

The problem with Dagger - Android

Long story short, Android is hard, and @ContributesAndroidInjector made things harder. That, in my own humble opinion was a strong reason, for Dagger-Android to be abandoned. Forgetting to add a dependency there and having a runtime crash as well as trying to fix build issues, brought a lot of headache to those who tried using it.

What changes the game with Hilt?

If I would try to phrase it, I would say that it treats Android classes like they deserve to be treated, as normal classes. While in Dagger-Android, Activities, Fragments, WorkManager were classes, but also mysterious objects, which were very scary to work with.

Note: this post assumes the reader knows Dagger

Quick start on Hilt

Let's just start with modules. Since it is built on top of Dagger, there are some things which remain. Let's suppose we have this example:

@Module
object MyAppScopeDependenciesModule{
  @Provides
  @Singleton
  fun provideDependency1() : Dep1 = Dep1.builder().build()
 
  @Provides
  @Singleton
  fun provideDependency2() : Dep2 = Dep2.builder().build()
}

And let's create a component just for the sake of the example (App level scope):

@Singleton
@Component(modules = [MyAppScopeDependenciesModule::class])
interface MyApplicationComponent{
  val dependency1: Dep1
  val dependency2: Dep2
 
  @Component.Factory
  interface Factory{
   fun create(application: Application): MyApplicationComponent
  }
}

And let's hit the Build button and then let's start importing dependencies:

class MyApplication : Application(){
 
  @Inject lateinit var dep1: Dep1
 
  override fun onCreate(){
    super.onCreate()
 
    DaggerMyApplicationComponent.factory().create(this)
  }
}

The relation between scopes and modules was always mysterious by them who never cared to check what the Dagger's annotation processor generated. Therefore, I must say that this was pretty well spotted by those who built Hilt. In Dagger, scoped modules were connected with scoped components by a stand-alone annotation, which provided nearly 0 information if these two (or more modules) were related.

Now, let's try to build the above example with hilt:

@Module
@InstallIn(ApplicationComponent::class)
object MyAppScopeDependenciesModule{
  @Provides
  fun provideDependency1() : Dep1 = Dep1.builder().build()
 
  @Provides
  fun provideDependency2() : Dep2 = Dep2.builder().build()
}

Before you hit Build button, that's all you need with Hilt.

Where is my component?

Hilt provides the component for you. No need there for creating a component or a scope. Think of components and scopes as they were merged together. And actually, this is why Hilt is a game changer. Here is the component hierarchy needed to be used in Android apps, coming from dagger.hilt.android.components.*. Basically, you know your dependencies life length, and now you know where to install it. One last step, let's perform Dependency Injection:

@HiltAndroidApp
class MyApplication : Application(){
 
  @Inject lateinit var dep1: Dep1
 
  override fun onCreate(){
    super.onCreate()
    ...
  }
}

Also, if you want to perform DI in Activities, Fragments, Views, Services or Broadcast receivers, there is no need anymore for AndroidInjection.inject(this). Instead just mark them with @AndroidEntryPoint at the top of the class:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // either comming from an ActivityComponent or ApplicationComponent
  @Inject lateinit var dependency: Dependency
 
  override fun onCreate() {
    super.onCreate()
    ...
  }
}

Note: The injection happens in onCreate()

Is that the best it can do?

Nope, Hilt has also finally solved the problem of ViewModels instantiate process and in general, having runtime and build time dependencies in the constructor at once. Before Hilt, I used to install AssistedInject to manage creating saveStateHandle properly and there is a full tutorial on how to do that. But let's also do something simple as a short presentation:

class MyViewModel @AssistedInject constructor(
  private val dep1: Dep1,
  @Assisted private val saveStateHandle: SaveStateHandle
){
 
  @AssistedInject.Factory
  interface Factory{
    fun create(saveStateHandle: SaveStateHandle) : MyViewModel
  }
}

And then install a module for it:

@AssistedModule
@Module(includes = [AssistedInject_ViewModelModule::class])
abstract class ViewModelModule

And then expose the ViewModel in the AppComponent:

@Component(...)
interface AppComponent{
  ...
  val vmFactory: MyViewModel.Factory
  ...
}

And after that I could have a ViewModel happily ever after in my Fragment:

inline fun  Fragment.viewModelFactory(
    crossinline provider: (SavedStateHandle) -> T
) = viewModels {
    object : AbstractSavedStateViewModelFactory(this, fragment.arguments ?: Bundle()) {
        override fun  create(key: String, modelClass: Class, handle: SavedStateHandle): T =
            provider(handle) as T
    }
}
 
class HomeFragment : Fragment() {
 
    private val myViewModel by viewModelFactory { Application.component().vmFactory.create(it) }
 
    ...
 }

Note: For more details on this solution check the link provided above.

Having to do all this steps for just a ViewModel was painful, not to mention that AssistedInject still had a lot to do (Or I could use Koin, but that is not the topic at the moment).

While with Hilt, it is pretty simple:

class EditorViewModel @ViewModelInject constructor(
    private val playgroundRepository: PlaygroundRepository,
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {}

After that, nothing else is needed. Just import the ViewModel as you normally do:

//inside Fragment
private val editorViewModel by viewModels()

Note: Please check Hilt's documentation for correct dependencies to import. The ViewModel and WorkManager solution come as separated dependencies. For more, check here

onDestroyView

Personally, I would be a very happy developer by using Hilt as a Dependency Injection tool for Android. It makes it easy to track dependencies, easy to start with and less boilerplate than Dagger.

However, one of the cons I noticed when using Hilt, was that it adds even more abstraction over your project and you either need to know a little more about code generation or better not use it by heart. Also, forgetting to perform DI as AndroidInjection.inject(this) or DaggerMComponent.builder().build().inject(this) and annotating the class with @AndroidEntryPoint is still tricky, you forget either way. But there is no problem with that since the error would be generated at build time.

Nevertheless, it looks very promising.

onDestroy

I hope I gave a short introduction to get started with Hilt and also some opinions on it. Android is not a simple framework/library to work with and having more and more configurations for every tool that you need to use is always a huge headache. Therefore, I am very glad that Google team introduced Hilt. And for all Dagger fans here, Hilt is a strong argument against all who complain about Daggers complexity.

Stavro Xhardha

Popular posts from this blog

What I learned from Kotlin Flow API

I used to check the docs and just read a lot about flows but didn't implement anything until yesterday. However, the API tasted really cool (even though some operations are still in Experimental state).Prerequisites: If you don't know RxJava it's fine. But a RxJava recognizer would read this faster.Cold vs Hot streamsWell, I really struggled with this concept because it is a little bit tricky. The main difference between cold and hot happened to be pretty simple: Hot streams produce when you don't care while in cold streams, if you don't collect() (or RxJava-s equivalent subscribe()) the stream won't be activated at all. So, Flows are what we call cold streams. Removing the subscriber will not produce data at all, making the Flows one of the most sophisticated asynchronous stream API ever (in the JVM world). I tried to make a illustration of hot and cold streams: Since I mentioned the word asynchronous this implies that they do support coroutines also. Flows vs…

Modularizing your Android app, breaking the monolith (Part 1)

Inspired by a Martin Fowlers post about Micro Frontends, I decided to break my monolithic app into a modular app. I tried to read a little more about breaking monolithic apps in Android, and as far as I got, I felt confident to share my experience with you. This will be some series of blog posts where we actually try to break a simple app into a modularized Android app.

Note: You should know that I am no expert in this, so if there are false statements or mistakes please feel free to criticize, for the sake of a better development. 

What do you benefit from this approach:
Well, people are moving pretty fast nowadays and delivery is required faster and faster. So, in order to achieve this, modularising Android apps is really necessary.You can share features across different apps. Independent teams and less problems per each.Conditional features update.Quicker debugging and fixing.A feature delay doesn't delay the whole app. As per writing tests, there is not too much difference about…

From Gson to Moshi, what I learned

There is no doubt that people are getting away from GSON and I agree with those reasons too. The only advantage GSON has over other parsing libraries is that it takes a really short amount of time to set up. Furthermore, the most important thing is that Moshi is embracing Kotlin support.

First let's implement the dependency:
implementation("com.squareup.moshi:moshi:1.8.0") It's not a struggle to migrate to Moshi. It's really Gson look-a-like. The only thing to do is annotate the object with @field:Json instead of @SerializedName (which is Gsons way for JS representation):

data class User( //GSON way @SerializedName("name") val name: String, @SerializedName("user_name") val userName: String, @SerializedName("last_name") val lastName: String, @SerializedName("email") val email: String ) data class User( //Moshi way @field:Json(name = "name") val name: String, @field:Json(name = "user_name…