Flet 1 - Inleiding en TodoApp Tutorial

Auteur

Fabrice Devaux

Publicatiedatum

16 december 2025

Inleiding

Wat is Flet?

Flet is een framework waarmee je moderne, interactieve user interfaces kunt bouwen. Met Flet maak je niet alleen een web-applicatie, maar ook desktop en mobile versies. En dit allemaal vanuit dezelfde code.

Flet gebruikt Flutter als UI-engine. De Flet UI-componenten (knoppen, layouts, navigatie, …) zijn eigenlijk Flutter-widgets die via Flet worden aangestuurd.

Flutter is een UI toolkit van Google en gebruikt de Dart programmeertaal.

Om met Flet te werken hoef je Flutter en Dart niet te kennen.

Een belangrijk verschil tussen Flutter en Flet is dat Flutter een declaratief model gebruikt, waarbij de UI automatisch opnieuw wordt opgebouwd wanneer de applicatiedata verandert.

Daar tegenover implementeert Flet een imperatief UI-model waarbij je de gebruikersinterface van je applicatie “handmatig” opbouwt met stateful controls en deze vervolgens aanpast door eigenschappen van die controls te wijzigen.

Voorbereiding

Zoals gebruikelijk starten we een nieuw project (nieuwe folder) en initialiseren we git en uv. Flet zelf installeren we via het flet package. Met flet[all] krijgen we alle extra componenten, zoals bvb. flet-web voor web-applicatie rendering.

cd flet-tutorial
git init
uv init
uv add "flet[all]"

Net als Flask heeft Flet een commando, flet. Een eerste commando is flet create, waarmee een standaard projectstructuur wordt opgezet.

uv run flet create

Flet Hello World

Met het flet create commando krijgen we al direct een eenvoudige voorbeeld-applicatie voorgeschoteld. Met het run commando kan deze uitgevoerd worden (als lokale desktop-applicatie).

uv run flet run

We bekijken hieronder de bijhorende code.

Bij Flet spreken we niet over widgets maar over controls. Elk component in de Flet applicatie is eigenlijk zo een control. Door controls in elkaar te nesten wordt de visuele structuur van de applicatie opgebouwd.

De Page control staat aan de basis en stelt de volledige applicatie voor.

src/main.py
1import flet as ft

2def main(page: ft.Page):
3    counter = ft.Text("0", size=50, data=0)

6    def increment_click(e):
7        counter.data += 1
        counter.value = str(counter.data)
8        counter.update()

5    page.floating_action_button = ft.FloatingActionButton(
        icon=ft.Icons.ADD, on_click=increment_click
    )
4    page.add(
        ft.SafeArea(
            ft.Container(counter, alignment=ft.alignment.center),
            expand=True
        )
    )

ft.app(main)
1
Niet verplicht maar de conventie is om flet volledig te importeren als ft
2
De main functie krijgt als argument automatisch de Page control.
3
Een Text control wordt gebruikt om de huidige waarde van de counter bij te houden en weer te geven. Belangrijk: het eerste argument ("0"), bevat de tekst (een str dus) die getoond wordt. In data kan om het even wat opgeslagen worden. In dit geval een int die we zullen kunnen incrementeren.
4
De counter control wordt in een Container control geplaatst, met als doel de counter te centreren binnen die Container. Die laatste komt op zijn beurt in een SafeArea control. Die zorgt ervoor dat de inhoud niet overlapt met andere externe componenten (bvb. camera uitsparing bij mobile toestellen). Uiteindelijk wordt deze SafeArea (met daarin dus de Container en daarin de Text) aan de Page toegevoegd.
5
Met de FloatingActionButton control wordt de + knop gemaakt. Deze bevat een icon, gekozen uit de Icons enum. Het on_click argument krijgt een functie (Callable) die zal worden aangeroepen wanneer er op de button geklikt wordt.
6
increment_click is dus die functie. Deze functies krijgen altijd als argument een ControlEvent mee, met o.a. informatie over de oorsprong van het event. In dit geval wordt het argument verder niet gebruikt.
7
De data van de counter (Text) is een int en kunnen we gewoon incrementeren. De value is een str (dit is wat effectief wordt weergegeven).
8
De update method van het Text object zorgt ervoor dat de weergave ook daadwerkelijk wordt aangepast.
Belangrijk

