Recuerdas el proyecto en el que creamos con un recyclerView y consumiendo un api rest usando json-server y ngrok obtuvimos los datos para llenar los items de ese recycler? Pues ese proyecto no tenía nada de inyección de dependencias, puedes ver cómo se construyó ese proyecto este tutorial link.

¿Qué es inyección de dependencias?

Cuando hablamos de inyectar dependencias debemos primero de saber que es una instancia y una clase:

  • La clase es la estructura del objeto que tendrá por ejemplo: Hablemos del objeto perro, la clase ó la estructura que representa al objeto perro será:
    • Atributos: patas, nariz, boca, orejas, raza
    • Métodos: olfatear, ladrar
  • La instancia es cuando se crea el objeto y ¿qué es eso? pues es cuando se aparta un espacio en memoria para almacenar a los datos de la clase, eso es crear un instancia!

Generalmente creamos una instancia en java usando new ó en kotlin usando <Nombre de la clase>( ).

Pero ¿cuándo se destruyen esas instancias que se crean en memoria?, pues hay 2 motivos:

  1. Cuando el sistema necesita liberar memoria y en el caso de Android la aplicación no se usa, el propio sistema la termina y destruye las instancias relacionadas al app.
  2. El garbage collector, siempre que creamos una instancia esta se asigna a una variable:
    val api = Api(),
    cuando la instancia queda volando, es decir val api, estaba en una actividad ó fragmento que se destruyo entonces la instancia que se construyo no está relacionado a nada tangible, nosotros no podemos recuperarla Api( ) y después de un tiempo pasa el recolector de basura (garbage collector) y libera el espacio que utilizaba Api( ).

Ahora si, ¿qué es inyección de dependencias?

Una dependencia es una instancia que se necesita en otro objeto, un objeto compuesto, por ejemplo hablemos de un Auto, necesita llantas, motor, etc. Cada uno de estos podría bien ser un objeto, el conjunto de estos objetos forma Auto.

Nosotros podemos si bien esperar las dependencias es decir: llantas, motor dentro de la clase auto, podríamos recibirlas en el constructor, para que la aplicación sea más testeable y no se tenga alto acoplamiento.

data class Auto(val llantas: List<Llanta>, val motor: Motor)
class AutoTipo1(){
val llantas: List<Llanta> = listOf()
val motor: Motor = Motor()
}
view raw Autos.kt hosted with ❤ by GitHub

El data class es menos acoplado pues si en algún momento tenemos más de un tipo de llanta ó motor, podemos agregarlo desde el constructor.

En cambio la clase donde se inician llantas y motos, no se puede cambiar, tendríamos que crear otra clase AutoTipo2, AutoTipo3, etc y eso lo hace altamente acoplado y difícil de modificar, por eso la mejor opción es la primera.

Cuando hablamos de inyección de dependencias, hablamos de delegar a alguien más (un framework) que se encargue de crear, administrar y destruir las instancias que necesitamos, las dependencias.

Es decir nosotros en teoría ya no vamos a crear instancias, las va a crear el framework!

Frameworks de inyección de dependencias al rescate!

Si bien hay varios, los más conocidos son:

  1. Dagger para Android y Java/Kotlin en general
  2. Spring Framework
  3. En Angular se pueden inyectar dependencias.
  4. Java beans (En este caso usan dependency lookup, es algo parecido pero no igual).

¿Cuál es la ventaja?

Básicamente un framework de inyección de dependencias administra el ciclo de vida de la dependencia, sí es singleton, sí dura lo que dura una actividad ó fragmento, si es de un solo uso y se puede descartar. Esto nos permite una aplicación con mejor uso de la memoria para las instancias

¿De dónde sale HILT?

Google hizo suyo Dagger, pero no estaba perfectamente acoplado para Android había que hacer muchísimo para hacerlo correr, ponerlo en la app, registrar cada componente de Android en la aplicación, para el contexto otro rollo, esperar a que las clases generadas no causen conflictos, etc etc.

Hilt nace para evitarnos ese trabajo de usar Dagger en Android, tal vez este mal el concepto pero para mí es un Dagger especializado completamente en Android.

