Saltar al contenido
K
Android: la plataformaPaso a paso35 min de lectura · 20 min de máquina Intermedio

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 hayUno por procesoUno por pantalla abierta, y otro nuevo cada vez que se recrea
Cuánto viveLo que el procesoHasta 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:

ejemplo mínimo: quién es quién (con comentarios)
// 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 proceso

Ejemplo: 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.

ejemplo mínimo: una Application, un object con private set y this@ (con comentarios)
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 un import arriba; es válido en Kotlin.
  • Toast.makeText(contexto, texto, duración).show() muestra un mensaje breve que flota unos segundos sobre la pantalla. Pide un Context, y por eso sirve aquí de ejemplo.
  • this@PrestamoActivity: dentro de una lambda, this puede referirse a otra cosa (como en apply, de Funciones de alcance). La etiqueta @PrestamoActivity dice «la Activity que lleva ese nombre»; aquí this ya 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 12345

Y 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@1f2e3d4

Al 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
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/dos_contextos/DosContextos.kt
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 …"):

MainActivity.kt: la línea temporal del ejercicio
com.sebastiannarvaez.marcador.ejercicios.dos_contextos.compararContextos(this) // EJERCICIO: quitar al terminar

Antes 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]: la línea nueva (con comentarios)
[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]: la línea nueva (con comentarios)
[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):

build.gradle.kts (Module :app): línea nueva (con comentarios)
    // 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 LeakCanary

Debe 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 …).

app → manifests
AndroidManifest.xmlse modifica
app → kotlin+java → com.sebastiannarvaez.marcador
MarcadorApplication.ktse crea
Un fichero nuevo y uno modificado, tal como los ves en la vista Android.

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.

AndroidManifest.xml: el atributo nuevo
<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
app/src/main/java/com/sebastiannarvaez/marcador/MarcadorApplication.kt
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.kt no contiene la palabra ejercicios.
  • Sync Now termina sin errores rojos, y build.gradle.kts (Module :app) tiene debugImplementation(libs.leakcanary.android).
  • El manifiesto lleva android:name=".MarcadorApplication" como primer atributo de <application.
  • Al arrancar la app, adb logcat -d -s Ciclo contiene una línea MarcadorApplication onCreate (pid …), y adb logcat -d -s LeakCanary dice que LeakCanary está en marcha.
  • En la Terminal de Android Studio, ./gradlew :app:assembleDebug termina en BUILD SUCCESSFUL.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.¿Cuál de estas afirmaciones sobre applicationContext es cierta?

  2. 2.¿Qué hace la línea android:name=".MarcadorApplication" en la etiqueta <application> del Manifest?

  3. 3.¿Por qué LeakCanary se añade con debugImplementation y no con implementation?

  4. 4.Dentro de una Activity escribes val a: Context = this y val b: Context = applicationContext. Giras la pantalla. ¿Cuál de los dos sigue siendo el mismo objeto?

Faltan 4 de 4