🧩 Semana 5 – Listas e Componentes Reutilizáveis

📌 Tópico 1 – ListView: quando usar e diferenças para Column

Em Flet, tanto ListView quanto Column organizam itens na vertical. A diferença está na intenção e no comportamento: ListView é pensado para listas longas, dinâmicas e roláveis (ex.: tarefas, mensagens), enquanto Column é ideal para poucos elementos estáticos (ex.: um formulário curto).

Recurso ListView Column
Rolagem Nativa por padrão (lista já é rolável) Opcional (scroll=...)
Uso típico Muitos itens, conteúdo que cresce em tempo real Layout de poucas seções estáticas
Propriedades úteis expand, spacing, padding, auto_scroll spacing, horizontal_alignment, scroll
Performance Mais apropriado para muitas linhas e adição/remoção frequentes Com scroll funciona, mas pode exigir mais cuidado com muitos itens
Casos de uso To-do, feed, chat, listagens CRUD Formulários, painéis, blocos de conteúdo
✅ Quando preferir ListView
  • Você terá muitos itens (dezenas/centenas) e precisa de rolamento fluido.
  • A lista cresce dinamicamente (ex.: inserir/remover itens em tempo de execução).
  • Quer usar auto_scroll=True para “grudar” no fim (padrão chat/log).
✅ Quando Column resolve melhor
  • Poucos blocos fixos (ex.: cabeçalho, formulário, rodapé) e a rolagem é secundária.
  • Alinhamentos mais simples, com elementos que não mudam de quantidade com frequência.
⚠️ Armadilhas comuns
  • Chamar page.update() muitas vezes: acumule mudanças (adicionar vários itens) e atualize uma única vez.
  • Recriar toda a lista ao editar um item: prefira atualizar apenas o controle modificado.
  • Falta de espaço: para o expand=True funcionar, o pai precisa permitir expansão (ex.: um Column pai ocupando área disponível).

🧪 Exemplo A – ListView básico (20 itens)
import flet as ft
    
    def main(page: ft.Page):
        page.title = "Semana 5 - ListView básico"
        lv = ft.ListView(expand=True, spacing=8, padding=12, auto_scroll=False)
        for i in range(1, 21):
            lv.controls.append(
                ft.Container(
                    ft.Text(f"Item {i}"),
                    padding=10, bgcolor="#FAFAFA",
                    border=ft.border.all(1, "#E5E7EB"), border_radius=8
                )
            )
        page.add(lv)
    
    ft.app(target=main)

Notas: spacing controla o espaço entre itens; padding é o respiro interno do ListView; cada item aqui é um Container estilizado como “card”.

🧪 Exemplo B – “Chat/Log” com auto_scroll=True
import flet as ft
    
    def main(page: ft.Page):
        page.title = "ListView - chat/log"
        entrada = ft.TextField(label="Mensagem", expand=True)
        lista = ft.ListView(expand=True, spacing=6, padding=12, auto_scroll=True)
    
        def enviar(e):
            if not entrada.value.strip():
                entrada.error_text = "Digite algo..."
                page.update(); return
            # adiciona mensagem ao final e "cola" a rolagem
            lista.controls.append(ft.Text(entrada.value.strip()))
            entrada.value, entrada.error_text = "", None
            page.update()
    
        page.add(lista, ft.Row([entrada, ft.FilledButton("Enviar", on_click=enviar)]))
    
    ft.app(target=main)

Por quê ListView aqui? O conteúdo cresce no fim da lista; auto_scroll mantém a visualização no último item, replicando o comportamento de chats e logs em tempo real.

🧪 Exemplo C – Quando uma Column com rolagem basta
import flet as ft
    
    def main(page: ft.Page):
        page.title = "Column com rolagem"
        col = ft.Column(
            controls=[
                ft.Text("Seção 1", weight=ft.FontWeight.BOLD),
                ft.Text("Conteúdo curto..."),
                ft.Divider(),
                ft.Text("Seção 2", weight=ft.FontWeight.BOLD),
                ft.Text("Mais conteúdo curto..."),
                ft.Divider(),
                ft.Text("Seção 3", weight=ft.FontWeight.BOLD),
                ft.Text("Mais conteúdo curto..."),
            ],
            scroll=ft.ScrollMode.AUTO,  # rola apenas se necessário
            spacing=10
        )
        page.add(col)
    
    ft.app(target=main)

