Flet 2 - Flaskr Flet App

Auteur

Fabrice Devaux

Publicatiedatum

16 december 2025

Inleiding

Met de basiskennis uit de vorige les op zak zullen we in deze les een Flet frontend maken voor de bestaande Flaskr blog applicatie.

Als backend kiezen we natuurlijk voor de Connexion implementatie. Een standaard (Open)API is eenvoudiger aan te spreken en we willen het liefst gestructureerde data, zoals JSON, terugkrijgen om mee te werken.

We kunnen op zich gewoon verderwerken in het bestaand flet-tutorial project. Je kan ook de Flet frontend toevoegen in het Flask project (flask-tutorial), of een nieuw project starten.

Het is wel belangrijk dat je een werkende Flaskr API hebt uit de Connexion les :

cd flask-tutorial
git switch flaskr-api-sqla
uv sync
uv run python -m flaskr.app

# Vanuit een andere terminal:
curl http://127.0.0.1:5000/posts/2

# Output:
{
  "author": "test",
  "body": "Somebody once told me",
  "created": "2025-12-04T12:53:17Z",
  "id": 2,
  "title": "And another one!"
}
Belangrijk

Voor het vervolg van deze les gaan we er van uit dat de flaskr-api altijd draait! Het is handig als dit in een apart terminal venster is waar je gemakkelijk de (fout)meldingen van de API kan bekijken.

Proof-of-Concept

Als eerste stap is het altijd nuttig om een zo eenvoudig mogelijk proof-of-concept te bouwen. We bekommeren ons hier nog niet om structuur, design of ‘propere’ code. We willen iets dat werkt en ons een idee geeft van wat we verder kunnen verwachten.

API Requests

Als eerste stap moeten we natuurlijk HTTP requests naar de API kunnen sturen, en de respons interpreteren.

Uit de Python standard library zouden we urllib.request kunnen gebruiken. Dit is echter erg “low level” - we zouden heel wat code moeten schrijven, o.a. rond JSON parsen, HTTP headers, timeouts, fouten, …

Er bestaan verschillende extra Python packages die de interactie met HTTP endpoints sterk vereenvoudigen. De populairste op dit moment zijn:

  • urllib3 is iets gebruiksvriendelijker dan de basis urllib maar nog steeds redelijk low-level.
  • requests is al jaren het meest gebruikte package en is gebouwd op urllib3.
  • aiohttp is specifiek ontwikkeld voor async programma’s.
  • httpx is recenter, ondersteunt HTTP/2 en zowel sync als async.

We kiezen hier voor de populaire optie: requests.

uv add requests

Het ophalen van alle blogposts via de /posts API is erg eenvoudig:

src/main.py
import requests

api_response = requests.get("http://127.0.0.1:5000/posts")
posts = api_response.json()

print(posts)

Blogposts als Columns

Bovenstaande json() method geeft ons een lijst met dictionaries die elke een blogpost voorstellen.

Werk als oefening volgende code af zodat we een Column krijgen met alle blogposts. Elke blogpost kan opnieuw voorgesteld worden als een Column met verschillende Text controls voor de verschillende onderdelen van een post.

src/main.py
import flet as ft
import requests

def main(page: ft.Page):
    api_response = requests.get("http://127.0.0.1:5000/posts")
    posts = api_response.json()

    posts_list_view = ft.Column()

    # TODO

    page.add(posts_list_view)

ft.app(target=main)
Oplossing
src/main.py
import flet as ft
import requests

def main(page: ft.Page):
    api_response = requests.get("http://127.0.0.1:5000/posts")
    posts = api_response.json()

    posts_list_view = ft.Column()
    for post in posts:
        post_view = ft.Column(
            controls=[
                ft.Text(f"Title: {post['title']}"),
                ft.Text(f"by {post['author']} on {post['created']}"),
                ft.Text(post["body"]),
            ]
        )
        posts_list_view.controls.append(post_view)

    page.add(posts_list_view)

ft.app(target=main)

Mooi is het niet, maar hiermee kunnen we wel besluiten dat het zinvol is om met deze aanpak verder te werken.

Design

Na zo een proof-of-concept is interessant om eerst eens na te denken hoe de applicatie er uit zou kunnen zien en welke componenten (Flet Controls) we daar voor willen gebruiken.

Een eenvoudige tekening, of wireframe, zal ons helpen tijdens het schrijven van de code.

