Como vimos en el post anterior sobre inyección de dependencias en Android, la forma más básica o más usual de empezar a hacerla es de forma manual sin utilizar librerías o frameworks externos. Bien, en este post vamos a ahondar un poco en esta técnica y de esta forma podemos afianzar conceptos que nos servirán después para adentrarnos en la utilización de Hilt y de Dagger 2, las librerías más usadas en esto de la inyección de dependencias en Android.

Las dependencias entre clases se pueden representar gráficamente como si fuese un árbol de jerarquías. Por ejemplo en la figura siguiente vemos que las activities dependen de los viewmodels y estos a su vez de los repositorios y estos repositorios dependen en un caso de modelos que pueden ser clases de Room o puede ser una clase que hace una llamada a red con Retrofit por ejemplo.

A todo este gráfico que representan a las clases de nuestro proyecto y a sus relaciones con otras es a lo que se llama Grafo de la aplicación. La inyección de dependencias nos ayuda a realizar estas conexiones entre clases.

Aplicando inyección de dependencias manual en Android

En nuestro ejemplo vamos a seguir el flujo del gráfico anterior. Vamos a tener una Activity para hacer un login a nuestra app, y esta Activity va a tener un ViewModel que va a gestionar la lógica de la vista. Este a su vez se va conectar con un Repository que será el encargado de «hablar» con una persistencia local, UserLocalDataSource y con una clase que será capaz de realizar llamadas a red mediante una librería, Retrofit.

Nuestro caso de uso será tal que así:

  • Existirá una pantalla de login, LoginActivity donde existirá un input para el email.
  • LoginViewModel será el encargado de gestionar la validación de ese campo que una vez que esté validado enviará el dato el UserRepository.
  • UserRepository una vez obtenga el input introducido, hará una llamada a una API externa donde comprobará si el usuario existe y se puede logar en la app.
  • Si la API responde satisfactoriamente, el UserRepository guardará los datos que trae la respuesta de la API en persistencia local utilizando UserLocalDataSource y responderá un OK al ViewModel para que este le diga a la vista que navegue hasta la pantalla de Home.

Con esto tenemos bien definidas las dependencias de cada clase con todas las que se relaciona. En los ejemplos de código siguiente solo voy a poner la parte de la clase que hace referencia a como se relacionan unas con otras y cómo se haría la inyección de dependencias.

Vamos a empezar definiendo como serían los constructores del ViewModel y de los repositorios.

class UserRepository(private val localDataSource: UserLocalDataSource,
                     private val remoteDataSource: UserRemoteDataSource) {
........
}

UserRepository tiene que hablar tanto con la clase que llamará a la API como la que persiste los datos obtenidos en local.

class UserLocalDataSource {
......
}

class UserRemoteDataSource(private val loginService: LoginService) {
.......
}


interface LoginService {

    @GET("/...")
    fun login(
        @Path("email") email: String
    ): Call<String>
}

UserLocalDataSource no depende de nadie en nuestro caso por no complicar más de momento el flujo, y dentro de ella se persistirá usando por ejemplo SharedPreferences.

En el caso de UserRemoteDataSource, esta clase va aa depender de un servicio LoginService que utilizamos mediante la librería Retrofit.

class LoginViewModel(private val userRepository: UserRepository) {
.......
}

Llegamos al ViewModel, y este depende del UserRepository. Como estamos viendo todas las dependencias se definen para ser pasadas o inyectadas por el constructor.

Pasamos al penúltimo paso, la Activity.

class LoginActivity : AppCompatActivity() {


    private lateinit var loginViewModel: LoginViewModel

   ........

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)

       ..............
    }

Bien, en la Activity tenemos que tener una instancia del ViewModel ya que desde aquí es desde donde se inicia el flujo de nuestro caso de uso. En nuestro caso utilizamos una variable de inicialización tardía básicamente para decirle al compilador que esa variable se inicializará seguro antes de utilizarse pero no en el momento de declararla.

Como vemos en el código, hemos creado algo que se llama AppContainer y que dentro de él es donde reside nuestra instancia del ViewModel. Este AppContainer es una clase donde tenemos descritas e implementadas todas las dependencias que hemos definido en nuestras clases y que en nuestro caso actúa como inyector de dependencias.

class AppContainer {

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://example.com")
        .build()
        .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    
    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

Este AppContainer lo que hace es justamente instanciar aquellas clases de las cuales dependen otras y pasárselas en el momento de su creación. Unificamos en un único punto todo el flujo de instanciación e inyección de dependencias de nuestro caso de uso.

El AppContainer cómo es el punto inicial de creación de las referencias de todas nuestras clases debe de crearse en el punto donde todas las activities tengan acceso a este, en el caso de una aplicación Android esto sería dentro de la clase Application. Clase que el sistema operativo llama y mantiene en memoria durante todo lo que dure el proceso de nuestra aplicación.

class MyApplication: Application() {
    val appContainer = AppContainer()
}

Ventajas e Inconvenientes de la inyección de dependencias manual

De la forma vista anteriormente tendríamos hecha perfectamente una inyección de dependencias en Android con separación de responsabilidades, donde una clase, AppContainer, se encarga de crear las referencias e inyectarlas y donde el resto de clases no se tiene que preocupar por saber como ni cuando hay que crear estas referencias y dependencias.

Esto que en un principio es válido lo cierto es que se complica bastante cuando tenemos más casos de usos, cuando queremos compartir ciertas instancias de clases entre diferentes casos de usos o cuando queremos que ciertas instancias de clases solo se creen y residan en memoria solo cuando sea estrictamente necesario.

En estos casos es cuando el potencial de un inyector de dependencias como Dagger 2 o Hilt brilla y es cuando nos damos cuenta de lo necesarios que son en cualquier desarrollo actual por pequeño que parezca.

A pesar de todo esto, puede que en casos muy concretos donde no existan mucha complejidad de código y los caos de usos sean más bien pequeños, una inyección manual de dependencias puede que nos salve de tener que estar lidiando con instalaciones de librerías, y «dependiendo» de estas para nuestros desarrollos.

Espero que hay quedado algo más claro el concepto de inyección de dependencias y como se realiza de una forma manual. En los siguientes post empezaremos a ver Dagger 2 antes que Hilt ya que Hilt por detrás utiliza Dagger 2.