De update method update niet alleen de control zelf, maar ook alle geneste controls. Met page.update() zouden we dus hetzelfde resultaat krijgen als met counter.update().

Flet run opties

Met uv run flet run krijg je een lokaal applicatievenster. We bekijken een aantal andere opties

Web

Met de --web optie wordt de applicatie beschikbaar gemaakt als web applicatie via een lokale webserver.

uv run flet run --web

Auto-reload:

Met de -d (directory) optie wordt de applicatie automatisch opnieuw geladen als een van de source-code bestanden wordt aangepast. Deze optie is te gebruiken in combinatie met andere opties zoals --web.

uv run flet run -d

Mobile

Via een aanvullende iOS of Android app kan je je applicatie gemakkelijk testen op een fysiek iOS of Android device.

uv run flet run --ios

uv run flet run --android

Controls

Vooraleer we aan een meer uitgebreide applicatie beginnen bekijken we een aantal belangrijke Flet controls individueel.

Op https://flet-controls-gallery.fly.dev krijg je een erg handig overzicht van alle controls met verschillende voorbeelden en bijhorende code. Op deze manier kan je ontdekken wat allemaal mogelijk is met Flet. Merk op dat die site zelf een Flet applicatie is!

Text

De Text control hebben we al gebruikt in het eerste voorbeeld om de counter waarde te tonen (en ook de huidig waarde bij te houden).

src/main.py
import flet as ft

def main(page: ft.Page):
    page.add(ft.Text("Size 10", size=10))
    page.add(
        ft.Text(
            "Size 40, w100",
            size=40,
            color=ft.Colors.RED,
            bgcolor=ft.Colors.BLUE_600,
            weight=ft.FontWeight.W_100,
        )
    )
    page.add(ft.Text("Selectable Text", size=40, selectable=True))
    page.add(
        ft.Text(
            "Proin rutrum, purus sit amet elementum volutpat, nunc lacus vulputate orci, cursus "
            "ultrices neque dui quis purus. Ut ultricies purus nec nibh bibendum, eget vestibulum "
            "metus various. Duis convallis maximus justo, eu rutrum libero maximus id.",
            size=40,
            max_lines=2,
            overflow=ft.TextOverflow.ELLIPSIS,
        )
    )
    page.add(
        ft.Text(
            "theme_style=ft.TextThemeStyle.HEADLINE_SMALL",
            theme_style=ft.TextThemeStyle.HEADLINE_SMALL
        )
    )

ft.app(target=main)

TextField

Met een TextField control maak je tekst velden die gebruikers van de applicatie kunnen invullen.

src/main.py
import flet as ft

def main(page: ft.Page):
    def button_clicked(e):
        text.value = f"Values: '{tf1.value}', '{tf2.value}', '{tf3.value}', '{tf4.value}'."
        page.update()

    tf1 = ft.TextField(label="Standard")
    tf2 = ft.TextField(label="Multiline", multiline=True)
    tf3 = ft.TextField(label="With Prefix", prefix_text="https://")
    tf4 = ft.TextField(
            label="Secret + Icon",
            icon=ft.Icons.KEY,
            password=True,
            can_reveal_password=True,
        )

    button = ft.ElevatedButton(text="Submit", on_click=button_clicked)
    text = ft.Text()
    page.add(tf1, tf2, tf3, tf4, button, text)

ft.app(target=main)

ElevatedButton

ElevatedButton hebben we in het vorige voorbeeld al gebruikt. Hiermee maak je een knop met tekst waar een bepaalde functie aan kan verbonden worden. Die functie wordt uitgevoerd bij het aanklikken van de knop.

Naast on_click zijn er nog andere events configureerbaar. Met on_hover detecteren we wanneer de muis over de knop komt. We maken van de gelegenheid gebruik om dieper in te gaan op events.

In de documentatie van on_hover voor ElevatedButton lezen we het volgende:

Fires when a mouse pointer enters or exists the button response area. data property of event object contains true (string) when cursor enters and false when it exits.

Op basis hiervan maken we het volgende voorbeeld.

src/main.py
import flet as ft

def main(page: ft.Page):
    def button_hover(e):
        if e.data == "true":
            text.value = "Hovering over the button"
        else:
            text.value = "Not hovering over the button"
        page.update()

    text = ft.Text("")
    button = ft.ElevatedButton("Hover me", on_hover=button_hover)

    page.add(text, button)

ft.app(target=main)

IconButton en SnackBar

IconButton werkt op dezelfde manier als ElevatedButton maar toont een enkel icoon.

In het volgend voorbeeld laten we als actie (on_click) een SnackBar verschijnen. Dat is een kort (“bite-sized”) bericht dat onderaan de applicatie wordt getoond.

We laten ook zien hoe een lambda functie kan gebruikt worden om op een compacte manier de on_click parameter in te stellen.

src/main.py
import flet as ft

def main(page: ft.Page):
    page.title = "Icon buttons"

    snackbar_yes = ft.SnackBar(
                       ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN_300)
                   )
    snackbar_no = ft.SnackBar(
                    ft.Icon(ft.Icons.CANCEL, color=ft.Colors.PINK_700)
                  )
    page.add(
          ft.IconButton(
              icon=ft.Icons.CHECK_CIRCLE,
              icon_color=ft.Colors.GREEN_300,
              icon_size=40,
              tooltip="Yep",
              on_click=lambda e: page.open(snackbar_yes),
          )
    )
    page.add(
          ft.IconButton(
              icon=ft.Icons.CANCEL,
              icon_color=ft.Colors.PINK_700,
              icon_size=40,
              tooltip="Nope",
              on_click=lambda e: page.open(snackbar_no),
          )
    )

ft.app(main)

Checkbox

Met Checkbox maak je natuurlijk een checkbox (selectievakje) die de gebruiker kan aan- of af-vinken.

Zoals de meeste andere controls kan de huidige waarde van de checkbox worden uitgelezen via het .value attribuut.

Met de on_change parameter kan een functie worden aangeroepen als de waarde verandert.

src/main.py
import flet as ft

def main(page: ft.Page):
    def checkbox(e):
        text.value = f"c1={c1.value}, c2={c2.value}, c3={c3.value}"
        page.update()

    text = ft.Text()
    c1 = ft.Checkbox(
        label="Checked by default checkbox", value=True, on_change=checkbox
    )
    c2 = ft.Checkbox(
        label="No default, tristate checkbox", tristate=True, on_change=checkbox
    )
    c3 = ft.Checkbox(label="Disabled checkbox", disabled=True, on_change=checkbox)
    page.add(text, c1, c2, c3)

ft.app(main)

Container

Met Container kan je eender welke andere control plaatsen binnen een zone en daarbij de exacte plaatsing en opmaak van die zone controleren.

In het volgende voorbeeld plaatsen we een eenvoudige Text control in een Container. Met verschillende parameters van de Container bepalen we zowel de opmaak (bvb. bgcolor voor de achtergrondkleur) als de layout (bvb. alignment om de Text control centraal in de Container te plaatsen).

We introduceren tegelijk ook een aantal parameters van de hoofd Page control om ervoor te zorgen dat alle controls van de Page ook daar centraal geplaatst worden.

src/main.py
import flet as ft

def main(page: ft.Page):
    page.title = "Container example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER

    container = ft.Container(
        content=ft.Text("Some text to be placed somewhere"),
        padding=10,
        alignment=ft.alignment.center,
        bgcolor=ft.Colors.ORANGE_800,
        width=150,
        height=150,
        border_radius=20,
        on_click=lambda e: print("Container was clicked"),
    )

    page.add(container)

ft.app(main)

Column en Row

Column en Row zijn twee andere belangrijke controls die de layout van een applicatie bepalen.

Ze bevatten een of meerdere andere controls die als een kolom of rij zullen worden uitgelijnd. Door Columns en Rows te combineren en te nesten heb je op een vrij eenvoudige manier veel controle over de plaatsing van de verschillende andere controls waar de applicatie uit bestaat.