Net zoals bij de Todo-App kunnen we gebruik maken van geneste Columns (en Rows) om de layout samen te stellen.

Bij het opstarten zullen we onmiddellijk de posts van de API inlezen en weergeven. Met een refresh knop (Icon) worden de posts opnieuw opgehaald.

Met een settings knop kan je een username en password ingeven via een dialoogvenster. Hierdoor worden de edit en delete knoppen op posts zichtbaar. Maar enkel voor de posts van de overeenkomstige auteur.

De new post en edit knoppen openen een gelijkaardig dialoogvenster maar bij edit moet de huidige title en body van de post al ingevuld zijn.

Data model en Client

Client

We maken een eenvoudige client class waar we de API interacties en complexiteit mee kunnen “encapsuleren”. Op die manier moet de rest van de code zich niet bekommeren hoe die interactie precies verloopt.

Om te starten implementeren we enkel het GET /posts endpoint.

src/flaskrclient.py
import requests

class FlaskrClient:
    url = "http://127.0.0.1:5000"

    def __init__(self):
        self.session = requests.Session()

    def posts(self) -> list[dict]:
        print("Gettings posts")
        response = self.session.get(f"{self.url}/posts")
        response.raise_for_status()
        data = response.json()
        print(f"Got {len(data)} posts")
        return data

Data model

FlaskrClient.post() geeft een lijst met “blogpost-dictionaries” terug. Maar met dictionaries werken is erg onhandig. Je moet steeds de juiste keys onthouden en fouten merk je pas tijdens het uitvoeren van het programma.

Met Python dataclasses kan je gemakkelijke een eenvoudig model maken van de data waarmee gewerkt wordt.

src/flaskrclient.py
from dataclasses import dataclass

@dataclass
class BlogPost:
    id: int
    author: str
    created: datetime
    title: str
    body: str

In een blogpost dictionary is created een gewone string. Omdat het handiger is om met echte datetime objecten te werken kunnen we de dataclass __post_init__ method gebruiken:

src/flaskrclient.py
from dataclasses import dataclass
from datetime import datetime

@dataclass
class BlogPost:
    id: int
    author: str
    created: datetime
    title: str
    body: str

    def __post_init__(self):
        if isinstance(self.created, str):
            self.created = datetime.fromisoformat(self.created)

Om een BlogPost object te maken kunnen we gewoon een dictionary unpacken naar keyword-arguments.

De FlaskrClient.posts method zal nu een lijst van BlogPost teruggeven. De volledige code wordt dan:

src/flaskrclient.py
from dataclasses import dataclass
from datetime import datetime
import requests

@dataclass
class BlogPost:
    id: int
    author: str
    created: datetime
    title: str
    body: str

    def __post_init__(self):
        if isinstance(self.created, str):
            self.created = datetime.fromisoformat(self.created)

class FlaskrClient:
    url = "http://127.0.0.1:5000"

    def __init__(self):
        self.session = requests.Session()

    def posts(self) -> list[BlogPost]:
        print("Gettings posts")
        response = self.session.get(f"{self.url}/posts")
        response.raise_for_status()
        data = response.json()
        print(f"Got {len(data)} posts")
        return [BlogPost(**d) for d in data]

Model en Client gebruiken

Pas nu als oefening de bestaande code in src/main.py aan om gebruik te maken van de nieuwe Client en Model.

Oplossing
src/main.py
import flet as ft
from flaskrclient import FlaskrClient

def main(page: ft.Page):
    client = FlaskrClient()
    posts = client.posts()

    posts_list_view = ft.Column()
    for post in posts:
        post_view = ft.Column(
            controls=[
                ft.Text(f"Title: {post.title}"),
                ft.Text(f"by {post.author} on {post.created}"),
                ft.Text(post.body),
            ]
        )
        posts_list_view.controls.append(post_view)

    page.add(posts_list_view)

ft.app(target=main)

PostView

Net zoals een Task bij de Todo app kunnen we Column subclassen in een eigen class die één enkele blogpost voorstelt, op basis van een gegeven BlogPost data-object.

We maken van de gelegenheid gebruik om de weergave van een blogpost al iets meer layout en stijl te geven.

src/main.py
import flet as ft
from flaskrclient import BlogPost, FlaskrClient