Por quê Column aqui? São poucos blocos, praticamente estáticos; a rolagem é só um “backup” se a janela ficar pequena.


🔗 Documentação: ListView · Column · Container · Text · Border · Scroll / ScrollMode

➕ Tópico 2 – Lista dinâmica: Adicionar itens

Vamos criar uma lista dinâmica onde o usuário digita um texto e adiciona um item ao ListView. Isso cobre as operações de Criação e Leitura do CRUD. Abaixo estão boas práticas de UX/estado para evitar problemas comuns.

✅ Boas práticas
  • Trate o input: trim (remover espaços) e valide antes de adicionar.
  • Habilite/Desabilite o botão conforme o campo esteja preenchido.
  • Atalho de teclado: permitir “Enter” para adicionar (via on_submit).
  • Rolagem da lista: defina auto_scroll=True se quiser “colar” no último item.
  • Evite duplicados (opcional): mantenha um conjunto (set) com textos já usados.
  • Performance: ao inserir muitos itens de uma vez, faça as alterações e chame page.update() apenas no final.
🧪 Exemplo A — Adicionar com botão + validação + auto-scroll
import flet as ft
    
    def main(page: ft.Page):
        page.title = "Semana 5 - Adicionar itens"
    
        # Campo de entrada e ListView (auto_scroll = True para colar no final)
        entrada = ft.TextField(
            label="Novo item",
            hint_text="Digite e clique em Adicionar",
            expand=True,
            prefix_icon=ft.icons.NOTE_ADD,
            autofocus=True
        )
        lista = ft.ListView(expand=True, spacing=8, padding=12, auto_scroll=True)
    
        # Botão começa desabilitado; será habilitado ao digitar algo
        btn_add = ft.FilledButton(text="Adicionar", disabled=True)
    
        # Conjunto opcional para não permitir textos repetidos (case-insensitive)
        vistos = set()
    
        def validar(e=None):
            texto = (entrada.value or "").strip()
            entrada.error_text = None
            btn_add.disabled = (texto == "")
            page.update()
    
        entrada.on_change = validar  # valida a cada digitação
    
        def adicionar(e):
            texto = (entrada.value or "").strip()
            if not texto:
                entrada.error_text = "Digite algo para adicionar à lista."
                page.update()
                return
    
            # Checagem opcional: duplicados
            chave = texto.lower()
            if chave in vistos:
                entrada.error_text = "Esse item já foi adicionado."
                page.update()
                return
    
            # Cria um "card" simples para o item
            item = ft.Container(
                content=ft.Row(
                    [ft.Icon(ft.icons.CIRCLE_OUTLINED, size=18), ft.Text(texto, expand=True)]
                ),
                padding=10, bgcolor="#FAFAFA",
                border=ft.border.all(1, "#E5E7EB"), border_radius=8
            )
    
            lista.controls.append(item)
            vistos.add(chave)
    
            # Limpa o input e volta a desabilitar o botão
            entrada.value = ""
            btn_add.disabled = True
            entrada.error_text = None
    
            # Uma única atualização ao final
            page.update()
    
        # Clique no botão adiciona
        btn_add.on_click = adicionar
    
        page.add(
            ft.Row([entrada, btn_add]),
            lista
        )
    
    ft.app(target=main)
🧪 Exemplo B — “Enter” para adicionar (on_submit)
import flet as ft
    
    def main(page: ft.Page):
        page.title = "Adicionar com Enter"
    
        lista = ft.ListView(expand=True, spacing=6, padding=12, auto_scroll=True)
        entrada = ft.TextField(
            label="Novo item",
            hint_text="Digite e pressione Enter",
            expand=True,
            prefix_icon=ft.icons.NOTE_ADD
        )
    
        def adicionar(e=None):
            texto = (entrada.value or "").strip()
            if not texto:
                entrada.error_text = "Campo vazio."
                page.update(); return
    
            lista.controls.append(
                ft.Container(
                    ft.Text(texto), padding=10, bgcolor="#FAFAFA",
                    border=ft.border.all(1, "#E5E7EB"), border_radius=8
                )
            )
            entrada.value, entrada.error_text = "", None
            page.update()
    
        entrada.on_submit = adicionar  # Enter aciona a mesma função
    
        page.add(lista, ft.Row([entrada, ft.FilledButton("Adicionar", on_click=adicionar)]))
    
    ft.app(target=main)
