Context y Application
Al terminar: Entender qué es un Context, la clase Application, e instalar LeakCanary.
Al terminar esta lección, Marcador tendrá su propia clase Application, que escribe en Logcat cada vez que arranca el proceso, y LeakCanary instalado, una herramienta que vigila la memoria de la app. En la lección siguiente la usarás para provocar una fuga de memoria y leerla.
Importa porque casi toda API de Android te pide un Context (una API son las funciones que Android ofrece a tu código), y saber cuál de los dos que existen entregar evita errores que no se ven a simple vista. Temas: Context de aplicación frente a Context de Activity, la clase Application y cómo se registra, e instalar una librería solo para depuración.
Qué es un Context
Un Context es el objeto que le da a tu código acceso al entorno de la app: sus recursos (textos, imágenes), los servicios del sistema (notificaciones, portapapeles), poder abrir otras pantallas, etc. Casi toda función de Android que ofrece el sistema te pide uno.
Hay dos Context que vas a usar constantemente, y son objetos distintos:
Application (applicationContext) | Activity (this dentro de una Activity) | |
|---|---|---|
| Cuántos hay | Uno por proceso | Uno por pantalla abierta, y otro nuevo cada vez que se recrea |
| Cuánto vive | Lo que el proceso | Hasta onDestroy (por ejemplo, al rotar) |
¿Se puede guardar en un object? (un object vive todo el proceso) | Sí | No: provoca una fuga de memoria |
Guardar un Context es dejarlo escrito en una propiedad de algo que vive todo el proceso, como un object (un singleton: una sola instancia en todo el proceso, lo verás en el ejemplo de abajo). Una fuga de memoria es un objeto que ya no sirve (una Activity destruida al rotar) y que, aun así, sigue ocupando memoria porque algo vivo lo guarda. Con Application no pasa, porque muere con el proceso. Las fugas las reproduces y las lees en Fugas de memoria.
«Estado global» quiere decir los datos y la configuración que necesita toda la app, no una pantalla concreta. Cómo se comparte uno de esos datos entre pantallas sin guardarlo en el Context, y cuándo conviene y cuándo no, lo verás con un caso real en Estado de toda la app: el partido en juego.
Una subclase es una clase que hereda de otra: tiene todo lo de la clase madre y puede añadir o cambiar cosas. Extender una clase es escribir una subclase suya, como MainActivity hereda de ComponentActivity. Application (que también es un Context) es una clase que puedes extender para ejecutar código al arrancar el proceso, antes de cualquier Activity. Se registra en el Manifest con android:name (el Manifest es el fichero AndroidManifest.xml, la ficha donde declaras a Android qué componentes tiene tu app; se explica en La Activity).
Dentro de una Activity tienes los dos a mano:
// Dentro de una Activity hay dos Context distintos:
val laPantalla: Context = this // esta Activity: muere al rotar
val laAplicacion: Context = applicationContext // MarcadorApplication: vive tanto como el procesoEjemplo: una biblioteca con su Application
Antes de tocar Marcador, la misma forma en una app inventada, la de una biblioteca. Tiene tres piezas: una clase Application, un singleton y un botón. Un singleton es una clase de la que existe una sola instancia en todo el proceso; en Kotlin se declara con object, y se explica a fondo en object, companion y vararg.
class BibliotecaApplication : Application() { // hereda de Application: Android crea UNA al arrancar el proceso
override fun onCreate() { // se llama una sola vez, antes que cualquier Activity
super.onCreate() // primero, el trabajo de la clase madre
Log.d("Biblioteca", "arranca el proceso ${android.os.Process.myPid()}") // myPid(): número del proceso (con la ruta completa en vez de un import)
}
}
object UltimoLector { // singleton: vive lo que el proceso
var nombre: String? = null // cualquiera puede LEERLO…
private set // …pero solo este object puede cambiarlo
fun apuntar(quien: String) { // la única puerta para cambiar el valor
nombre = quien
}
}
// Dentro de una Activity llamada PrestamoActivity, en setContent:
Button(onClick = { // aquí this ya sería la Activity; la etiqueta lo deja claro
Toast.makeText(this@PrestamoActivity, "Prestado", Toast.LENGTH_SHORT).show() // this@PrestamoActivity: la Activity
}) {
Text("Prestar")
}Las piezas nuevas, una a una:
private set: ya lo viste en Propiedades y visibilidad; deja leer la propiedad desde fuera pero solo cambiarla desde dentro.Log.d(tag, mensaje)escribe una línea en Logcat, la ventana donde Android registra lo que ocurre en la app; el primer texto es una etiqueta (tag) para filtrar.android.os.Process.myPid()devuelve el número del proceso, que cambia en cada ejecución. Va con la ruta completa (android.os.) para no tener que escribir unimportarriba; es válido en Kotlin.Toast.makeText(contexto, texto, duración).show()muestra un mensaje breve que flota unos segundos sobre la pantalla. Pide unContext, y por eso sirve aquí de ejemplo.this@PrestamoActivity: dentro de una lambda,thispuede referirse a otra cosa (como enapply, de Funciones de alcance). La etiqueta@PrestamoActivitydice «la Activity que lleva ese nombre»; aquíthisya lo sería, pero escribirla no estorba nunca.
Lo que se ve. Al abrir la app, en Logcat sale una sola vez:
D Biblioteca: arranca el proceso 12345Y al pulsar el botón Prestar, un mensaje «Prestado» flota unos segundos sobre la pantalla. Aunque rotes el móvil y la Activity se recree, la línea de Logcat no se repite: la Application no se recrea con ella.
Ejercicio · dos Context, dos vidas
Crea una subcarpeta nueva dentro de ejercicios (la carpeta que creaste en Rotación y muerte del proceso): clic derecho sobre com.sebastiannarvaez.marcador → New → Package → escribe .ejercicios.dos_contextos al final. Después, clic derecho sobre dos_contextos → New → Kotlin Class/File → File → DosContextos. Se ejecuta con una línea temporal en MainActivity.kt y Logcat con tag:Ejercicio, como en aquella lección.
Reto · Ejercicio: el Context de la pantalla y el de la aplicación
Escribe una función llamada compararContextos que reciba la pantalla (un parámetro de tipo ComponentActivity) y escriba en Logcat, con la etiqueta Ejercicio, dos líneas. La primera, «pantalla:» seguido del nombre de la clase y el número de instancia del Context de la pantalla. La segunda, «aplicación:» con lo mismo para el Context de la aplicación. Después enseña un Toast (el mensaje breve que flota sobre la pantalla, como el «Prestado» del ejemplo) con el texto «Hola desde el Context de la app», creado con el Context de la aplicación.
Añade, en MainActivity.kt, justo debajo de anotar("onCreate …"), una línea temporal que llame a tu función pasándole this (la propia Activity), con la ruta completa, sin import, y con el comentario // EJERCICIO: quitar al terminar. Ejecuta y gira el móvil una vez. Debes ver algo así (los números cambian):
D Ejercicio: pantalla: MainActivity@c764bd7
D Ejercicio: aplicación: Application@1f2e3d4
D Ejercicio: pantalla: MainActivity@6929fbc
D Ejercicio: aplicación: Application@1f2e3d4Al girar cambia el número de la pantalla y el de la aplicación no. Todavía sale Application porque MarcadorApplication llega en el paso de esta lección.
Pistas, todo ya visto: applicationContext (arriba); javaClass.simpleName y System.identityHashCode(x).toString(16) (el anotar de El ciclo de vida de la Activity); Toast.makeText(…).show() (el ejemplo de la biblioteca).
Ver solución
package com.sebastiannarvaez.marcador.ejercicios.dos_contextos
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
// Escribe en Logcat quién es cada Context y enseña un mensaje breve con el de la aplicación.
fun compararContextos(pantalla: ComponentActivity) {
val deLaPantalla: Context = pantalla // la Activity: otra en cada giro
val deLaApp: Context = pantalla.applicationContext // la Application: una por proceso
Log.d("Ejercicio", "pantalla: ${quienEs(deLaPantalla)}")
Log.d("Ejercicio", "aplicación: ${quienEs(deLaApp)}")
Toast.makeText(deLaApp, "Hola desde el Context de la app", Toast.LENGTH_SHORT).show()
}
// El nombre de la clase y el número de esa instancia, como en el ayudante `anotar`.
private fun quienEs(contexto: Context): String =
"${contexto.javaClass.simpleName}@${System.identityHashCode(contexto).toString(16)}"La línea temporal de MainActivity.kt, justo debajo de anotar("onCreate …"):
com.sebastiannarvaez.marcador.ejercicios.dos_contextos.compararContextos(this) // EJERCICIO: quitar al terminarAntes del paso, borra la línea temporal (la que acaba en // EJERCICIO: quitar al terminar). En MainActivity.kt, Cmd+F (Ctrl+F) con la palabra ejercicios no debe dar ningún resultado; si Android Studio añadió un import, bórralo también. El fichero del ejercicio se queda en su carpeta.
Paso · Instala LeakCanary
Objetivo: que la variante debug de :app lleve LeakCanary, y comprobar en Logcat que arranca. Aún no hay ninguna fuga: primero se instala la herramienta y luego se provoca el fallo.
Primera librería solo para debug: receta de Los ficheros de Gradle de tu proyecto.
1 · Qué instalas. LeakCanary vigila las Activity destruidas y avisa por Logcat si alguna no se libera; sin él no verías la fuga de esta lección.
2 · [versions]. En Gradle Scripts → libs.versions.toml (Version Catalog "libs"), al final de [versions]:
[versions]
# … las de antes …
leakcanaryAndroid = "2.14" # nombre de la librería en camelCase; la última estable de la serie 2 (evita las alpha: versiones de prueba, todavía sin estabilizar)3 · [libraries]. Al final de la sección:
[libraries]
# … las de antes …
# group y name son las coordenadas; version.ref apunta a la línea de arriba; el alias se lee libs.leakcanary.android
leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanaryAndroid" }4 · No es un plugin, así que la raíz (build.gradle.kts (Project: Marcador)) no cambia.
5 · La dependencia. En build.gradle.kts (Module :app), al final de dependencies { … } (variantes: Gradle: build types y BuildConfig):
// debugImplementation: solo en la variante debug (la de tu emulador al pulsar Run). Con implementation
// viajaría también en la app publicada y analizaría la memoria en cada móvil. Variantes: lección «Gradle: build types y BuildConfig»
debugImplementation(libs.leakcanary.android)6 · Sync Now. Sin errores rojos; si no, mira «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto (aquí: guion donde va un punto, o sin internet). LeakCanary no necesita código: se instala solo al arrancar.
Para comprobarlo, ejecuta la app (Run, con app elegido en el selector de arriba y el emulador arrancado, como en La Activity) y, en la Terminal de Android Studio, ejecuta esta orden de adb (todas las de esta lección están en la Chuleta de adb):
adb logcat -d -s LeakCanaryDebe aparecer una línea con LeakCanary is running and ready to detect memory leaks.
Paso · MarcadorApplication y el manifiesto
Objetivo. Crear la clase Application de Marcador y registrarla en el manifiesto. Al terminar, cada vez que arranque la app, Logcat escribirá la línea MarcadorApplication onCreate (pid …).
El fichero nuevo se crea como se creó Main.kt en Tu primer proyecto en Kotlin: en el panel, app → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador (el que no lleva (test) ni (androidTest) detrás) → New → Kotlin Class/File. Elige File, escribe el nombre MarcadorApplication y pulsa Intro. Android Studio crea el fichero con la línea package ya escrita y lo abre; la declaración de la clase la escribes tú.
El fichero va en la raíz, junto a MainActivity: son las dos clases que Android arranca por su cuenta (están en el manifiesto). Todo lo demás irá en carpetas con nombre.
El manifiesto: android:name en <application>
Abre app → manifests → AndroidManifest.xml (estructura en La Activity). android:name dice qué clase Application crea Android al arrancar; sin él, tu MarcadorApplication existiría pero nadie la crearía.
<application
android:name=".MarcadorApplication"
android:allowBackup="true"En XML no se puede escribir un comentario <!-- … --> dentro de una etiqueta, así que se explica aquí: android:name=".MarcadorApplication" es la línea nueva, y el punto inicial significa «en el paquete de la app», como en .MainActivity. android:allowBackup="true" ya estaba y los demás atributos de <application no cambian.
Dos fallos típicos: olvidarlo (la app arranca, pero Logcat no tiene la línea MarcadorApplication onCreate) o escribir mal el nombre (.MarcadorAplication: la app se cierra al abrirse, con Unable to instantiate application en Logcat).
Primero, el manifiesto (esto no se intenta, se hace tal cual): en AndroidManifest.xml, en la etiqueta <application, añade android:name=".MarcadorApplication" como primer atributo. Aún marcará rojo, porque la clase no existe: ahora la escribes.
Intenta escribirlo tú: la clase MarcadorApplication, que al arrancar el proceso escribe en Logcat, con el tag Ciclo, su nombre y el número del proceso (las piezas son las del ejemplo de la biblioteca). Lo que debes ver al arrancar, en Logcat (el número del proceso cambia en cada ejecución):
D Ciclo : MarcadorApplication onCreate (pid 12345)El título del bloque de abajo es la ruta del fichero en tu proyecto; en la vista Android está en app → kotlin+java.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador
import android.app.Application
import android.util.Log
// La clase Application: la crea Android UNA vez, antes que cualquier Activity, y
// vive tanto como el proceso. Se registra en el Manifest con android:name.
//
// Es un Context (Application es un Context) que NO depende de ninguna
// pantalla: por eso es seguro guardarlo en un singleton. Una Activity tambien es
// un Context, pero muere al rotar; guardarla es una FUGA DE MEMORIA.
class MarcadorApplication : Application() {
override fun onCreate() {
super.onCreate()
Log.d("Ciclo", "MarcadorApplication onCreate (pid ${android.os.Process.myPid()})")
}
}Ejecuta la app y comprueba que, al arrancar, adb logcat -d -s Ciclo contiene la línea MarcadorApplication onCreate (pid …). Si no está, revisa el android:name del manifiesto.
Punto de control
MainActivity.ktno contiene la palabraejercicios.- Sync Now termina sin errores rojos, y
build.gradle.kts (Module :app)tienedebugImplementation(libs.leakcanary.android). - El manifiesto lleva
android:name=".MarcadorApplication"como primer atributo de<application. - Al arrancar la app,
adb logcat -d -s Ciclocontiene una líneaMarcadorApplication onCreate (pid …), yadb logcat -d -s LeakCanarydice que LeakCanary está en marcha. - En la Terminal de Android Studio,
./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Cuál de estas afirmaciones sobre
applicationContextes cierta?2.¿Qué hace la línea
android:name=".MarcadorApplication"en la etiqueta<application>del Manifest?3.¿Por qué LeakCanary se añade con
debugImplementationy no conimplementation?4.Dentro de una Activity escribes
val a: Context = thisyval b: Context = applicationContext. Giras la pantalla. ¿Cuál de los dos sigue siendo el mismo objeto?