class PostView(ft.Column):
    def __init__(self, post: BlogPost):
        super().__init__()
        self.post = post
        self.controls = [
            ft.Row(
                controls=[
                    ft.Text(
                        self.post.title, theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM
                    ),
                ],
            ),
            ft.Text(
                f"by {self.post.author} on {self.post.created}",
                theme_style=ft.TextThemeStyle.LABEL_SMALL,
            ),
            ft.Text(self.post.body),
            ft.Divider(),
        ]

def main(page: ft.Page):
    client = FlaskrClient()
    posts = client.posts()

    posts_list_view = ft.Column()
    for post in posts:
        posts_list_view.controls.append(PostView(post))

    page.add(posts_list_view)


ft.app(target=main)

Authenticatie

Voordat we andere functionaliteit kunnen toevoegen moeten we voor authenticatie zorgen.

FlaskrClient auth method

Eerst breiden we de FlaskrClient uit met een optie om (achteraf, aan een bestaand object) authenticatiegegevens toe te voegen.

In een requests.Session object kan HTTP basic auth worden ingesteld als een (username, password) tuple in de auth property.

src/flaskrclient.py
class FlaskrClient:
    url = "http://127.0.0.1:5000"

    def __init__(self):
        self.session = requests.Session()

    def set_auth(self, username: str, password: str):
        self.session.auth = (username, password)

    def posts(self) -> list[BlogPost]:
        ...

Settings knop

Vervolgens moeten we een “settings” knop (IconButton) toevoegen. Een goed moment om ook de titel van de applicatie toe te voegen. Als we naar het design kijken willen we een Row met daarin o.a. de titel en de settings knop. Die Row komt dan samen met de bestaande posts_list_view Column in een andere “main” Column

src/main.py
def main(page: ft.Page):
    client = FlaskrClient()
    posts = client.posts()

    def enter_creds(e):
        # TODO
        ...

    posts_list_view = ft.Column()
    for post in posts:
        posts_list_view.controls.append(PostView(post))

    app_view = ft.Column(
        controls=[
            ft.Row(
                alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
                controls=[
                    ft.Text(
                        value="FlaskR Blog",
                        theme_style=ft.TextThemeStyle.HEADLINE_LARGE,
                    ),
                    ft.IconButton(icon=ft.Icons.SETTINGS, on_click=enter_creds),
                ],
            ),
            posts_list_view,
        ],
    )

    page.add(app_view)

Wat moet er gebeuren wanneer deze nieuwe settings IconButton wordt aangeklikt?

  • Een nieuw dialoogvenster moet geopend worden.
  • In het dialoogvenster kunnen username en password worden ingegeven.
  • Via een Save knop worden de ingegeven waarden opgeslagen d.m.v. de nieuwe FlaskrClient.set_auth method. Daarna wordt het dialoogvenster terug gesloten.
  • Via een Cancel knop kan het dialoogvenster zonder verdere acties gesloten worden.

Probeer als oefening de enter_creds functies te implementeren.

Tip 1

Voor het dialoogvenster gebruik je best een AlertDialog.

Zo een dialoogvenster open (en sluit) je met de page.open(dialog) (en page.close(dialog)) functie, waarbij dialog het AlertDialog object is.

In het venster zul je (via de AlertDialog.content property) twee TextField velden nodig hebben, een voor de username en een voor het password.

Voor de actions van het AlertDialog kan je twee ElevatedButton’s gebruiken, een voor Save en een voor Cancel.

De Save knop zal een aparte, geneste functie moeten aanroepen (bvb. set_creds) die enerzijds client.set_auth gebruikt om de ingegeven username/password in te stellen, en vervolgens het AlertDialog sluit.

Oplossing
src/main.py
    def enter_creds(e):
        def set_creds(e):
            client.set_auth(str(username_field.value), str(password_field.value))
            page.close(dialog)

        username_field = ft.TextField(label="Username", autofocus=True)
        password_field = ft.TextField(label="Password", password=True)
        dialog = ft.AlertDialog(
            title=ft.Text("API Credentials"),
            content=ft.Column([username_field, password_field], tight=True),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: page.close(dialog)),
                ft.ElevatedButton("Save", on_click=set_creds),
            ],
            modal=True,
        )
        page.open(dialog)

New Post

Met de authenticatie opgelost moet het nu mogelijk zijn nieuwe blogposts aan te maken.

FlaskrClient new_post method

Opnieuw moeten we eerst de FlaskrClient uitbreiden met een method om een nieuwe blogpost naar de API te sturen