In het volgende voorbeeld maken we één Column die bestaat uit drie Rows. Elke Row bestaat dan weer uit drie Containers. Op die manier maken we een rooster van Container controls. Met de spacing parameter in zowel Column als Row bepalen we hoe ver de verschillende Containers uit elkaar liggen, zowel vertical als horizontaal

src/main.py
import flet as ft

def create_row(color: ft.Colors) -> ft.Row:
    row = ft.Row(spacing=2)
    for i in range(1, 4):
        row.controls.append(
            ft.Container(
                content=ft.Text(str(i)),
                alignment=ft.alignment.center,
                width=50,
                height=50,
                bgcolor=color,
            )
        )
    return row

def main(page: ft.Page):
    column = ft.Column(
        controls=[
            create_row(ft.Colors.AMBER),
            create_row(ft.Colors.CYAN),
            create_row(ft.Colors.GREEN),
        ],
        alignment=ft.MainAxisAlignment.SPACE_AROUND,
    )

    page.add(column)

ft.app(main)

To-Do App Tutorial

In dit hoofdstuk bouwen we stap voor stap een eenvoudige ToDo applicatie, gebaseerd op deze tutorial uit de Flet documentatie. Op die manier krijgen we wat meer inzicht hoe verschillende controls gecombineerd kunnen worden tot een volledige applicatie.

Het uiteindelijke resultaat moet er als volgt uitzien en kan je hier uittesten.

We werken verder in het flet-tutorial project.

ToDo’s toevoegen

Als eerste stap willen we gewoon taken kunnen toevoegen.

Deze eerste stap kan als oefening gemaakt worden. Vertrek dan van de volgende basis:

src/main.py
import flet as ft

def main(page: ft.Page):
    new_task = ft.TextField(hint_text="What's needs to be done?")

    page.add(new_task)

ft.app(main)

Je zult in de FloatingActionButton de on_click parameter moeten gebruiken.

page.add kan ook binnen de on_click functie worden uitgevoerd.

Een nieuwe control toegevoegd via page.add wordt pas zichtbaar na een page.update()!

Oplossing
src/main.py
import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        page.add(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        page.update()

    new_task = ft.TextField(hint_text="What's needs to be done?")

    page.add(new_task, ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked))

ft.app(main)

Basis Layout

We willen dat de applicatie de volgende layout krijgt:

Hiervoor gebruiken we de basis layout-control Column.

Om de volledige applicatie centraal de plaatsen (bvb. in de webpagina) geef je page.horizontal_alignment de waarde ft.CrossAxisAlignment.CENTER.

Als eerste stap plaatsen we de bestaande controls in deze layout.

Column (en Row) hebben ook een width parameter. Je hebt dus geen extra Container control nodig om de breedte van de “view Column” te bepalen.

Kijk goed naar het layout-schema. Je zult Columns in elkaar moeten nesten.

Oplossing
src/main.py
import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        tasks_view.controls.append(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
    tasks_view = ft.Column()
    view=ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)

ft.app(main)

Tasks aanpassen

Naast elke task (checkbox) moet een edit knop komen waarmee de tekst van die task aangepast kan worden.

Elke task krijgt dus eigenlijk twee verschillende visuele voorstellingen (views):

  1. De normale (display) view toont de checkbox met de task tekst en een edit knop.
  2. De edit view toont een input tekstveld (TextField) met een save knop om te nieuwe tekst te bevestigen.

Elke view kan een aparte Row zijn, die we groeperen in een nieuwe Column. Op elke moment is maar één van beide Rows zichtbaar. Dit kunnen we bepalen m.b.v. de visible property van de Row.

Het resultaat kunnen we als volgt representeren:

Voor de knoppen kan IconButton gebruikt worden met Icons.

Een eerste versie, waarbij de knoppen nog niets doen, kan er als volgt uitzien:

src/main.py
import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        checkbox = ft.Checkbox(label=new_task.value)
        display_view = ft.Row(
            controls=[
                checkbox,
                ft.Row(
                    spacing=0,
                    controls=[
                        ft.IconButton(icon=ft.Icons.CREATE_OUTLINED),
                        ft.IconButton(icon=ft.Icons.DELETE_OUTLINE),
                    ],
                ),
            ],
        )
        edit_text = ft.TextField()
        edit_view = ft.Row(
            visible=False,
            controls=[
                edit_text,
                ft.IconButton(icon=ft.Icons.DONE_OUTLINE_OUTLINED),
            ],
        )

        task = ft.Column(controls=[display_view, edit_view])
        tasks_view.controls.append(task)

        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
    tasks_view = ft.Column()
    view = ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)

ft.app(main)

Om de knoppen actief te maken binden we nieuwe functies aan elke IconButton, via de on_click parameter (net zoals bij de FloatingActionButton om tasks toe te voegen).

Als de edit knop aangeklikt wordt moet dus de display_view (Row) onzichtbaar gemaakt worden (visible = False) en de edit_view zichtbaar. Bij het aanklikken van de save knop net het omgekeerde.

Afhankelijke van welke knop aangeklikt is zal het Checkbox object aangepast moeten worden, of de juiste task Column verwijderd.

De nieuwe on_click functies moeten toegang hebben tot het juiste Checkbox object, om het te kunnen aanpassen of verwijderen. Ze zullen dus binnen de bestaande add_clicked functie genest moeten worden!

Om een task (Column) te verwijderen gebruik je bvb. tasks_view.controls.remove(task)

Na elke aanpassing van een control zal er opnieuw ge-update moeten worden. Het is niet nodig de volledige page te updaten (ook al zou dit wel werken) - enkel het control object dat aangepast is (bvb. view.update())

Oplossing
src/main.py
import flet as ft

def main(page: ft.Page):
    def add_clicked(e):
        def edit_clicked(e):
            display_view.visible = False
            edit_view.visible = True
            view.update()

        def save_clicked(e):
            display_view.visible = True
            edit_view.visible = False
            checkbox.label = edit_text.value
            view.update()

        def delete_clicked(e):
            tasks_view.controls.remove(task)
            view.update()

        checkbox = ft.Checkbox(label=new_task.value)
        display_view = ft.Row(
            controls=[
                checkbox,
                ft.Row(
                    spacing=0,
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            on_click=edit_clicked,
                        ),
                        ft.IconButton(
                            ft.Icons.DELETE_OUTLINE,
                            on_click=delete_clicked,
                        ),
                    ],
                ),
            ],
        )
        edit_text = ft.TextField(new_task.value)
        edit_view = ft.Row(
            visible=False,
            controls=[
                edit_text,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    on_click=save_clicked,
                ),
            ],
        )

        task = ft.Column(controls=[display_view, edit_view])
        tasks_view.controls.append(task)

        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
    tasks_view = ft.Column()
    view = ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)

ft.app(main)

Refactoring!

De laatste code werkt wel, maar begint erg onoverzichtelijk te worden en moeilijk te begrijpen. En we zijn nog niet klaar! Tijd om te refactoren om daarna makkelijker te kunnen verderwerken.

Task

Als we het concept van een task implementeren in een eigen component (eigen control) krijgen we een handige abstractie van de verschillende views. Momenteel is een task een Column dus we kunnen hiervan vertrekken en een nieuwe Task class maken als subclass van Column.

  • Een Task krijgt natuurlijk de tekst van de task mee als argument, maar ook de uit te voeren functie om een task te deleten. Immers het deleten van een task wordt wel geïnitieerd vanuit de task zelf, maar wat er dan precies moet gebeuren is extern aan de task. In dit geval zal de task verwijderd moeten worden uit een lijst controls van een Column.
  • De Task class kan deze functie dan aanroepen wanneer het delete icon aangeklikt wordt, en zichzelf (self) meegeven als argument.
  • De class kan zelf de twee Row objecten (display en edit) initialiseren en instellen als zijn eigen controls.
src/main.py
from typing import Callable
import flet as ft

