Listas longas¶
Uma lista de 10 mil itens não pode virar 10 mil nós no DOM. O tempestweb resolve isso com listas virtualizadas: você declara quantos itens existem e como construir o item de um índice, e só a janela visível é materializada.
Nesta página você monta uma lista virtualizada, liga infinite scroll e pull-to-refresh — as duas bordas de qualquer lista de verdade. 🚀
Virtualização: o LazyColumn¶
Comece pelo caso mais simples: mil itens, uma janela.
from tempest_core import App, Container, Style, Text, Widget
from tempest_core import Edge
from tempest_core import LazyColumn
def view(app: App[None]) -> Widget:
"""Render a thousand items with only a window in the DOM."""
def build_row(index: int) -> Widget:
return Container(
key=str(index),
style=Style(padding=Edge.all(8)),
child=Text(content=f"Item {index}", key=f"t{index}"),
)
return LazyColumn(
key="rows",
item_count=1000,
item_builder=build_row,
window_size=30,
style=Style(height=300.0),
)
Pedaço por pedaço:
item_count=1000— o tamanho da lista inteira. É o que a barra de rolagem descreve.item_builder=build_row— a fábrica que constrói o item de um índice. É um callable Python: nunca atravessa o fio, e só é chamado para os índices da janela.window_size=30— quantos itens são materializados. Peça mais do que cabe no viewport, para haver folga antes de a janela precisar deslizar.style=Style(height=300.0)— a altura é o que faz o elemento virar um viewport rolável. Sem altura, a lista cresce com o conteúdo e não rola.
Cada item precisa de key
O item é keado pelo índice absoluto, então uma janela que desliza vira uma sequência mínima de remove/reorder/insert em vez de uma árvore nova.
O resultado no browser: 30 nós no DOM e uma barra de rolagem de mil itens — o espaço fora da janela é reservado sem criar elemento nenhum.
Infinite scroll: on_end_reached¶
Lista paginada não sabe o tamanho final: ela carrega mais quando o leitor chega
perto do fim. Declare on_end_reached.
from dataclasses import dataclass
from tempest_core import App, Container, Style, Text, Widget
from tempest_core import Edge
from tempest_core import EndReachedEvent
from tempest_core import LazyColumn
PAGE_SIZE = 25
TOTAL_ITEMS = 200
@dataclass
class ListState:
"""How many items are available so far."""
loaded: int = PAGE_SIZE
def view(app: App[ListState]) -> Widget:
"""Render a list that loads another page at its end."""
def build_row(index: int) -> Widget:
return Container(
key=str(index),
style=Style(padding=Edge.all(8)),
child=Text(content=f"Item {index}", key=f"t{index}"),
)
def load_more(event: EndReachedEvent) -> None:
if app.state.loaded >= TOTAL_ITEMS:
return
app.set_state(
lambda state: setattr(
state, "loaded", min(TOTAL_ITEMS, state.loaded + PAGE_SIZE)
)
)
return LazyColumn(
key="rows",
item_count=app.state.loaded,
item_builder=build_row,
window_size=30,
end_reached_threshold=0.8,
on_end_reached=load_more,
style=Style(height=300.0),
)
end_reached_threshold é a fração do scroll que dispara o evento — 0.8, o
default, significa "a 80% do caminho". O cliente reporta end_reached uma vez
por travessia: entrou na zona final, avisou, e só volta a avisar depois de a
lista sair dela (o que acontece naturalmente quando o handler acrescenta itens).
Sempre tenha uma condição de parada
O evento continua sendo reportado enquanto o leitor rola no fim da lista. Se
o handler crescer o estado sem limite, a lista cresce para sempre. O return
quando tudo já foi carregado é o que torna isso inofensivo — responder com
estado inalterado é uma resposta perfeitamente válida.
Pull-to-refresh: on_refresh + refreshing¶
O DOM não tem pull-to-refresh de elemento, então o cliente reconhece o gesto: um arrasto a partir da origem do scroll, ao longo do eixo da lista, passando de 64px. Fora da origem o arrasto é scroll, não pull.
import asyncio
from tempest_core import RefreshEvent
async def reload(event: RefreshEvent) -> None:
"""Reload the list from the top."""
app.set_state(lambda state: setattr(state, "refreshing", True))
await asyncio.sleep(0.6) # a busca de verdade entra aqui
def done(state: ListState) -> None:
state.refreshing = False
state.loaded = PAGE_SIZE
app.set_state(done)
Passe o handler e o estado para a lista:
LazyColumn(
key="rows",
item_count=app.state.loaded,
item_builder=build_row,
refreshing=app.state.refreshing,
on_refresh=reload,
on_end_reached=load_more,
style=Style(height=300.0),
)
refreshing faz duas coisas: desenha o indicador (uma faixa na borda do pull) e
bloqueia um segundo pull enquanto a recarga está em voo. Também vira
aria-busy, para a espera ser anunciada.
Handler async é o que torna o estado visível
Um handler sincrono liga e desliga refreshing no mesmo tick — o leitor
nunca vê o indicador. async + await da busca real renderiza o estado
intermediário.
RefreshControl: o gesto sem a lista¶
Quer pull-to-refresh em conteúdo que não é lista? Use o controle avulso:
from tempest_core import RefreshControl
RefreshControl(key="pull", refreshing=app.state.refreshing, on_refresh=reload)
Ele é uma folha da IR: o renderizador é dono do que aparece dentro — um spinner
invisível em repouso, visível quando o pull arma, girando enquanto refreshing
está ativo.
SectionList: a lista que corre na página¶
SectionList agrupa seções com cabeçalho e itens virtualizados por seção. Ela
não é um viewport com altura própria: corre no fluxo da página. on_end_reached
funciona igual — o progresso é medido por quanto da caixa da lista o viewport da
página já revelou.
Detalhes técnicos: como o cliente mede o fim
O renderizador marca a lista com data-tw-end-threshold, e o cliente
(client/lists.js) escolhe a geometria:
- elemento que rola a própria caixa →
(scrollTop + clientHeight) / scrollHeight, que numa lista virtualizada já inclui o espaço reservado fora da janela, e portanto acompanha oitem_countreal; - elemento no fluxo da página → quanto da caixa o viewport revelou.
O gesto de pull vira data-tw-refresh (y/x, então num LazyRow o pull é
para a direita) e o estado armado vira data-tw-pull-armed. No fio, os dois
eventos são {"type": "end_reached", "key": "..."} e
{"type": "refresh", "key": "..."} — sem payload, iguais nos três modos.
Recap¶
LazyColumn/LazyRow/LazyGriddeclaramitem_count+item_builder; só a janela existe no DOM. A altura noStyleé o que faz o viewport rolar.on_end_reached+end_reached_thresholddão infinite scroll — com uma condição de parada no handler.on_refresh+refreshingdão pull-to-refresh, com indicador e sem recarga duplicada. Handlerasyncpara o estado ser visível.RefreshControlleva o gesto para conteúdo que não é lista.SectionListmede o fim pelo scroll da página, não pelo próprio.
O exemplo completo, com as três coisas ligadas ao mesmo tempo, está em
examples/list_demo/app.py: