Observable: Patterns and Recipes
ViewModel Pattern
Keep source state and derived state in a ViewModel, and keep rendering logic in the View.
This pattern makes responsibilities clear:
- The ViewModel owns state transitions and business logic.
- The View only renders and forwards user actions.
- Derived values (such as counts and flags) are centralized and reusable.
As a result, code becomes easier to test, reason about, and maintain.
from nuiitivet.observable import Observable, batch
class TodoViewModel:
items = Observable([])
selected_item = Observable(None)
def __init__(self):
self.items.dispatch_to_ui()
self.selected_item.dispatch_to_ui()
self.item_count = self.items.map(lambda items: len(items))
self.has_items = self.item_count.map(lambda count: count > 0)
def add_item(self, text: str):
with batch():
current = self.items.value
self.items.value = current + [{"text": text, "done": False}]
In this structure, the View does not mutate low-level state directly. It only invokes ViewModel methods, while rendering uses Observable-derived values.
import nuiitivet as nv
from nuiitivet import material
class TodoView:
def __init__(self, vm: TodoViewModel):
self.vm = vm
def build(self):
return nv.Column(
children=[
material.Text(text=self.vm.item_count.map(lambda c: f"Items: {c}")),
material.Button(
text="Add",
on_click=lambda: self.vm.add_item("New item")
, style=material.ButtonStyle.filled()),
]
)
Derived State Composition
Use derived state composition when one value should be computed from other observables. This keeps calculation logic in one place and avoids duplicating the same formula across multiple views. It also makes changes safer because you only update the derivation once.
from nuiitivet.observable import Observable, combine
class ShoppingCart:
items = Observable([])
tax_rate = Observable(0.1)
def __init__(self):
self.subtotal = self.items.map(
lambda items: sum(item["price"] * item["qty"] for item in items)
)
self.total = combine(self.subtotal, self.tax_rate).compute(
lambda sub, rate: sub * (1 + rate)
)
Memory Management with Disposable
Use Disposable for long-lived objects that own multiple subscriptions or derived observables.
By registering disposables in one place, you can release resources deterministically and prevent leaks.
This pattern is especially useful for screens or services with explicit lifecycle boundaries.
from nuiitivet.observable import Disposable, Observable
class ViewModel(Disposable):
def __init__(self):
super().__init__()
self.count = Observable(0)
self.doubled = self.count.map(lambda x: x * 2)
self.add_disposable(self.doubled)
def dispose(self):
super().dispose()
Async Data Fetch Recipe
Use this pattern when data is loaded on a worker thread but rendered in the UI. The key point is to keep UI-facing observables dispatched to the UI thread while tracking loading and error state explicitly. This gives you predictable rendering for success, loading, and failure paths.
import threading
from nuiitivet.observable import Observable
class DataViewModel:
data = Observable([])
loading = Observable(False)
error = Observable(None)
def __init__(self):
self.data.dispatch_to_ui()
self.loading.dispatch_to_ui()
self.error.dispatch_to_ui()
def load_data_async(self):
def worker():
try:
self.loading.value = True
self.error.value = None
result = fetch_data_from_api()
self.data.value = result
except Exception as e:
self.error.value = str(e)
finally:
self.loading.value = False
threading.Thread(target=worker).start()