Saltar al contenido
K
Kotlin: el lenguajePaso a paso37 min de lectura · 3 min de máquina Básico

Ficheros y depurador

Al terminar: Leer y escribir ficheros con use y depurar con breakpoints y la pila de llamadas.

Hasta ahora todo lo que hace Marcador vive en memoria: al terminar el programa, la tabla desaparece. En esta lección convertirás textos en dorsales con try como expresión, y Torneo aprenderá a escribir su tabla en un fichero del disco, cerrándolo siempre, devolviendo un Result<Unit> que dice si lo consiguió (Unit es el «ningún dato» de Funciones y parámetros: la función no devuelve nada útil, y el Result solo cuenta si salió bien o mal). Antes de escribir código nuevo aprenderás a pararlo a mitad de ejecución con un punto de interrupción (breakpoint) de Android Studio y a mirar qué hay en cada variable.

Temas: leer y escribir ficheros con java.io.File, use para cerrarlos pase lo que pase, y breakpoints en Android Studio.

Ficheros: abrir, escribir y cerrar

Un fichero es un bloque de datos con nombre guardado en el disco: sobrevive cuando el programa termina. Para trabajar con uno, el programa lo abre, lee o escribe y lo cierra. Cerrarlo importa: mientras está abierto el sistema lo tiene reservado, y lo escrito puede no haber llegado al disco todavía.

Como viste en JDK y verificación, Kotlin se apoya en la máquina de Java, así que puede usar las clases que trae Java. Una de ellas es java.io.File: representa un fichero o una ruta del disco, exista o no (File("/no/existe.txt") es el fichero de esa ruta, aunque no esté en el disco), y tiene funciones para escribirlo, leerlo, saber su tamaño y borrarlo.

use: cerrar pase lo que pase

Escribir en un fichero puede fallar (un disco lleno, una carpeta que no existe) y, si falla a mitad, el cierre no se haría. Con las herramientas de Excepciones: try, catch y require, se arreglaría a mano con try y finally, que se ejecuta siempre. bufferedWriter() te da un escritor de texto para ese fichero:

(Es un fragmento: destino sería un fichero, como el del ejemplo de más abajo.)

fragmento: lo que use hace por ti
val escritor = destino.bufferedWriter()
try {
    escritor.write("texto")
} finally {
    escritor.close()   // el bloque finally se ejecuta pase lo que pase, incluso con excepción
}

use es el atajo de ese patrón: destino.bufferedWriter().use { it.write("texto") } y el cierre lo hace él, también si dentro salta una excepción.

ejemplo mínimo: escribir y leer un fichero con use (con comentarios)
import java.io.File                      // import: trae una clase de otro paquete para usarla por su nombre corto
 
fun main() {
    val destino = File.createTempFile("prestamos", ".txt")  // un fichero vacío en la carpeta temporal (`createTempFile` es una función que se llama escribiendo `File.` delante, sin crear antes ningún `File`)
    destino.bufferedWriter().use {                           // use cierra el escritor al salir, pase lo que pase
        it.write("Rayuela: Eva")                             // escribe texto…
        it.newLine()                                         // …y un salto de línea
        it.write("Niebla: Leo")
    }
    println(destino.readText())                              // readText(): el contenido entero del fichero
    println(destino.length())                                // length(): el tamaño en bytes (un byte es, más o menos, una letra sin acento)
    destino.delete()                                         // borra el fichero temporal
}

Al ejecutarlo, imprime:

Rayuela: Eva
Niebla: Leo
24

Para leer un fichero basta readText(), que lo abre, lo lee entero y lo cierra por ti. Leer también puede fallar (si el fichero no existe), y por eso suele ir dentro de un runCatching, como verás en el ejercicio.

Reto · Ejercicio: un fichero que se cierra solo

En el Kotlin Playground, fuera de tu proyecto (si el Playground no te deja escribir en el disco, hazlo en un scratch de Android Studio: File → New → Scratch File → Kotlin):