🧪 Exemplo C — Inserindo em lote (uma única atualização)
import flet as ft
    
    def main(page: ft.Page):
        page.title = "Inserção em lote"
        lista = ft.ListView(expand=True, spacing=6, padding=12)
    
        def adicionar_muitos(e):
            # adiciona 100 itens e atualiza somente no final
            base = len(lista.controls) + 1
            for i in range(base, base + 100):
                lista.controls.append(
                    ft.Container(ft.Text(f"Item {i}"), padding=10, bgcolor="#FAFAFA",
                                 border=ft.border.all(1, "#E5E7EB"), border_radius=8)
                )
            page.update()
    
        page.add(
            ft.Row([ft.FilledButton("Adicionar 100 itens", on_click=adicionar_muitos)]),
            lista
        )
    
    ft.app(target=main)

🔗 Documentação: TextField · ListView · Row · FilledButton · Icon · Container · SnackBar

✏️ Tópico 3 – Remover e Editar itens (CRUD completo)

Agora vamos completar o nosso CRUD! Além de adicionar e listar, o usuário poderá editar e remover itens da lista.

Para editar, vamos abrir uma AlertDialog com um campo de texto preenchido com o valor atual. Para excluir, basta clicar no ícone da lixeira. Também adicionamos a opção de riscar itens com Checkbox.

import flet as ft
    
    def main(page: ft.Page):
        page.title = "Semana 5 - Editar/Remover itens"
        entrada = ft.TextField(label="Novo item", expand=True)
        lista = ft.ListView(expand=True, spacing=8, padding=12)
    
        editor_input = ft.TextField(label="Editar item")
        editor_dialog = ft.AlertDialog(
            modal=True,
            title=ft.Text("Editar item"),
            content=editor_input,
            actions=[],
            actions_alignment=ft.MainAxisAlignment.END
        )
    
        current_label: ft.Text | None = None
    
        def abrir_editor(label: ft.Text):
            def handler(e):
                nonlocal current_label
                current_label = label
                editor_input.value = label.value
                page.dialog = editor_dialog
                editor_dialog.actions = [
                    ft.TextButton("Cancelar", on_click=lambda e: fechar_editor(False)),
                    ft.ElevatedButton("Salvar", on_click=lambda e: fechar_editor(True)),
                ]
                editor_dialog.open = True
                page.update()
            return handler
    
        def fechar_editor(confirmar: bool):
            nonlocal current_label
            if confirmar and current_label and editor_input.value.strip():
                current_label.value = editor_input.value.strip()
            editor_dialog.open = False
            page.update()
    
        def make_item(texto: str) -> ft.Control:
            label = ft.Text(texto, expand=True)
            check = ft.Checkbox(value=False)
    
            def toggle(e):
                label.color = "grey" if check.value else None
                label.decoration = ft.TextDecoration.LINE_THROUGH if check.value else None
                page.update()
            check.on_change = toggle
    
            def remover(e):
                lista.controls.remove(card)
                page.update()
    
            btn_edit = ft.IconButton(icon=ft.icons.EDIT, tooltip="Editar", on_click=abrir_editor(label))
            btn_del  = ft.IconButton(icon=ft.icons.DELETE_OUTLINE, tooltip="Remover", on_click=remover)
    
            row = ft.Row([check, label, btn_edit, btn_del], alignment=ft.MainAxisAlignment.SPACE_BETWEEN)
            card = ft.Container(content=row, padding=10, bgcolor="#FAFAFA",
                                border=ft.border.all(1, "#E5E7EB"), border_radius=8)
            return card
    
        def adicionar(e):
            if not entrada.value.strip():
                entrada.error_text = "Digite algo..."
                page.update()
                return
            lista.controls.append(make_item(entrada.value.strip()))
            entrada.value, entrada.error_text = "", None
            page.update()
    
        page.add(ft.Row([entrada, ft.FilledButton("Adicionar", on_click=adicionar)]), lista)
    
    ft.app(target=main)

🧠 Exemplo do mundo real: Imagine um aplicativo de tarefas. Agora o usuário pode editar um compromisso que foi digitado errado ou remover uma tarefa que já não é mais necessária. Além disso, marcar a tarefa como "feita" riscando o texto ajuda a visualizar o progresso do dia.

🔗 Documentação: Checkbox · Text · IconButton · AlertDialog · ListView · Row · Container

🏗️ Tópico 4 – Componentização (funções fábrica)

À medida que o código do nosso app cresce, ele pode começar a ficar bagunçado e repetitivo. Para resolver isso, usamos a técnica de componentização: criar funções que montam partes da interface, chamadas de funções fábrica ou "factory functions".

🎨 Analogia: imagine que você está montando uma pizzaria. Ao invés de preparar cada pizza manualmente, você cria uma receita padronizada que, ao ser chamada, monta a pizza do mesmo jeito. No nosso app, cada tarefa será como uma "pizza" montada por uma função.

Vamos transformar a criação de uma tarefa em uma função reutilizável:

import flet as ft
    
    def criar_tarefa(texto):
        return ft.Container(
            content=ft.Row([
                ft.Checkbox(),
                ft.Text(texto, expand=True),
                ft.IconButton(icon=ft.icons.DELETE, icon_color="red")
            ]),
            padding=10,
            bgcolor="#F8FAFC",
            border=ft.border.all(1, "#CBD5E1"),
            border_radius=8
        )
    
    def main(page: ft.Page):
        entrada = ft.TextField(label="Nova tarefa", expand=True)
        lista = ft.ListView(expand=True, spacing=10, padding=10)
    
        def adicionar(e):
            if entrada.value.strip():
                lista.controls.append(criar_tarefa(entrada.value))
                entrada.value = ""
                page.update()
    
        page.add(
            ft.Row([entrada, ft.FilledButton("Adicionar", on_click=adicionar)]),
            lista
        )
    
    ft.app(target=main)

Com isso, você separa a lógica de criação dos componentes da lógica do fluxo do app, deixando o código mais organizado, legível e fácil de manter.

🔗 Documentação: Checkbox · IconButton · Container

🧱 Tópico 5 – Componentes como classes (ft.UserControl)

Quando nossos componentes precisam ter estado interno (como armazenar se uma tarefa foi concluída), usamos classes baseadas em ft.UserControl. Isso permite encapsular a lógica de cada item da interface.

🧠 Por que usar classes?
Quando uma tarefa precisa lembrar se está marcada ou não, ou responder a eventos de forma isolada, uma função simples não é suficiente. A classe resolve isso mantendo o estado e comportamento juntos.

🧱 Analogia: Se a função fábrica monta uma pizza na hora, a classe seria como criar um robô pizzaiolo, que sabe montar, modificar e até jogar fora a própria pizza. Ele lembra o que fez e reage ao cliente.

Veja um exemplo de componente "Tarefa" como classe:

import flet as ft

class Tarefa(ft.UserControl):
    def __init__(self, texto):
        super().__init__()
        self.texto = texto

    def build(self):
        self.checkbox = ft.Checkbox(on_change=self.marcar)
        self.titulo = ft.Text(self.texto, expand=True)
        self.botao_excluir = ft.IconButton(
            icon=ft.icons.DELETE,
            icon_color="red",
            on_click=self.excluir
        )

        return ft.Container(
            content=ft.Row([self.checkbox, self.titulo, self.botao_excluir]),
            padding=10,
            border=ft.border.all(1, "#E2E8F0"),
            border_radius=8
        )

    def marcar(self, e):
        if self.checkbox.value:
            self.titulo.style = ft.TextStyle(decoration=ft.TextDecoration.LINE_THROUGH)
        else:
            self.titulo.style = ft.TextStyle(decoration=None)
        self.update()

    def excluir(self, e):
        self.visible = False
        self.update()

def main(page: ft.Page):
    entrada = ft.TextField(label="Nova tarefa", expand=True)
    lista = ft.ListView(expand=True, spacing=10, padding=10)

    def adicionar(e):
        if entrada.value.strip():
            lista.controls.append(Tarefa(entrada.value))
            entrada.value = ""
            page.update()

    page.add(
        ft.Row([entrada, ft.FilledButton("Adicionar", on_click=adicionar)]),
        lista
    )

ft.app(target=main)

🔁 O componente "Tarefa" agora é reutilizável, com lógica própria de marcação e exclusão. Essa é uma abordagem profissional que imita o comportamento de frameworks modernos.

🔗 Documentação: UserControl · Checkbox · Text

🔎 Tópico 6 – Boas práticas de estado e organização

À medida que a aplicação cresce, fica difícil manter tudo em um único arquivo ou misturar lógica e interface. Por isso, aprender a organizar seu código e controlar o estado da forma certa é essencial.