Su uso es transparente y fácil, pensado para una aplicación Android que tiene actividades, fragmentos y aplicación las cuales las crea el sistema y no el desarrollador.

Este proyecto tal como lo iniciamos no usa nada de inyección de dependencias, me tarde 5 minutos en agregar Hilt, y sin más preámbulos VAMOSS!

Comenzando…

Si no tienes el repositorio aquí esta, la rama master únicamente tiene el proyecto inicial, y la rama feature/hilt tiene los cambios que veremos a continuación. Te dejo el repo link

Si quieres ver el tutorial de que hicimos te dejo el link también Tutorial link.

Lo primero que hay que hacer es agregar dependencias las agregaremos en 2 archivos:

  1. build.gradle (root)
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

2. build.gradle (app)

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileSdkVersion 29
buildToolsVersion "30.0.1"
defaultConfig {
applicationId "com.codetecuhtli.recyclertuto"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//Libraries
//Recycler
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.recyclerview:recyclerview:1.1.0"
//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
//Network
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.8.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.8.0'
//Images
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
//MVVM Jetpack
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
//Hilt
implementation "com.google.dagger:hilt-android:2.28-alpha"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02"
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Creando nuestra clase Application

Vamos a crear una clase que extienda/herede de Application y la vamos a nombrar como queramos, pero también la vamos a anotar a nivel clase con la anotación @HiltAndroidApp

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class CodetecuhtliApp: Application()

Con esto le vamos a decir que nuestras dependencias van a manejarse desde application, porque es el componente que tiene las actividades y vive todo lo que dura la aplicación.

Module

Un módulo es la clase en donde vamos a decirle a Dagger como va a crear los componentes que vamos a inyectar y que no pueden crearse directamente en el constructor, como es el caso de retrofit, para el cuál tenemos que hacer algo como esto:

private fun createApi(): MovieApi {
//Interceptor
val interceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
//Client
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
//Retrofit
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.baseUrl("https://0e6e640e1d5a.ngrok.io/") //No es la mejor forma de hacerlo
.build()
return retrofit.create(MovieApi::class.java)
}
}
view raw MovieApi.kt hosted with ❤ by GitHub

Como puedes ver requiere cierto procesamiento y es por eso que no se puede inyectar directamente en el constructor así que vamos a crear un módulo para eso.

NetworkModule.kt se va a llamar y se debería de ver algo así

@InstallIn(ApplicationComponent::class)
@Module
class NetworkModule {
@Singleton
@Provides
fun providesMovieApi() = MovieApi.instance
}

Como puedes ver es una clase común que será anotada con @Module e @InstallIn.

@InstallIn sirve para decirle quien va a contener esta dependencia, quien la va a administrar, había comentado que antes era todo un caos configurar Dagger en este caso tendríamos que agregar esté módulo a la aplicación de manera manual pero recuerdas que en la clase Application que creaste agregaste la anotación @HiltAndroidApp? bueno InstallIn le dice que el contenedor será la clase que tenga la anotación @HiltAndroidApp es por eso que @InstallIn recibe ApplicationComponent::class.

Hablemos de @Provides, para que sirve?

Aquí le vamos a explicar a Dagger como queremos que se cree nuestra dependencia, cada vez que Dagger necesite la dependencia va a ejecutar el código de la función, en este punto la función podría recibir parámetros que también deben de ser dependencias y que deben de ser creadas con su respectiva anotación @Provides ó @Binds, en este caso vamos a referirnos a todos ellos como bindings que son las instancias que le diremos a Dagger como crearlas.

@Singleton, con está anotación le diremos a Dagger que esta dependencia solo se va a crear una vez y va a durar el tiempo que dure la aplicación. ¿Cuándo usar esta anotación? Depende de tu diseño si está clase se va a utilizar en todas partes conviene bastante, si solo la vas a utilizar una vez mejor no ponerla.

Los elementos que SI se pueden inyectar desde el constructor…

Es momento de empezar a inyectar las dependencias.

Comencemos con la dependencia de MovieApi que definimos en Module, aún no la inyectamos y la clase que la ocupa es: MovieRepository.kt