Recuerda que File("ruta") representa el fichero de esa ruta, exista o no. Escribe en un fichero temporal tres líneas («Ana: 3», «Iván: 2» y «Luis: 1»), cerrándolo pase lo que pase. Léelo entero e imprímelo. Después intenta leer el fichero /no/existe.txt y muestra el motivo del fallo sin que el programa se detenga. Al final, borra el temporal.

Salida esperada (el motivo entre paréntesis lo escribe tu sistema y puede variar; lo que importa es «No se pudo leer:» seguido del nombre del fichero y de un motivo):

Ana: 3
Iván: 2
Luis: 1
No se pudo leer: /no/existe.txt (No such file or directory)
Ver solución
solución del reto
import java.io.File
 
fun leer(fichero: File): Result<String> = runCatching { fichero.readText() }
 
fun main() {
    val destino = File.createTempFile("goleadores", ".txt")
    destino.bufferedWriter().use {
        it.write("Ana: 3")
        it.newLine()
        it.write("Iván: 2")
        it.newLine()
        it.write("Luis: 1")
    }
    leer(destino).onSuccess { println(it) }
    leer(File("/no/existe.txt"))
        .onSuccess { println(it) }
        .onFailure { println("No se pudo leer: ${it.message}") }
    destino.delete()
}

Salida:

Ana: 3
Iván: 2
Luis: 1
No se pudo leer: /no/existe.txt (No such file or directory)

El escritor se cierra solo al salir de use, y el fallo de leer un fichero que no existe no tumba el programa: llega como Result.failure y se muestra con onFailure.

Breakpoints en Android Studio

Un breakpoint es una marca en una línea: cuando el programa llega a ella, se detiene, y tú puedes mirar el valor de cada variable antes de seguir. Esta sección usa el código que ya tienes de la lección anterior.

Localiza cada botón del depurador por su nombre, que se muestra al dejar el puntero encima (los nombres de paneles y pestañas pueden variar un poco entre versiones de Android Studio; busca lo que hace cada uno). El atajo para poner un breakpoint en la línea del cursor es Command+F8.

  1. En la vista Android, abre con doble clic torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo → Torneo.kt y localiza la primera línea de registrar: require(partido.local !== partido.visitante) { "Un equipo no puede jugar contra sí mismo" }.
  2. Haz clic en el margen gris a la izquierda del número de línea. Aparece un punto rojo.
  3. Abre Main.kt (en el mismo paquete). A la izquierda de fun main() hay un triángulo verde. Púlsalo y elige Debug 'MainKt' (MainKt es el nombre que Kotlin da por dentro a lo que hay en Main.kt; también sirve el botón Debug de la barra de herramientas).
  4. El programa arranca y se detiene en tu línea: la línea queda resaltada y se abre abajo la ventana Debug (si no se abre sola, View → Tool Windows → Debug). En ella verás la pila de llamadas (qué función llamó a cuál) y la pestaña de variables, con el valor de partido y de this (el torneo).
  5. Con los botones de la barra de esa ventana avanzas: Step Over pasa a la línea siguiente sin entrar en las funciones que llama, Step Into entra en la función de esa línea, Step Out sale de la función actual y Resume Program sigue hasta el siguiente breakpoint o hasta el final.
  6. Lo que imprime println mientras depuras sale en la pestaña de la consola de esa misma ventana Debug, la que muestra la salida del programa.
  7. main pasa siete veces por registrar(partido): tres en el apply, una con torneoAntiguo y tres con las llamadas de errores, así que el programa se detiene siete veces. Estás en la primera parada; pulsa Resume Program seis veces para llegar a la séptima. En ella partido.local y partido.visitante son el mismo equipo (lobos), y el siguiente Step Over ejecuta el require, que lanza; runCatching lo captura y la función devuelve un Result.failure sin que el programa se detenga.

Para terminar, pulsa Resume Program hasta que el programa acabe, o el botón Stop (el cuadrado rojo) de la barra de herramientas. Para quitar el breakpoint, vuelve a hacer clic sobre el punto rojo.