De requests.post functie zorgt automatisch voor de juiste headers (bvb. Content-Type) en JSON encoding als we aan de json parameter een gewone dictionary meegeven.

Door de API response om te zetten in een BlogPost en dit te returnen zullen we deze onmiddellijk kunnen toevoegen aan de posts_list_view Column.

src/flaskrclient.py
class FlaskrClient:

    def new_post(self, title: str, body: str) -> BlogPost:
        print("Creating new post")
        response = self.session.post(
            f"{self.url}/posts", json={"title": title, "body": body}
        )
        response.raise_for_status()
        post = BlogPost(**response.json())
        print(f"New post created with ID {post.id}")
        return post

New Post knop

Voor de New Post knop kunnen we een ElevatedButton gebruiken. Deze knop moet disabled zijn tot de API credentials zijn opgegeven.

Als dialoogvenster gebruiken we opnieuw AlertDialog. De implementatie is erg vergelijkbaar met deze om de API credentials in te geven.

src/main.py
def main(page: ft.Page):
    client = FlaskrClient()
    posts = client.posts()

    def enter_creds(e):
        def set_creds(e):
            client.set_auth(str(username_field.value), str(password_field.value))
2            new_post_button.disabled = False
            new_post_button.update()
            page.close(dialog)
            ...

    def new_post(e):
        def submit_post(e):
            post = client.new_post(
                title=str(title_field.value), body=str(body_field.value)
            )
3            posts_list_view.controls.insert(0, PostView(post))
            posts_list_view.update()
            page.close(dialog)

        title_field = ft.TextField(label="Title", autofocus=True, max_length=100)
        body_field = ft.TextField(
            label="Body", multiline=True, min_lines=5, max_lines=10, max_length=5000
        )

        dialog = ft.AlertDialog(
            title=ft.Text("New Post"),
            content=ft.Column([title_field, body_field], tight=True, width=500),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: page.close(dialog)),
                ft.ElevatedButton("Submit", on_click=submit_post),
            ],
        )
        page.open(dialog)

1    new_post_button = ft.ElevatedButton( text="New Post", on_click=new_post, disabled=True )

    app_view = ft.Column(
        controls=[
            ft.Row(...),
            new_post_button,
            posts_list_view,
        ],
    )

    page.add(app_view)
1
De new_post_button ElevatedButton is initieel disabled.
2
Nadat de API creds zijn ingevoerd, wordt de new_post_button geactiveerd. De ElevatedButton moet ook worden geupdate zodat het resultaat zichtbaar wordt.
3
Het resultaat van client.new_post moet vooraan worden toegevoegd aan de lijst van posts. Ook hier moeten we update() gebruiken.

Refactor

De code in main begint erg lang en onoverzichtelijk te worden, met geneste functies en variabelen zoals new_post_button die in verschillende functies gebruikt worden.

We volgen dezelfde aanpak als bij de Todo app en herschrijven de code in een nieuwe FlaskrApp class.

FlaskrApp subclassed Column en stelt de huidige app_view voor. We willen de main functie reduceren tot het volgende:

src/main.py
def main(page: ft.Page):
    client = FlaskrClient()
    flaskr_app = FlaskrApp(client=client)
    page.add(flaskr_app)
    flaskr_app.refresh()

De FlaskrApp class ziet er als volgt uit:

src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        super().__init__()
        self.client = client
        self.new_post_button = ft.ElevatedButton(
            text="New Post", on_click=self.new_post, disabled=True
        )
        self.posts_list_view = ft.Column()
        self.controls = [
            ft.Row(
                controls=[
                    ft.Text(value="FlaskR Blog", theme_style=ft.TextThemeStyle.HEADLINE_LARGE),
                    ft.IconButton(icon=ft.Icons.SETTINGS, on_click=self.enter_creds),
                ],
            ),
            self.new_post_button,
            self.posts_list_view,
        ]

1    def refresh(self):
        self.posts_list_view.controls = [PostView(post=post) for post in self.client.posts()]
        self.update()

    def enter_creds(self, e):
        def set_creds(e):
            self.client.set_auth(str(username_field.value), str(password_field.value))
            self.new_post_button.disabled = False
