Ejemplo práctico 13: Scope adecuado del ViewModel por destino con HILT
Objetivo
Se reproducirá la aplicación de ejemplo planteada en la documentación (punto 4.4.2.) utilizando inyección de dependencias con HILT.
Configuración del proyecto
La configuración de un proyecto Android Studio que haga uso de HILT es algo más compleja que añadir una simple librería, pero no es un inconveniente teniendo en cuenta la ayuda que proporciona. En primer lugar deberás añadir los plugins de KSP y HILT al build.gradle.kts (Project: ...)
y sincronizar Gradle.
1plugins {
2 ...
3 id("com.google.devtools.ksp") version "2.0.21-1.0.28" apply false // KSP for annotation processing, used by libraries like Hilt.
4 id("com.google.dagger.hilt.android") version "2.57" apply false // Hilt for dependency injection.
5}
¿Qué es KSP? Es una herramienta que permite procesar anotaciones (@) de forma más eficiente que la anterior (KAPT) ya que está optimizada para Kotlin.
¿Cómo saber que versión utilizar? Para saber que versión debes utilizar, tendrás que consultar la URL, en la que deberás buscar tu versión de Kotlin según el archivo del proyecto libs.versions.toml
en la propiedad kotlin
. Por ejemplo, en este caso la versión es kotlin = "2.0.21"
, que coincide con el primer valor del plugin.
!> Cuidado con actualizar la versión de Kotlin, también deberás actualizar la versión del plugin KSP.
Ahora, en el build.gradle.kts (Module :app)
, en la sección de plugins añade los siguientes plugins. No sincronices todavía, no pasa nada, pero te dirá que falta la dependencia de HILT.
1plugins {
2 ...
3 id("com.google.devtools.ksp")
4 id("com.google.dagger.hilt.android")
5}
Siguiendo con este fichero, deberás añadir las siguientes librerías y, ahora sí, sincronizar.
1// Navigation Compose
2implementation("androidx.navigation:navigation-compose:2.9.2")
3
4// Hilt
5implementation("com.google.dagger:hilt-android:2.57")
6ksp("com.google.dagger:hilt-android-compiler:2.57") // For annotation processing.
7
8// Hilt integration with ViewModel for Compose
9implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
Inicializa Hilt en la aplicación
Para inicializar HILT deberás crear una nueva clase que extienda de Application
con la anotación @HiltAndroidApp
.
1// App.kt
2
3@HiltAndroidApp
4class App: Application()
La anotación @HiltAndroidApp
crea un contenedor de dependencias global asociado al ciclo de vida de la aplicación, permitiendo que cualquier Activity
, Fragment
, etc, pueda recibir dependencias de Hilt. Dicho de otro modo, convierte la aplicación en el punto central en el que HILT configura e inyecta las dependencias necesarias para el proyecto.
Ahora, para que sea la primera clase en crearse al lanzar la aplicación, deberás registrala en el AndroidManifest.xml
.
1<application
2 android:name=".App"
3 android:allowBackup="true"
4 android:dataExtractionRules="@xml/data_extraction_rules"
5 ...>
Crea la siguiente sealed class para los destinos
1sealed class Screen(val route: String) {
2 object HomeScreen : Screen("home")
3 object DetailScreen : Screen("detail")
4}
Crea el ViewModel para el destino
Se crea el ViewModel
adaptado al scope del destino, en este caso, el detalle.
1@HiltViewModel
2class DetalleViewModel @Inject constructor(
3 private val savedStateHandle: SavedStateHandle
4) : ViewModel() {
5
6 val id = savedStateHandle.get<String>("id") ?: "No ID"
7 val contenido = mutableStateOf("Contenido del ítem $id")
8}
Composable MyAppNav
Se crea el método encargado de controlar la navegación, en este caso, en la ruta detalle se utiliza la inyección con HILT.
1@Composable
2fun MyAppNav() {
3 val navController = rememberNavController()
4
5 NavHost(navController = navController, startDestination = Screen.HomeScreen.route) {
6 composable(Screen.HomeScreen.route) {
7 HomeScreen(navController)
8 }
9 composable("detalle/{id}") { backStackEntry ->
10 val detalleViewModel: DetalleViewModel = hiltViewModel()
11 DetalleScreen(detalleViewModel)
12 }
13 }
14}
Composable Home
Representa la pantalla inicial.
1@Composable
2fun HomeScreen(navController: NavController) {
3 Column(modifier = Modifier.padding(16.dp)) {
4 Text("Home Screen")
5 Button(
6 onClick = {
7 // Navegar a la pantalla de detalle con un ID ficticio
8 navController.navigate("detalle/321")
9 }
10 ) { Text("Detalle item") }
11 }
12}
Composable para el detalle
1@Composable
2fun DetalleScreen(viewModel: DetalleViewModel) {
3 Column(modifier = Modifier.padding(16.dp)) {
4 Text("ID: ${viewModel.id}")
5 Text("Contenido: ${viewModel.contenido.value}")
6 }
7}
MainActivity
La clase MainActivity
podría quedar como se muestra a continuación. Observa la anotación @AndroidEntryPoint
, esta le indica a Hilt que una clase de Android (como Activity
, Fragment
, View
, etc.) será un punto de entrada para la inyección de dependencias. Básicamente habilita la inyección automática de dependencias en una clase de Android, gestionando su ciclo de vida y las instancias necesarias.
1@AndroidEntryPoint
2class MainActivity : ComponentActivity() {
3 @OptIn(ExperimentalMaterial3Api::class)
4 override fun onCreate(savedInstanceState: Bundle?) {
5 super.onCreate(savedInstanceState)
6 enableEdgeToEdge()
7 setContent {
8 ExampleT4_13Theme {
9 Scaffold(
10 topBar = {
11 TopAppBar(
12 title = { Text(getString(R.string.app_name)) },
13 colors = topAppBarColors(
14 containerColor = MaterialTheme.colorScheme.primaryContainer,
15 titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
16 )
17 )
18 },
19 modifier = Modifier.fillMaxSize()
20 ) { innerPadding ->
21 Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
22 MyAppNav()
23 }
24 }
25 }
26 }
27 }
28}