class Task(ft.Column):
    def __init__(self, task_text: str, task_delete: Callable):
        super().__init__()
        self.task_text = task_text
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(value=False, label=self.task_text)
        self.edit_name = ft.TextField()

        self.display_view = ft.Row(
            controls=[
                self.display_task,
                ft.Row(
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            ft.Icons.DELETE_OUTLINE,
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    on_click=self.save_clicked,
                ),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def delete_clicked(self, e):
        self.task_delete(self)

Probeer als oefening de bestaande main code aan te passen om gebruik te maken van deze nieuwe class. Je zult heel wat bestaande code kunnen verwijderen!

I.p.v. een Column wordt een nieuwe task nu een Task.

De functie om een task te deleten (functie die we meegeven aan elke Task) hoeven we nu niet meer te nesten binnen de add_clicked functie. We krijgen de “juiste” Task immers mee als argument.

Oplossing
src/main.py
def main(page: ft.Page):
    def task_delete(task: Task):
        tasks_view.controls.remove(task)
        tasks_view.update()

    def add_clicked(e):
        task = Task(task_text=new_task.value, task_delete=task_delete)
        tasks_view.controls.append(task)
        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
    tasks_view = ft.Column()
    view = ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)

To-Do App

Ook de code van de volledige applicatie kunnen we herstructureren in een eigen class. Opnieuw kunnen we vertrekken van een Column.

De main functie wordt dan enkel nog:

src/main.py
def main(page: ft.Page):
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(TodoApp())

Gebruik de Task class als inspiratie. TodoApp kan op een gelijkaardige manier zijn eigen controls definiëren in __init__ en aanpassen in andere methodes.

Functies als add_clicked en task_delete zullen methodes worden in de TodoApp class.

Oplossing voor class TodoApp
src/main.py
class TodoApp(ft.Column):
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
        self.tasks = ft.Column()
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=self.add_clicked)
                ],
            ),
            self.tasks,
        ]

    def add_clicked(self, e):
        task = Task(task_text=self.new_task.value, task_delete=self.task_delete)
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.update()

    def task_delete(self, task: Task):
        self.tasks.controls.remove(task)
        self.update()


def main(page: ft.Page):
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(TodoApp())

Filter Tabs

Met Tabs kunnen we tabs toevoegen om de lijst met tasks die getoond worden te filteren. We willen opties om alle tasks te zien, of enkel de actieve, of enkel de afgewerkte.

Met volgende code in de Todo class voegen we de tabs toe:

src/main.py
class TodoApp(ft.Column):
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
        self.tasks = ft.Column()
        self.tabs = ft.Tabs(
            selected_index=0,
            tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
        )
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ]
            ),
            self.tabs,
            self.tasks,
        ]

We zouden nu in elke Tab.content een verschillende Column control kunnen plaatsen met daarin de “juiste” Task objecten. Telkens een task wordt toegevoegd, aan/af-gevinkt of verwijderd moet deze dan in de juiste Column worden toegevoegd en zo nodig verwijderd.

Hier kiezen we voor een alternatieve aanpak waarbij de Tabs control enkel als filter wordt gebruikt om te bepalen welke Tasks objecten al dan niet zichtbaar moeten zijn (via de visible property).

De before_update method wordt automatisch uitgevoerd wanneer een Control wordt geupdate. We gebruiken dit in de TodoApp class om te bepalen welke Tasks zichtbaar moeten zijn, op basis van de geselecteerde tab en de huidige waarde (aangevinkt of niet) van elke Task.

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

    def before_update(self):
        status = self.tabs.tabs[self.tabs.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and not task.display_task.value)
                or (status == "completed" and task.display_task.value)
            )

De TodoApp class moet hier de interne structuur van Tasks inspecteren (task.display_task.value). Dat is een weinig elegante designkeuze. In de plaats willen we dat onze Task class rechtstreeks zijn status kan aangeven, bvb. via een nieuw bool completed attribuut. De Task kan dit attribuut dan updaten via de on_change parameter van de self.display_task Checkbox:

src/main.py
class Task(ft.Column):
    def __init__(self, task_text: str, task_delete: Callable):
        super().__init__()
        self.completed: bool = False
        self.display_task = ft.Checkbox(
            value=False, label=self.task_text, on_change=self.status_changed
        )

    def status_changed(self, e):
        self.completed = self.display_task.value