Un último detalle: al depurar, Android Studio crea una configuración llamada MainKt y la deja elegida en el selector de la barra superior (junto al botón de ejecutar). Ábrelo y elige otra vez app, para que el botón de ejecutar vuelva a lanzar la app de Android y no la consola de Marcador.

Paso · Torneo.exportarTabla escribe un fichero

Objetivo. Añadir dorsalDesdeTexto, que convierte un texto en dorsal, y exportarTabla(destino), que escribe la tabla del torneo en un fichero y devuelve si lo consiguió, y usarlas desde main. Después, copiar ErroresTest.kt, que comprueba todo lo de las dos lecciones anteriores y esta.

torneo/
kotlin+java → com.sebastiannarvaez.marcador.torneo
Exportar.ktse crea
Jugador.ktse modifica
Main.ktse modifica
kotlin+java → com.sebastiannarvaez.marcador.torneo (test)
ErroresTest.ktse copia
Un fichero nuevo, dos que cambian y un test. Los nodos son los de la vista Android.

Cómo crear Exportar.kt. En la vista Android, despliega torneo → kotlin+java, haz clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test) detrás) → New → Kotlin Class/File. En la ventanita elige File, escribe Exportar y pulsa Intro.

Los bloques de código llevan como título la ruta del fichero en tu proyecto. En la vista Android, esos ficheros están en torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo.

Piezas nuevas de este paso: java.io.File con createTempFile, bufferedWriter(), write, newLine y use, todas en el ejemplo de arriba. El otro trozo del paso, dorsalDesdeTexto, usa try como expresión, que ya viste en Excepciones: try, catch y require.

Intenta escribirlo tú.

  1. dorsalDesdeTexto(texto) convierte un texto en dorsal o devuelve null si no es un número. El texto puede traer espacios a los lados: trim() los quita del principio y del final.
  2. exportarTabla(destino) escribe la tabla del torneo (la que ya produce tablaComoTexto()) en un fichero, cerrándolo pase lo que pase, y devuelve si lo consiguió. Si la carpeta no existe o el disco falla, el error llega como Result y no como excepción suelta.
  3. En main, al final, tras las líneas de antes: se prueban los textos "10", " 7 ", "diez" y "" como dorsal, mostrando ««texto» -> » y el dorsal o «no es un dorsal»; después se exporta la tabla a un fichero temporal (File.createTempFile("tabla", ".txt")) mostrando «Tabla escrita en » con su nombre y su tamaño en bytes, o «No se pudo exportar: » con el motivo, y se borra el fichero. Debe verse (el número del nombre del fichero cambia en cada ejecución):
«10» -> 10
« 7 » -> 7
«diez» -> no es un dorsal
«» -> no es un dorsal
Tabla escrita en tabla14738739224306143089.txt (161 bytes)

Qué se cambia y dónde (obligatorio): dorsalDesdeTexto va al final de Jugador.kt; import java.io.File va arriba de Main.kt, justo debajo de la línea package, y el bloque de dorsales y exportar va al final de main, antes de la llave de cierre.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

Jugador.kt, al final del fichero:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Jugador.kt
// `try` como expresión: su valor es el del bloque que se ejecute.
// Aquí, un texto que no es un número produce null en vez de tumbar el programa.
fun dorsalDesdeTexto(texto: String): Int? =
    try {
        texto.trim().toInt()
    } catch (error: NumberFormatException) {
        null
    }

Exportar.kt, fichero nuevo entero (su comentario habla del «try/finally»: es el bloque try { … } finally { … } del fragmento de arriba, que ejecuta el finally pase lo que pase, y que use resume en una línea):

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Exportar.kt
package com.sebastiannarvaez.marcador.torneo
 
import java.io.File
 
// Escribe la tabla en un fichero de texto.
// `use` cierra el escritor SIEMPRE, también si `write` lanza una excepción
// (es el try/finally de siempre, en una línea). Todo va dentro de `runCatching`
// para que el fallo (carpeta inexistente, disco lleno…) llegue como Result.
fun Torneo.exportarTabla(destino: File): Result<Unit> = runCatching {
    destino.bufferedWriter().use { escritor ->
        escritor.write(tablaComoTexto())
        escritor.newLine()
    }
}

