5. User Input e File di Layout

Nella lezione precedente abbiamo implementato la possibilità di fare richieste HTTP e ricevere risposte tramite la API di MediaWiki alla nostra applicazione WikipediaReader sviluppata con la libreria Kivy. Al momento l’app ha un layout a griglia molto semplice: un pulsante che permette di scaricare un articolo casuale di Wikipedia Italia e un’area testuale con possibilità di scrolling che ne mostra a schermo l’estratto principale.

In questa lezione estenderemo il nostro layout in modo da includere un secondo pulsante che scarichi un articolo richiesto dall’utente tramite input.

Potete trovare il codice completo di Wikipedia Reader nel repository GitHub.


Come creare un nuovo file di estensione .kv separato da main.py

Dal momento che il numero di widget della nostra applicazione sta aumentando, è necessario separare la loro definizione tramite linguaggio KV in un file separato. Creiamo un nuovo file dandogli il nome che preferiamo ma aggiungendogli l’estensione .kv: nel nostro caso sarà wikireader.kv. Copiamo il contenuto della nostra stringa multiriga KV all’interno del nuovo file tralasciando gli apici:

#:kivy 1.1.1

Screen:
    GridLayout:
        rows: 2
        ScrollView:
            MDLabel:
                id: mdlab
                text: "Benvenuti su Wikipedia Reader!"
                size_hint_y: None
                height: self.texture_size[1]
                text_size: self.width, None
        MDRaisedButton:
            id: mdbu
            text: "CERCA ARTICOLO CASUALE"
            size_hint_x: 1
            on_press: app.random_search_button()

In main.py modifichiamo l’istruzione return nel metodo build della classe WikiReaderApp in modo da caricare il file che abbiamo creato:

class WikiReaderApp(MDApp):

    def build(self):
        # ...
        return Builder.load_file("wikireader.kv")


Come aggiungere un widget MDTextField per permettere di inserire il testo nella barra di ricerca

All’interno di wikireader.kv aumentiamo a 4 il numero di righe della nostra griglia di layout per predisporlo ad ospitare un nuovo tasto e una nuova area testuale e implementiamo quest’ultima nello stesso livello di indentazione di ScrollView e MDRaisedButton aggiungendo un nuovo widget MDTextField:

Screen:
    GridLayout:
        rows: 4
        MDTextField:

Specifichiamo come parametri un id per il nostro widget, un placeholder tramite hint_text e l’aspetto visivo del widget quando viene selezionato dall’utente tramite mode, che accetta come valori line, rectangle e fill.

Screen:
    GridLayout:
        rows: 4
        MDTextField:
				    id: mdtext
            hint_text: "Cosa Stai Cercando?"
            mode: "rectangle"


Aggiungiamo un altro tasto MDRaisedButton per avviare la ricerca di un articolo specifico

Nello stesso livello di indentazione aggiungiamo anche un secondo pulsante per avviare la ricerca di un articolo specifico: definiamo un altro widget MDRAisedButton e rimuoviamo l’id dal pulsante di ricerca di un articolo casuale perché non ci serve più. Al nostro nuovo pulsante assegniamo all’evento on_press il metodo normal_search_button che creeremo tra poco. Aggiungiamo il parametro size_hint_y ad entrambi i pulsanti per fare in modo che l’altezza occupi il 10% dello spazio disponibile assegnandogli come valore 0.1:

Screen:
    GridLayout:
        rows: 4

        MDTextField:
            # ...

        MDRaisedButton:
            text: "AVVIA RICERCA"
            size_hint_x: 1
            size_hint_y: 0.1
            # on_press: app.normal_search_button()

        ScrollView:
            # ...

        MDRaisedButton:
            text: "CERCA ARTICOLO CASUALE"
            size_hint_x: 1
            on_press: app.random_search_button()

Torniamo al nostro file main.py e definiamo il metodo normal_search_button nella classe WikiReaderApp: per fare in modo di ottenere il contenuto da mostrare nell’area testuale definiamo la variabile query assegnandole il testo tramite l’id di MDTextField che abbiano definito nella stringa KV:

class WikiReaderApp(MDApp):

    # ...

    def normal_search_button(self):
        query = self.root.ids["mdtext"].text


Modifichiamo il metodo get_data per permettergli di raccogliere i dati di articoli casuale e specifici

A questo punto dobbiamo modificare get_data: nella lezione precedente abbiamo fatto in modo che il metodo accetti come parametri request e response in modo da poter essere chiamato come callback function di random_search_button, ora dobbiamo fare in modo che possa essere utilizzato anche da normal_search_button per raccogliere i dati di un articolo specifico.

Aggiungiamo quindi ai parametri passati a get_data anche un titolo a cui assegniamo il valore di default None e sostituiamo request e response con *args in modo tale che il metodo accetti una quantità variabile di parametri.

def get_data(self, *args, title=None):
    # ...

Per reperire i dati ottenuti in risposta alla richiesta di un articolo casuale dobbiamo accedere al secondo elemento della tupla args. Modifichiamo quindi il metodo get_data in modo tale che, qualora venga chiamato senza che venga passato un valore per il parametro title, recuperi i dati dell’articolo e estragga il titolo da inserire nell’endpoint. Quando invece viene passato un titolo specifico sarà quest’ultimo ad essere utilizzato per definire l’URL dell’endpoint.