class TodoApp(ft.Column):

    def before_update(self):
        status = self.tabs.tabs[self.tabs.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and not task.completed)
                or (status == "completed" and task.completed)
            )

Deze before_update gebeurt nu enkel wanneer de TodoApp zelf wordt geupdate. Dit moet ook nog gebeuren:

  1. Wanneer een andere tab geselecteerd wordt. Dat kan via de on_change parameter van de Tabs control.
  2. Wanneer een individuele Task Checkbox wordt aan/af-gevinkt. Dat kan door een functie mee te geven aan elke Task (net zoals de task_delete functie) en deze in de Task uit te voeren wanneer de status van de Checkbox verandert.
src/main.py
class Task(ft.Column):
    def __init__(self, task_text: str, task_delete: Callable, task_status_change: Callable):
        super().__init__()
        self.completed: bool = False
        self.task_text = task_text
        self.task_status_change = task_status_change

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change()

class TodoApp(ft.Column):
    def __init__(self):

        self.tabs = ft.Tabs(
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
        )

    def add_clicked(self, e):
        task = Task(
            task_text=self.new_task.value,
            task_delete=self.task_delete,
            task_status_change=self.update,
        )

    def tabs_changed(self, e):
        self.update()

De volledige code is nu:

Oplossing
src/main.py
from typing import Callable
import flet as ft