2            self.update()
3            self.page.close(dialog)

        username_field = ft.TextField(label="Username", autofocus=True)
        password_field = ft.TextField(label="Password", password=True)
        dialog = ft.AlertDialog(
            title=ft.Text("API Credentials"),
            content=ft.Column([username_field, password_field], tight=True),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: self.page.close(dialog)),
                ft.ElevatedButton("Save", on_click=set_creds),
            ],
            modal=True,
        )
        self.page.open(dialog)

    def new_post(self, e):
        ...
1
Het houden interactieve code liefst uit de __init__ method dus voor het ophalen van de blogposts en opstellen van de self.posts_list_view Column maken we een extra method aan. Deze kunnen we bovendien later opnieuw gebruiken om de blogposts te refreshen.
2
Omdat we Column subclassen kunnen we om te updaten gewoon self.update() aanroepen.
3
De page is in elk control object beschikbaar via self.page.

Implementeer als oefening de new_post method, op basis van de bestaande functie.

Oplossing
src/main.py
    def new_post(self, e):
        def submit_post(e):
            post = self.client.new_post(
                title=str(title_field.value), body=str(body_field.value)
            )
            self.posts_list_view.controls.insert(0, PostView(post))
            self.update()
            self.page.close(dialog)

        title_field = ft.TextField(label="Title", autofocus=True, max_length=100)
        body_field = ft.TextField(
            label="Body", multiline=True, min_lines=5, max_lines=10, max_length=5000
        )

        dialog = ft.AlertDialog(
            title=ft.Text("New Post"),
            content=ft.Column([title_field, body_field], tight=True, width=500),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: self.page.close(dialog)),
                ft.ElevatedButton("Submit", on_click=submit_post),
            ],
        )
        self.page.open(dialog)

Posts Editen

FlaskrClient edit_post method

We breiden nogmaals de FlaskrClient class uit met een nieuwe method. We voegen ook direct een extra boolean property toe om aan te geven of de client al dan niet authenticatie credentials bevat.

src/flaskrclient.py
    def edit_post(self, post_id: int, title: str, body: str) -> BlogPost:
        print(f"Updating post with ID {post_id}")
        response = self.session.put(
            f"{self.url}/posts/{post_id}", json={"title": title, "body": body}
        )
        response.raise_for_status()
        post = BlogPost(**response.json())
        print(f"Post with ID {post.id} updated")
        return post

Ons design bepaalt dat een post kan geëdit worden met een IconButton naast de titel van de post. Maar enkel wanneer die post is aangemaakt door de huidige user.

Die IconButton zal dus deel uitmaken van een PostView. Maar de uit te voeren functie, wanneer de knop wordt aangeklikt, zal buiten PostView liggen. PostView moet zich niet bezighouden met API interacties. Elke PostView zal dus een extra on_edit functie meekrijgen. Door deze optioneel te maken (enkel wanneer de post van de huidige user is) kan PostView de edit knop al dan niet zichtbaar maken.

PostView

We bekijken eerst de aanpassingen aan de PostView class:

src/main.py
class PostView(ft.Column):
1    def __init__(self, post: BlogPost, on_edit: Optional[Callable] = None):
        super().__init__()
        self.post = post
        self.on_edit = on_edit

2        self.title = ft.Text(self.post.title, theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM)
        self.subtitle = ft.Text(f"by {self.post.author} on {self.post.created}", theme_style=ft.TextThemeStyle.LABEL_SMALL)
        self.body = ft.Text(self.post.body)
3        interaction_buttons = ft.Row()
        if self.on_edit:
            interaction_buttons.controls.append(
                ft.IconButton(icon=ft.Icons.EDIT, on_click=lambda e: self.on_edit(self))
            )
        self.controls = [
            ft.Row(controls=[self.title, interaction_buttons]),
            self.subtitle,
            self.body,
            ft.Divider(),
        ]

4    def update(self):
        self.title.value = self.post.title
        self.body.value = self.post.body
        super().update()
1
Een PostView kan nu optioneel een functie meekrijgen, uit te voeren via de edit knop.
2
De verschillende controls in een PostView worden apart bijgehouden zodat ze later geupdate kunnen worden.
3
De (optionele) edit knop zetten we in een Row want later zal nog een delete knop worden toegevoegd. Bij on_click wordt opgegeven functie uitgevoerd met de PostView zelf als argument.
4
De standard update method (van Column) wordt aangepast om eerst de titel en body Text controls te updaten.

FlaskrApp

We bekijken eerst de aanpassingen bij het maken van de PostView controls:

src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        self.username = ""