🧠 O que é "estado"?
É tudo aquilo que muda com o tempo e afeta a interface. Por exemplo: se um checkbox está marcado ou não, se um item está visível, o conteúdo de um campo de texto, etc.

🧱 Analogia: Imagine uma cafeteria. O “estado” do pedido muda à medida que o cliente escolhe os itens. Se o cliente muda de ideia, o pedido também muda. O sistema precisa refletir essas mudanças sem bagunçar a lógica.


📂 Organização em arquivos separados

Com o tempo, é melhor dividir os componentes em arquivos diferentes. Por exemplo:

📁 meu_app/
│
├── main.py               # ponto de entrada (page.add e lógica principal)
├── componentes/
│   ├── tarefa.py         # componente de tarefa como classe (UserControl)
│   └── cabecalho.py      # campo de entrada e botão de adicionar
└── utilidades.py         # funções auxiliares (validação, temas etc.)

No main.py, você importa seus próprios componentes:

from componentes.tarefa import Tarefa
from componentes.cabecalho import Cabecalho

🧩 Separando lógica e aparência

Sempre que possível, evite misturar a lógica (como o que acontece ao clicar) com a aparência (como o layout visual). Mantenha funções bem nomeadas e separadas.

# Exemplo ruim
botao = ft.ElevatedButton("Clique aqui", on_click=lambda e: print("Clicou"))
# Exemplo bom
def quando_clicar(e):
    print("Clicou")

botao = ft.ElevatedButton("Clique aqui", on_click=quando_clicar)

⚖️ Boas práticas ao trabalhar com estado
  • Evite variáveis globais. Prefira classes ou controle por atributos do próprio componente.
  • Atualize a interface com self.update() quando algo muda.
  • Separe responsabilidade: um componente deve cuidar apenas de si mesmo.

Exemplo de organização interna de um componente com estado:

class Contador(ft.UserControl):
    def build(self):
        self.valor = 0
        self.label = ft.Text(str(self.valor), size=30)
        self.botao = ft.FilledButton("Somar", on_click=self.somar)
        return ft.Column([self.label, self.botao])

    def somar(self, e):
        self.valor += 1
        self.label.value = str(self.valor)
        self.update()

📦 Dica extra: use `__init__.py`

Crie um arquivo vazio chamado __init__.py na pasta componentes. Isso permite importar os arquivos como um pacote. Exemplo:

from componentes import tarefa, cabecalho

🔗 Documentação: UserControl · TextField · ElevatedButton · Column

🛠️ Prática Guiada

Vamos consolidar o conteúdo da semana com um exercício guiado. O objetivo é criar uma aplicação de lista de tarefas que permita:

  • Adicionar uma nova tarefa
  • Marcar uma tarefa como concluída
  • Editar o texto de uma tarefa existente
  • Remover uma tarefa

A aplicação será dividida em componentes reutilizáveis, com boa separação de responsabilidades, usando UserControl e funções organizadas em arquivos separados.

Recomendação de estrutura de pastas:

meu_app/
├── main.py
├── componentes/
│   ├── cabecalho.py       # Campo de entrada e botão de adicionar
│   └── tarefa.py          # Classe Tarefa (UserControl)
└── utilidades.py          # Validação e funções auxiliares

✅ Siga os tópicos anteriores e desenvolva sua aplicação por partes, testando cada funcionalidade antes de avançar.

Dica: Use variáveis de instância para manter o estado e atualize sempre com self.update() quando necessário.

📚 Leituras e Materiais

Além disso, recomendo revisar os exemplos do próprio site oficial do Flet. Eles oferecem snippets prontos com preview e explicações úteis.

📝 Tarefa / Entrega

Objetivo: Desenvolver uma aplicação de lista de tarefas completa, com as seguintes funcionalidades:

  • Adicionar item
  • Marcar como concluído
  • Editar e salvar
  • Excluir item
  • Organizar a aplicação em arquivos separados

Entrega:

  • Envie o código fonte no formato .zip com a estrutura organizada em pastas
  • Grave um vídeo curto (2-3 minutos) explicando o funcionamento da aplicação

Avaliação:

  • Funcionamento completo da aplicação (40%)
  • Organização do código (30%)
  • Uso correto de UserControl e práticas de estado (20%)
  • Explicação clara no vídeo (10%)