Kotlin - File

  • El sistema de fitxers de l'ordinador ens permet persistir les dades en memòria secundària.

    Introducció

    Entorn de treball

    Crea un projecte file amb Kotlin - Amper.

    Configura el projecte tal com s’explica a Fonaments.

    Write

    A continuació treballarem amb el fitxer src/main.kt

    Kotlin té la classe kotlin.io.path.Path per representar un fitxer, que equival a la classe java.nio.file.Path.

    Aquesta classe proporciona funcionalitat addicional sobre la implementació de Java mitjançant les funcions d’extensió.

    El primer que has de fet per escriure un fitxer, és crear l’objecte que representa el “path” del fitxer.

    import kotlin.io.path.*
    
    fun main() {
        val path = Path("mary.json")
    }

    writeText()

    A continuació pots utilitzar el “path” per escriure el contingut del fitxer:

    @Serializable
    data class Person(val name: String)
    
    fun main() {
    
        val text = Json.encodeToString(Person("Mary"))
    
        val path = Path("mary.json")
        path.writeText(text)
    }

    Missió complerta: tenim el fitxer mary.json amb aquest contingut!

    Però aquest mètode sempre funciona?

    Si el fitxer té restriccions d’accés o ja s’ha obert en un altre procés no s’escriurà res perquè es produirà una AccessDeniedException per posar un exemple.

    On està el fitxer?

    En el directori des d’on has executat el codi.

    Si és una IDE en el directori arrel del projecte.

    I per ser més exactes?

    directory

    Per guardar les dades és millor crear una carpeta data.

    Una “path” pot representar un fitxer o un directori:

    fun main() {
    
        val data = Path("data")
        data.createDirectory()
    }

    Si executo per segon cop aquest codi es produeix una Exception de tipus java.nio.file.FileAlreadyExistsException (el fitxer ja existeix).

    Una opció és capturar l’excepció amb unn block “try-catch” i ignorar l’excepció:

    fun main() {
    
        try {
            val data = Path("data")
            data.createDirectory()
        } catch (e: java.nio.file.FileAlreadyExistsException) {
            // Ignore
        }
    }

    L’altra manera més habitual és comprovar si el fitxer existeix (una carpeta és un fitxer):

    fun main() {
    
        val data = Path("data")
        if (!data.exists()) {
            data.createDirectory()
        }
    }

    Ja pots crear el fitxer mary.json dins la carpeta data:

    val data = Path("data")
    
        val text = Json.encodeToString(Person("Mary"))
    
        if (!data.exists()) {
            data.createDirectory()
        }
    
        val file = data / "mary.json"
        file.writeText(text)

    Pots veure que has utilitzat Kotlin - Extension Function / a la classe Path.

    Aquesta funció et permet construir una ruta de manera segura:

    import kotlin.io.path.div
    
    val file = Path("data") / "mary.json"
    Activitat

    Guarda les dades de “Esther”, “Marc”, “Eva” i “Raquel” en la carpeta data.

    @Serializable
    data class Person(val name: String)

    appendText()

    En lloc de guardar les dades en fitxers separats, és millor guardar totes les dades en un únic fitxer ndjson.

    Com ho fem? 🤔

    Afegint les dades al final del fitxer amb la funció appendText(text: String).

    val persons = listOf(Person("Esther"), Person("Marc"), Person("Eva"), Person("Raquel"))
    
    val data = Path("data")
    if (!data.exists()) {
        data.createDirectory()
    }
    
    val file = data / "person.ndjson"
    if (!file.exists()) {
        file.createFile()
    }
    
    persons.forEach { person ->
        file.appendText("${Json.encodeToString(person)}\n")
    }

    Obre el fitxer person.ndjson i verifica que estan les dades correctes.

    Pots veure que no has creat un fitxer JSON.

    Es tracta d’un fitxer de ndjson (Newline delimited JSON).

    Un cas d’ús habitual per NDJSON és el lliurament de múltiples instàncies de text JSON mitjançant protocols de transmissió com TCP o UNIX Pipes.

    També es pot utilitzar per emmagatzemar dades semi-estructurades.

    Activitat

    Tens aquest diagrama de dades:

    Client

    id: Long

    givenName: String

    familyName: String

    /name: String

    Address

    street: String

    city: String

    Crea les data class correspondents.

    Recorda que Address també s’ha de marcar com @Serializable.

    Activitat

    Guarda les dades de “Maria Dalmau”, “Susana Vila”, “Miquel Pol” i “Roger Font” en el fitxer data.ndjson

    Activitat

    La “Susana Vila” i el “Roger Font” han canviat d’adreça.

    Afegeix les noves dades al final del fitxer data.ndjson

    Pots veure que no sobreescrius les dades antigues.

    Read

    Ja saps que una seqüència de bytes pot representar qualsevol cosa, entre altres coses un text.

    readText()

    El mètode readText() llegeix tot el fitxer i el converteix en un objecte String.

    Com que el mètode readText() per defecte “llegeix” el contingut en format UTF-8, l’únic que fa és copiar els bytes a un atribut intern de l’objecte String.

    Per exemple, pots carregar en memòria el fitxer mary.json i convertir-lo en un objecte Person:

    val maryText = (Path("data") / "mary.json").readText()
    require(maryText == """{"name":"Mary"}""")
    
    val mary = Json.decodeFromString<Person>(maryText)
    require(mary == Person("Mary"))

    Si la ruta relativa no és correcta, o el fitxer no existeix:

    val johnText = (Path("data") / "john.json").readText()
    require(johnText == """{"name":"John"}""")

    Es produeix un error de FileNotFoundException en executar el codi …

    Exception in thread "main" java.nio.file.NoSuchFileException: data\john.json

    De totes maneres, el més habitual és verificar primer que el fitxer existeix 😼

    fun main() {
    
        require(loadPerson("john.json") == null)
        require(loadPerson("mary.json") == Person("Mary"))
    }
    
    fun loadPerson(file: String): Person? {
        val file = Path("data") / file
        if (!file.exists()) {
            return null
        }
    
        return Json.decodeFromString<Person>(file.readText())
    }
    

    readLines()

    La funció readLines() fa el mateix que la funció readText() excepte que en lloc de tornar un String torna una List<String> .

    Com el nom indica, cada String de la llista és una línia de text.

    Abans has guardat les dades d‘“Esther”, “Marc”, “Eva” i “Raquel” en el fitxer data.ndjson.

    val lines = (Path("data") / "person.ndjson").readLines()
    
    val persons = lines.map { Json.decodeFromString<Person>(it) }
    
    require(persons == listOf(Person("Esther"), Person("Marc"), Person("Eva"), Person("Raquel")))

    forEachLine()

    Si vols llegir un fitxer gran no té sentit carregar-lo tot directament a memòria principal, és millor anar llegint per parts.

    Amb una funció lambda podem processar el contingut línia per línia sense saturar la memòria principal:

    val persons = mutableListOf<Person>()
    
    (Path("data") / "person.ndjson").forEachLine {
        persons += Json.decodeFromString<Person>(it)
    }
    
    require(persons == listOf(Person("Esther"), Person("Marc"), Person("Eva"), Person("Raquel")))

    Activitat

    En l’activitat de clients i adreces has guardat les dades de client en un fitxer data.ndjson.

    Llegeix el fitxer i crea un MutableMap<Long, Client> amb les dades que estan al fitxer.

    Com que en el Map només pot haver-hi una dada per clau, només tindras 4 clients amb les dades actualitzades.

    val clients= mutableMapOf<Long,Client>()

    Projecte

    Crea una aplicació de gestió de lloguer de cotxes.

    Has de tenir clients, cotxes i lloguers.

    Instruccions:

    1. Has de crear el model de dades (diagrama).
    2. Les dades s’han de guardar en el sistema de fitxers.
    3. L’aplicació ha de tenir una interficie gràfica senzilla (utilitza Kotter)
    4. Crea una distribució amb Amper.

    Ajuda

    • Has de tenir les dades en memòria en un o més Maps.
    • En iniciar el programa has de carregar les dades dels fitxers ndjson en els Maps corresponents.
    • Quan actualitzes una dada, has d’actualitzar el Map corresponent i afegir la dada en el fitxer ndjson corresponent.