1    def refresh(self):
        postview_list = []
        for post in self.client.posts():
            if self.client.authenticated and self.username == post.author:
                postview_list.append(PostView(post=post, on_edit=self.edit_post))
            else:
                postview_list.append(PostView(post=post))
        self.posts_list_view.controls = postview_list
        self.update()

    def enter_creds(self, e):
        def set_creds(e):
2            self.username = str(username_field.value)
            ...
        ...
1
Bij het aanmaken van de PostView controls wordt on_edit=self.edit_post meegegeven enkel als er authenticatie beschikbaar is en de username overeen komt met de auteur van de post.
2
Hiervoor moeten we dus de ingegeven username bijhouden als extra property in de FlaskrApp.

En dan de nieuwe edit_post method:

src/main.py
class FlaskrApp(ft.Column):
    ...

    def edit_post(self, postview: PostView):
        def submit_post(e):
            title = str(title_field.value)
            body = str(body_field.value)
            post = self.client.edit_post(post_id=postview.post.id, title=title, body=body)
            self.page.close(dialog)
1            postview.post = post
            postview.update()

2        title_field = ft.TextField(
            value=postview.post.title, label="Title", autofocus=True, max_length=100
        )
        body_field = ft.TextField(
            value=postview.post.body,
            label="Body",
            multiline=True,
            min_lines=5,
            max_lines=10,
            max_length=5000,
        )

        dialog = ft.AlertDialog(
            title=ft.Text("Edit Post"),
            content=ft.Column([title_field, body_field], tight=True, width=500),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: self.page.close(dialog)),
                ft.ElevatedButton("Submit", on_click=submit_post),
            ],
        )
        self.page.open(dialog)
1
Na het updaten van de blogpost via de API moet de PostView control worden geupdate.
2
Het dialoogvenster gebruikt een AlertDialog control die bijna identiek is als bij een New Post.

Posts Deleten

Implementeer als oefening alle nodige aanpassingen om de delete knop toe te voegen naast de edit knop. Gebruik dezelfde aanpak als voor de edit knop.

Oplossing
src/flaskrclient.py
class FlaskrClient:
    ...

    def delete_post(self, post_id: int):
        print(f"Deleting post with ID {post_id}")
        response = self.session.delete(f"{self.url}/posts/{post_id}")
        response.raise_for_status()
        print(f"Post with ID {post_id} deleted")
src/main.py
class PostView(ft.Column):
    def __init__(
        self,
        post: BlogPost,
        on_edit: Optional[Callable] = None,
        on_delete: Optional[Callable] = None,
    ):
        ...
        self.on_delete = on_delete
        ...
        if self.on_delete:
            interaction_buttons.controls.append(
                ft.IconButton(
                    icon=ft.Icons.DELETE,
                    icon_color=ft.Colors.RED_300,
                    on_click=lambda e: self.on_delete(self),
                )
            )
src/main.py
class PostView(ft.Column):
    ...

    def refresh(self):
        postview_list = []
        for post in self.client.posts():
            if self.client.authenticated and self.username == post.author:
                postview_list.append(
                    PostView(
                        post=post, on_edit=self.edit_post, on_delete=self.delete_post
                    )
                )
            else:
                postview_list.append(PostView(post=post))
        self.posts_list_view.controls = postview_list
        self.update()

    def delete_post(self, postview: PostView):
        def delete_post(e):
            self.client.delete_post(post_id=postview.post.id)
            self.page.close(dialog)
            self.posts_list_view.controls.remove(postview)
            self.posts_list_view.update()

        dialog = ft.AlertDialog(
            title=ft.Text(f"Delete '{postview.post.title}'?"),
            actions=[
                ft.ElevatedButton("Cancel", on_click=lambda _: self.page.close(dialog)),
                ft.ElevatedButton("Delete", on_click=delete_post),
            ],
        )
        self.page.open(dialog)

Final touches

Posts lijst scrollen

Als er meer blogposts zijn dan er verticale ruimte is wordt momenteel de laatste zichtbare post gewoon afgesneden en kan er niet gescrolled worden.

Om dit op te lossen zijn twee aanpassingen nodig:

  • De post_list_view Column moet de scroll parameter hebben.
  • Zowel diezelfde Column als die er buiten (de FlaskrApp zelf) moeten expand=True krijgen zodat ze de volledig beschikbare (hier vooral de verticale) ruimte innemen.