def get_data(self, *args, title=None):
    if title == None:
        response = args[1]
        random_article = response["query"]["random"][0]
        title = random_article["title"]
    endpoint = f"https://it.wikipedia.org/w/api.php?prop=extracts&explaintext&exintro&format=json&action=query&titles={title.replace(' ', '%20')}"
    self.data_request = UrlRequest(endpoint,
                                    on_success=self.set_textarea,
                                    ca_file=certifi.where())

Richiamiamo il metodo get_data che abbiamo definito nella lezione precedente passandogli come parametro per il titolo la variabile query:

class WikiReaderApp(MDApp):

    # ...

    def normal_search_button(self):
        query = self.root.ids["mdtext"].text
        self.get_data(title=query)


Gestiamo l’eccezione KeyError in modo da mostrare un avviso qualora l’articolo richiesto non esista nel database di Wikipedia

Se a questo punto eseguiamo la nostra applicazione vedremo che tutto funziona e che possiamo sia inserire un articolo specifico che richiederne uno casuale, ma manca ancora un dettaglio importante: dobbiamo gestire l’eventualità che l’articolo richiesto dall’utente non esista nel database di Wikipedia. In questo caso il json che otteniamo come risposta avrà un titolo ma non avrà la chiave extract, e se tentiamo di mostrare l’articolo nella nostra app otterremo l’errore KeyError.

{"batchcomplete":"","query":{"normalized":[{"from":"xyz10101010101010","to":"Xyz10101010101010"}],"pages":{"-1":{"ns":0,"title":"Xyz10101010101010","missing":""}}}}

Possiamo quindi utilizzare l’errore KeyError all’interno di un blocco try ed except per gestire l’eccezione. Modifichiamo il metodo set_text_area in questo modo per permettere all’applicazione di mostrare un messaggio di errore se l’articolo richiesto non viene trovato:

class WikiReaderApp(MDApp):

    # ...

    def set_textarea(self, request, response):
        page_info = response["query"]["pages"]
        page_id = next(iter(page_info))
        page_title = page_info[page_id]["title"]
        try:
            content = page_info[page_id]["extract"]
        except KeyError:
            content = f"Ci spiace, ma la ricerca '{page_title}' non ha prodotto risultati!\n\nRiprova! "
        self.root.ids["mdlab"].text = f"{page_title}\n\n{content}"

Se eseguiamo la nostra applicazione adesso potremo quindi anche visualizzare un articolo specifico:

kivy_lezione5

Codice del filemain.py:

import certifi
from kivy.lang import Builder
from kivy.network.urlrequest import UrlRequest
from kivymd.app import MDApp


class WikiReaderApp(MDApp):

    def build(self):
        self.title = "WikipediaReader"
        self.theme_cls.primary_palette = "Teal"
        self.theme_cls.primary_hue = "400"
        return Builder.load_file("wikireader.kv")

    def normal_search_button(self):
        query = self.root.ids["mdtext"].text
        self.get_data(title=query)

    def random_search_button(self):
        endpoint = "https://it.wikipedia.org/w/api.php?action=query&list=random&rnlimit=1&rnnamespace=0&format=json"
        self.root.ids["mdlab"].text = "Caricamento in corso..."
        self.rs_request = UrlRequest(endpoint,
                                     on_success=self.get_data,
                                     ca_file=certifi.where())

    def get_data(self, *args, title=None):
        if title == None:
            response = args[1]
            random_article = response["query"]["random"][0]
            title = random_article["title"]
        endpoint = f"https://it.wikipedia.org/w/api.php?prop=extracts&explaintext&exintro&format=json&action=query&titles={title.replace(' ', '%20')}"
        self.data_request = UrlRequest(endpoint,
                                       on_success=self.set_textarea,
                                       ca_file=certifi.where())

    def set_textarea(self, request, response):
        page_info = response["query"]["pages"]
        page_id = next(iter(page_info))
        page_title = page_info[page_id]["title"]
        try:
            content = page_info[page_id]["extract"]
        except KeyError:
            content = f"Ci spiace, ma la ricerca '{page_title}' non ha prodotto risultati!\n\nRiprova! "
        self.root.ids["mdlab"].text = f"{page_title}\n\n{content}"



WikiReaderApp().run()

Codice del filewikireader.kv:

#:kivy 1.1.1

Screen:
    GridLayout:
        rows: 4

        MDTextField:
            id: mdtext
            hint_text: "Cosa Stai Cercando?"
            mode: "rectangle"

        MDRaisedButton:
            text: "AVVIA RICERCA"
            size_hint_x: 1
            size_hint_y: 0.1
            on_press: app.normal_search_button()

        ScrollView:
            MDLabel:
                id: mdlab
                text: "Benvenuti su Wikipedia Reader!"
                size_hint_y: None
                height: self.texture_size[1]
                text_size: self.width, None

        MDRaisedButton:
            text: "CERCA ARTICOLO CASUALE"
            size_hint_x: 1
            size_hint_y: 0.1
            on_press: app.random_search_button()

Nella prossima lezione implementeremo la barra di navigazione laterale a scomparsa per permettere all'utente di accedere a funzionalità aggiuntive.