class MovieRepository @Inject constructor(private val movieApi: MovieApi) {

Ya que MovieApi está definida en el @Module simplemente hay que inyectarla con la anotación @Inyect en el constructor de la clase que la va a utilizar, esta anotación hará 2 cosas:

  1. Va a inyectar movieApi
  2. Hará de MovieRepository una dependencia que puede ser inyectada, es por eso que ahora vamos con la clase que utiliza MovieRepository y esa es nuestro ViewModel: MovieViewModel.

ViewModel

Cuando inyectamos un viewModel es distinto porque depende completamente de un Activity ó un Fragment, pero Hilt también tiene arreglado esto por nosotros usando la anotación @ViewModelInject podemos inyectar dependencias en nuestro ViewModel y así mismo hacer nuestro ViewModel inyectable.

class MovieViewModel @ViewModelInject constructor(
private val repository: MovieRepository
): ViewModel() {

Como puedes ver la anotación se usa a nivel constructor y con esto ya no será necesario crear ViewModelFactory para nuestros ViewModels con parámetros en el constrúctor.

Qué más se puede inyectar…

Falta nuestro adapter que será usado en nuestro recyclerView y también se puede inyectar haciendo lo siguiente:

Antes…

class MovieAdapter(private val context: Context,
private val onClick: (movie: Movie) -> Unit): RecyclerView.Adapter<MovieAdapter.MovieHolder>() {
var movies: MutableList<Movie> = mutableListOf()
set(value) {
field.let {
it.clear()
it.addAll(value)
}
notifyDataSetChanged()
}
view raw MovieAdapter.kt hosted with ❤ by GitHub

Con Hilt…

class MovieAdapter @Inject constructor(@ApplicationContext private val context: Context):
RecyclerView.Adapter<MovieAdapter.MovieHolder>() {
var movies: MutableList<Movie> = mutableListOf()
set(value) {
field.let {
it.clear()
it.addAll(value)
}
notifyDataSetChanged()
}
var onClick: ((movie: Movie) -> Unit)? = null
set(value) {
field = value
notifyDataSetChanged()
}
view raw MovieAdapter.kt hosted with ❤ by GitHub

Como puedes ver el mismo Dagger nos obliga a quitar las malas prácticas en este caso aunque era muy útil tuvimos que quitar el onClick, del constructor porque ese depende de lo que se quiera hacer con el elemento, así que se movió como atributo de la clase.

Lo siguiente que se modificó fue que se quiere inyectar el contexto para eso se utiliza la anotación @ApplicationContext que son bindings predefinidos en Hilt que nos proporcionan el contexto de la aplicación sin más trabajo :’).

Ahora si tenemos todo lo que necesitamos solo falta usarlo en nuestra actividad! VAMOSS!

Activity

Para los componentes de Android y los fragmentos si, también era un caos ya que son elementos que el sistema operativos decide su ciclo de vida, pero claro Hilt ya también tiene esto y perdón la emoción pero es que ahora todo es más fácil con Hilt para que un Activity, Fragment, Service, View ó BroadcastReceiver pueda inyectar dependencias basta con agregar la anotación @AndroidentryPoint en la clase como se muestra a continuación:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val movieViewModel: MovieViewModel by viewModels()
@Inject lateinit var movieAdapter: MovieAdapter
view raw MainActivity.kt hosted with ❤ by GitHub

Nota: Si haces esto en un Fragment, todas las Activities que usen el Fragment deberán ser anotadas también por @AndroidEntryPoint.

Lo siguiente que se aprecia en la imagen es que ya estamos inyectando nuestro viewModel y el adapter.

Todo lo que no sea ViewModel y se inyecte debe de ser lateinit simplemente porque se va a iniciar poco después y no lo haremos nosotros, además de que no puede ser privado.

Para los viewModels hay que usar el delagate by viewModels que ya sabe como inyectar nuestro ViewModel y ahora si, ya terminamos de agregar Hilt a nuestra aplicación sin Hilt previamente, fácil no?

Por último, me encontré con un cheatsheet de Hilt el cuál espero les sirva aquí se los dejo:

CheatSheet

Muchas gracias!