src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        super().__init__()
        ...
        self.expand = True
        self.posts_list_view = ft.Column(scroll=ft.ScrollMode.ALWAYS, expand=True)

Ingelogde user weergeven

Het design toont linksboven de naam van de ingelogde user.

Dit kan opgelost worden met een extra Text control in the bovenste Row. Initieel is die Text leeg.

src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        ...
        self.user_info = ft.Text()
        ...
        self.controls = [
            ft.Row(
                controls=[
                    self.user_info,
                    ...

Na het ingeven van de API credentials wordt diezelfde Text dan geupdate:

src/main.py
class FlaskrApp(ft.Column):
    ...

    def enter_creds(self, e):
        def set_creds(e):
            self.username = str(username_field.value)
            self.client.set_auth(self.username, str(password_field.value))
            self.new_post_button.disabled = False
            self.user_info.value = f"Logged in as: {self.username}"
            self.refresh()
            self.page.close(dialog)

Refresh knop

Naast de “settings” knop willen we ook een knop om manueel de lijst met blogposts te refreshen.

FlaskrApp heeft al een refresh method. We hoeven dus enkel een extra IconButton te voorzien die deze method aanroept.

src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        ...
        self.controls = [
            ft.Row(
                controls=[
                    self.user_info,
                    ft.Text( value="FlaskR Blog", theme_style=ft.TextThemeStyle.HEADLINE_LARGE, ),
                    ft.IconButton(icon=ft.Icons.REFRESH, on_click=lambda e: self.refresh()),
                    ft.IconButton(icon=ft.Icons.SETTINGS, on_click=self.enter_creds),
                ],
            ),

Bovenste row uitlijnen

Om de Row beter uit te lijnen kan de parameter alignment=ft.MainAxisAlignment.SPACE_BETWEEN gebruikt worden. Om de twee IconButtons volledig rechts samen te houden kan je ze groeperen in een extra Row - met spacing=0 om de iconen zo dicht mogelijk bij elkaar te houden.

src/main.py
class FlaskrApp(ft.Column):
    def __init__(self, client: FlaskrClient):
        ...
        self.controls = [
            ft.Row(
                controls=[
                    self.user_info,
                    ft.Text( value="FlaskR Blog", theme_style=ft.TextThemeStyle.HEADLINE_LARGE, ),
                    ft.Row(
                        [
                            ft.IconButton( icon=ft.Icons.REFRESH, on_click=lambda e: self.refresh() ),
                            ft.IconButton( icon=ft.Icons.SETTINGS, on_click=self.enter_creds ),
                        ],
                        spacing=0,
                ],
                alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            ),

Extra

Referentie Implementatie

Een volledig werkende implementatie gebaseerd op deze les vind je op https://github.com/dvx76/flet-tutorial/tree/flaskr-app.

Flet Apps builden

Een afgewerkte Flet applicatie wil je natuurlijk kunnen verdelen zonder dat gebruikers daarvoor Python en Flet zelf moeten installeren.

De manier en complexiteit om een Flet applicatie de builden hangt sterkt af van het gewenste platform (MacOS, Windows, Android, …).

Details per platform vind je op https://flet.dev/docs/publish.

Pagination en Infinite Scroll

Zowel de Flet applicatie als de onderliggende API bevatten een typische beginnersfout: ze ondersteunen geen paginering.

Er worden telkens alle posts teruggestuurd en weergegeven. Dit is geen probleem tijdens onze eenvoudige testen waarbij maximum een paar tiental posts aanwezig zijn. Maar stel je voor dat we duizenden posts in de database hebben.

Een API zal in principe resultaten altijd pagineren.

Deze commit toont hoe we eenvoudige paginering kunnen toevoegen aan de API.

In de Flet applicatie willen we dan graag pas de volgende ‘pagina’(s) met posts ophalen wanneer het nodig is. Ideaal is dat dit automatisch gebeurd wanneer de gebruiker dicht bij het einde komt van de huidige lijst posts. We spreken over een zogenaamde infinite scroll.

In de Flet documentatie vinden we voor Column het volgende voorbeeld: https://flet.dev/docs/controls/column/#infinite-scroll-list

Om snel een grote hoeveelheid posts toe te voegen aan de database kunnen we volgend commando gebruiken (voor SQLite):

INSERT INTO post (title, body, author_id)
WITH RECURSIVE counter(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM counter WHERE n < 1000
)
SELECT 'title' || n, 'body' || n, 1 FROM counter;