Main.kt, el import, que va debajo del package:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
import java.io.File

Main.kt, el bloque de dorsales y de exportar, antes de la llave de cierre de main:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    // `try` es una expresión: devuelve un valor.
    for (texto in listOf("10", " 7 ", "diez", "")) {
        println("«$texto» -> ${dorsalDesdeTexto(texto) ?: "no es un dorsal"}")
    }
 
    // `use` cierra el fichero aunque falle la escritura.
    val destino = File.createTempFile("tabla", ".txt")
    torneo.exportarTabla(destino)
        .onSuccess { println("Tabla escrita en ${destino.name} (${destino.length()} bytes)") }
        .onFailure { println("No se pudo exportar: ${it.message}") }
    destino.delete()

En el bloque de main, destino.name es el nombre del fichero, sin la carpeta. Cuando compile, entrega el test. ErroresTest.kt es nuevo (10 tests, uno por cada caso de error y por dorsalDesdeTexto y exportarTabla). Comprueba lo que escribiste en las dos lecciones, así que se copia con el botón (copia al portapapeles y tú pegas); la única pieza nueva, assertIs, se explica justo debajo.

  1. En la vista Android, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) → New → Kotlin Class/File → File → escribe ErroresTest y Intro.
  2. Pulsa Copiar fichero en el bloque de ErroresTest.kt.
  3. En el editor, selecciona todo y pega: Cmd+A / Cmd+V. El texto copiado trae su propio package.
ErroresTest.kt

Dentro de ErroresTest hay una pieza que despista: en equipoNoInscritoDevuelveFailure, tras assertIs<IllegalArgumentException>(error) se escribe error.message sin ?., y en otros tests se escribe error?.message.

ejemplo del test
val error = resultado.exceptionOrNull()          // Throwable? (puede ser null)
assertIs<IllegalArgumentException>(error)        // comprueba el tipo y, si pasa, Kotlin ya sabe que no es null
assertEquals("…", error.message)                 // sin `?.`: después de assertIs, `error` es un IllegalArgumentException

assertIs hace un smart cast (el mismo de Nulabilidad): si la comprobación pasa, Kotlin trata error como ese tipo a partir de ahí. Sin assertIs, error sigue siendo Throwable? y hace falta error?.message.

Punto de control

  • En la Terminal de Android Studio (View → Tool Windows → Terminal), ./gradlew :torneo:test :torneo:run termina en BUILD SUCCESSFUL. Si un test falla, Gradle lo dice con FAILED y no llega a BUILD SUCCESSFUL.
  • Abre el informe de tests (open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test): arriba pone 75 tests (65 de antes más los 10 nuevos de ErroresTest).
  • La salida trae No se registró: Intruso no juega en Lobos, Marcador: 1-0 y Eventos (o -1 si falla): -1.
  • Trae cuatro líneas de dorsales: «10» -> 10, « 7 » -> 7, «diez» -> no es un dorsal y «» -> no es un dorsal.
  • La última línea de esta parte empieza por Tabla escrita en tabla y termina en (161 bytes).
  • Ya no queda ninguna llamada a agregar en el proyecto y Android Studio no marca nada en rojo.
  • En el paquete (test) está ErroresTest.kt.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué garantiza destino.bufferedWriter().use { … } aunque dentro del bloque salte una excepción?

  2. 2.Ejecutas runCatching { File("/no/existe.txt").readText() }. ¿Qué devuelve?

  3. 3.El depurador está parado en una línea que llama a una función tuya. ¿Qué botón entra dentro de esa función para ver sus líneas?

  4. 4.Has puesto un breakpoint en la primera línea de registrar y main pasa siete veces por esa función. Estás en la primera parada. ¿Qué hace **Resume Program**?

  5. 5.Después de depurar MainKt, el botón de ejecutar lanza la consola de Marcador en lugar de la app. ¿Por qué y cómo se arregla?

Faltan 5 de 5