class Task(ft.Column):
    def __init__(
        self, task_text: str, task_delete: Callable, task_status_change: Callable
    ):
        super().__init__()
        self.completed: bool = False
        self.task_text = task_text
        self.task_status_change = task_status_change
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(
            value=False,
            label=self.task_text,
            on_change=self.status_changed,
        )
        self.edit_name = ft.TextField()

        self.display_view = ft.Row(
            controls=[
                self.display_task,
                ft.Row(
                    controls=[
                        ft.IconButton(icon=ft.Icons.CREATE_OUTLINED, on_click=self.edit_clicked),
                        ft.IconButton(icon=ft.Icons.DELETE_OUTLINE, on_click=self.delete_clicked),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            controls=[
                self.edit_name,
                ft.IconButton( icon=ft.Icons.DONE_OUTLINE_OUTLINED, on_click=self.save_clicked),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def delete_clicked(self, e):
        self.task_delete(self)

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change()

class TodoApp(ft.Column):
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
        self.tasks = ft.Column()
        self.tabs = ft.Tabs(
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
        )
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ]
            ),
            self.tabs,
            self.tasks,
        ]

    def add_clicked(self, e):
        task = Task(
            task_text=self.new_task.value,
            task_delete=self.task_delete,
            task_status_change=self.update,
        )
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.update()

    def task_delete(self, task: Task):
        self.tasks.controls.remove(task)
        self.update()

    def tabs_changed(self, e):
        self.update()

    def before_update(self):
        status = self.tabs.tabs[self.tabs.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and not task.completed)
                or (status == "completed" and task.completed)
            )

def main(page: ft.Page):
    # page.title = "To-Do App"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(TodoApp())
    page.update()

ft.app(main)

Final touches

Tabs uitlijnen

We willen de 3 tabs graag gespreid over de breedte van de app. Dat kan met de Tabs.scrollable parameter.

src/main.py
        self.tabs = ft.Tabs(
            scrollable=False,
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
        )

Tasks uitlijnen

De tasks kunnen wat beter uitgelijnd worden, zodat de edit, delete en save iconen altijd op dezelfde plaats staan.

Dit kan simpelweg door de expand=True parameter toe te voegen aan de Checkbox en TextField componenten in de Task class.

src/main.py
        self.display_task = ft.Checkbox(
            value=False,
            label=self.task_text,
            on_change=self.status_changed,
            expand=True,
        )
        self.edit_name = ft.TextField(expand=True)

We willen ook vermijden dat de tekst van een Task overloopt op de IconButtons er naast. De CheckBox control zelf maar een beperkt aantal opties. Om dit op te lossen kan de CheckBox in een Container control geplaatst worden met de parameter clip_behavior=ft.ClipBehavior.HARD_EDGE. Hierdoor wordt de control “afgesneden” indien er niet genoeg plaats is. expand=True is ook nodig zodat de Container de beschikbare breedte inneemt (en niet meer).

src/main.py
        self.display_view = ft.Row(
            controls=[
                ft.Container(
                    content=self.display_task,
                    expand=True,
                    clip_behavior=ft.ClipBehavior.HARD_EDGE,
                ),
                ...

Icon tool-tips en kleur

We IconButton componenten kunnen we een tool-tip meegeven met de tooltip parameter. Bvb. tooltip="Edit To-Do" voor de edit knop.

Het icoon voor de delete knop willen we graag in het rood en de save knop in het groen. Daarvoor gebruiken we de icon_color parameter met een waarde van de ft.Colors enum.

src/main.py
        self.display_view = ft.Row(
            controls=[
                self.display_task,
                ft.Row(
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            tooltip="Edit To-Do",
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            icon=ft.Icons.DELETE_OUTLINE,
                            icon_color=ft.Colors.RED,
                            tooltip="Delete To-Do",
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    icon_color=ft.Colors.GREEN,
                    tooltip="Update To-Do",
                    on_click=self.save_clicked,
                ),
            ],
        )

Enter submit & Focus

Bij het ingeven van een nieuwe task is het handig als we deze kunnen toevoegen met de Enter toets, i.p.v. op de + knop te moeten klikken. Het self.new_task TextField heeft een on_submit parameter die we hiervoor kunnen gebruiken.

Daarnaast willen we na het toevoegen onmiddellijk een volgende task kunnen ingeven. Hiervoor moeten we op het gepaste moment de focus() methode van het self.new_task TextField aanroepen.

src/main.py
        self.new_task = ft.TextField(
            hint_text="What needs to be done?", on_submit=self.add_clicked, expand=True
        )

    def add_clicked(self, e):

        self.new_task.focus()

Complete code

Oplossing
src/main.py
from typing import Callable
import flet as ft

class Task(ft.Column):
    def __init__(
        self, task_text: str, task_delete: Callable, task_status_change: Callable
    ):
        super().__init__()
        self.completed: bool = False
        self.task_text = task_text
        self.task_status_change = task_status_change
        self.task_delete = task_delete
        self.display_task = ft.Checkbox(
            value=False,
            label=self.task_text,
            on_change=self.status_changed,
            expand=True,
        )
        self.edit_name = ft.TextField(expand=True)

        self.display_view = ft.Row(
            controls=[
                ft.Container(
                    content=self.display_task,
                    expand=True,
                    clip_behavior=ft.ClipBehavior.HARD_EDGE,
                ),
                ft.Row(
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            tooltip="Edit To-Do",
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            icon=ft.Icons.DELETE_OUTLINE,
                            icon_color=ft.Colors.RED,
                            tooltip="Delete To-Do",
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    icon_color=ft.Colors.GREEN,
                    tooltip="Update To-Do",
                    on_click=self.save_clicked,
                ),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def delete_clicked(self, e):
        self.task_delete(self)

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change()

class TodoApp(ft.Column):
    def __init__(self):
        super().__init__()
        self.new_task = ft.TextField(
            hint_text="What needs to be done?", on_submit=self.add_clicked, expand=True
        )
        self.tasks = ft.Column()
        self.tabs = ft.Tabs(
            scrollable=False,
            selected_index=0,
            on_change=self.tabs_changed,
            tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
        )
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ]
            ),
            self.tabs,
            self.tasks,
        ]

    def add_clicked(self, e):
        task = Task(
            task_text=self.new_task.value,
            task_delete=self.task_delete,
            task_status_change=self.update,
        )
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.new_task.focus()
        self.update()

    def task_delete(self, task: Task):
        self.tasks.controls.remove(task)
        self.update()

    def tabs_changed(self, e):
        self.update()

    def before_update(self):
        status = self.tabs.tabs[self.tabs.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and not task.completed)
                or (status == "completed" and task.completed)
            )

def main(page: ft.Page):
    page.title = "To-Do App"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(TodoApp())
    page.update()

ft.app(main)