diff --git a/docs/getting-started/state-management.md b/docs/getting-started/state-management.md index e121965..929bc53 100644 --- a/docs/getting-started/state-management.md +++ b/docs/getting-started/state-management.md @@ -1,234 +1,563 @@ -## Reactivity & Reactive Objects in FletX +# FletX State Management -**Reactivity** is a core pillar of the FletX framework. It enables building apps where the UI and behavior **automatically adapt** when data changes โ€” with no need for manually propagating state or triggering refreshes. +> How FletX makes managing app data **reactive**, **modular**, and **fun** โ€” no manual refresh calls required. --- -### ๐Ÿ”„ Why reactivity matters +## What Is Reactive State Management? -In most frameworks, when a value changes (e.g. a user logs in), you need to manually update the UI, synchronize the state, or refresh components. +In traditional Python apps, changing a variable doesn't affect your UI automatically โ€” you must manually tell it to refresh. -FletX eliminates all of that. It builds tight bindings between your data and your UI/logic, so everything stays in sync, automatically. +FletX introduces **reactive variables** that automatically notify the UI whenever their value changes. +Think of it like: -This leads to: +> **"When data changes, the UI reacts โ€” instantly."** -* Cleaner code, fewer bugs -* Better separation of concerns -* Faster development -* A more responsive user experience +--- + +## โš–๏ธ Static vs. Reactive Variables + +#### ๐Ÿงฑ Static (Non-Reactive) + +```python +count = 0 + +def increment(): + global count + count += 1 + print(count) # UI doesn't update automatically +``` + +Static variables store data but **donโ€™t trigger any automatic UI refresh**, youโ€™d need to manually call a render or update function whenever the value changes. + +--- -### ๐Ÿ”„ What is a reactive object? +#### โšก Reactive (FletX) -A reactive object is an **observable variable** that can be watched. When its value changes, **everything depending on it** (UI elements, logic, services, navigation, etc.) gets automatically **updated** or notified. +```python +from fletx.core import RxInt -You can use it to: +count = RxInt(0) + +def increment(): + count.value += 1 # UI updates automatically โœจ +``` -* Refresh widgets -* Trigger API calls -* Display alerts -* Change pages or views -* Execute custom business logic +Reactive variables are **data wrappers** (e.g., `RxInt`, `RxStr`, `RxBool`) that: + +- Keep track of their dependencies. +- Automatically notify bound widgets or observers when their value changes. +- Make state updates seamless โ€” no manual re-rendering. --- -### ๐Ÿ“ฆ Built-in reactive types +> ๐Ÿ’ก **Tip:** Use reactive types when you want automatic UI updates or computed state reactions. +> For static data that never changes, plain Python variables are fine. -| Type | Description | -| ------------- | ---------------------- | -| `RxInt` | Reactive integer | -| `RxStr` | Reactive string | -| `RxBool` | Reactive boolean | -| `RxList` | Reactive list | -| `RxDict` | Reactive dictionary | -| `Reactive[T]` | Custom reactive object | +--- +## ๐Ÿงฎ Example 1 โ€” Simple Counter App -**Example 1** ```python -from fletx.core import RxInt, Reactive +import flet as ft +from fletx.core import RxInt +from fletx.widgets import obx + +count = RxInt(0) -# Simple reactive int -counter = RxInt(0) -counter.increment() +def main(page: ft.Page): + def increment(e): + count.value += 1 -# Custom reactive object -class User: - def __init__(self, name): - self.name = name + page.add( + ft.Column([ + obx(lambda: ft.Text(f"Count: {count.value}")), + ft.ElevatedButton("Increment", on_click=increment) + ]) + ) + +ft.app(target=main) +``` + +๐Ÿ” **How it works** + +- `RxInt(0)` โ†’ creates a reactive integer +- `obx()` โ†’ rebuilds the widget when reactive values inside it change +- Changing `count.value` automatically refreshes the text โ€” no extra code! + +--- + +## ๐Ÿง  Using a Controller + +As apps grow, logic should live outside the UI. +Controllers in FletX manage both state and business logic cleanly. -rx_user = Reactive(User("Henri")) -rx_user.value = (User("Sarah")) +#### Without Controller + +```python +count = RxInt(0) + +def increment(): + count.value += 1 ``` -**Example 2** +#### With Controller + ```python +from fletx.core.controller import FletXController from fletx.core import RxInt -counter = RxInt(0) -counter.listen(lambda v: print("New value:", v)) +class CounterController(FletXController): + def __init__(self): + super().__init__() + self.count = RxInt(0) + + def increment(self): + self.count.value += 1 + + +# Usage example +ctrl = CounterController() -counter.increment() # Prints: New value: 1 ``` -**Example 3** +> โœ… **Why this matters:** It separates UI (presentation) from logic (state management), making your app easier to maintain and test. + +**Full example:** + ```python +import flet as ft +from fletx.core.controller import FletXController as Controller +from fletx.core import RxInt +from fletx.widgets.obx import Obx -rx_user = Reactive(User("Luc")) +class CounterController(Controller): + def __init__(self): + self.count = RxInt(0) -rx_user.listen(lambda u: print(f"User changed to: {u.name}")) + def increment(self): + self.count.value += 1 -rx_user.value = User("Sarah") # Prints: User changed to: Sarah +def main(page: ft.Page): + ctrl = CounterController() + + page.add( + ft.Column([ + Obx(lambda: ft.Text(f"Count: {ctrl.count.value}")), + ft.ElevatedButton("Increment", on_click=lambda e: ctrl.increment()) + ]) + ) + +ft.app(target=main) ``` --- -### โš™๏ธ UI Reactivity with FletX Decorators +## ๐Ÿงฑ Reactive Data Types Overview + +| Type | Description | Example | +| ----------------- | ------------------- | ---------------------------- | +| `RxInt` | Reactive integer | `count = RxInt(0)` | +| `RxStr` | Reactive string | `name = RxStr("Sam")` | +| `RxBool` | Reactive boolean | `is_loading = RxBool(False)` | +| `RxFloat` | Reactive float | `price = RxFloat(0.0)` | +| `reactive_list()` | Reactive list | `tasks = reactive_list([])` | +| `reactive_dict()` | Reactive dictionary | `user = reactive_dict({})` | + +> โš ๏ธ **Note:** Reactive containers like lists and dicts, notify the UI when their content changes (not just when reassigned). + +--- + +## ๐Ÿงฉ Reactive Lists and Dicts + +```python +import flet as ft +from fletx.core.state import RxList +from fletx.widgets.obx import Obx + +tasks = RxList(["Learn FletX", "Write Docs"]) -One of the core pillars of FletX is its reactive widget system, built directly on top of Flet's native controls, thanks to powerful decorators like `@simple_reactive`, `@reactive_form`, etc.. +def main(page: ft.Page): + def add_task(e): + tasks.append(f"New Task {len(tasks) + 1}") + + page.add( + ft.Column([ + Obx(lambda: ft.Column([ft.Text(t) for t in tasks])), + ft.ElevatedButton("Add Task", on_click=add_task) + ]) + ) + +ft.app(target=main) +``` -#### ๐ŸŽฏ How does it work? +> ๐Ÿ’ก **Reactive Lists** track changes automatically. When you append or remove an item, the UI updates โ€” no manual refresh required. -When you decorate a class that extends a Flet control using `@simple_reactive`: +--- -- It creates a binding between Fletโ€™s properties (like `text`, `value`, `disabled`) and reactive objects like `RxStr`, `RxBool`, etc. -- It listens to those reactive variables and **automatically updates** the widget whenever they change. +## ๐Ÿงฎ Computed (Derived) Values -#### โœ… Example 1 โ€“ Creating a custom reactive widgets +Sometimes, a variable depends on others โ€” you can create a **computed** value that updates automatically. ```python -@simple_reactive( - bindings={ - 'value': 'text' # binds self.value() to the Flet Text's .text property - } -) -class MyReactiveText(ft.Text): - def __init__(self, value: RxStr, **kwargs): - self.value: RxStr = value - super().__init__(**kwargs) +import flet as ft +from fletx.core.state import RxInt, Computed +from fletx.widgets.obx import Obx + +price = RxInt(10) +quantity = RxInt(2) + +# Create a computed value that auto-updates +total = Computed(lambda: price.value * quantity.value) +def main(page: ft.Page): + page.add( + Obx(lambda: ft.Text(f"Total: ${total.value}")) + ) + +ft.app(target=main) -@two_way_reactive({ # Enables two way binding allowing ui to change - 'value': 'rx_value', # reactive object's value - 'visible': 'rx_visible', # (like Angular two way data binding system) - 'disabled': 'rx_disabled' # value <--> rx_value -}) -class ReactiveTextField(TextField): - """Example of two way Reactive TextField""" - - def __init__( - self, - rx_value: RxStr = RxStr(""), - rx_visible: RxBool = RxBool(True), - rx_disabled: RxBool = RxBool(False), - **kwargs - ): - # Define reactive properties - self.rx_value = rx_value - self.rx_visible = rx_visible - self.rx_disabled = rx_disabled - - super().__init__(**kwargs) ``` -**usage in a page** +> โœ… **Result:** Changing either `price` or `quantity` updates `total` instantly. + +--- + +## โš™๏ธ Batch Updates (Efficient State Mutations) + +When multiple reactive state updates happen together, FletX lets you group them efficiently using a batching context. + +This ensures updates trigger **only one UI refresh**, improving performance in reactive UIs. ```python -class MyPage(FletXPage): - def build(self): - self.counter = RxInt(0) +import asyncio +from fletx.core.state import RxInt +from fletx.decorators.reactive import reactive_batch + +count = RxInt(0) +double = RxInt(0) + +@reactive_batch() +def increment_both(): + count.value += 1 + double.value = count.value * 2 + +# Run inside an asyncio event loop +async def main(): + increment_both() + await asyncio.sleep(0) # allow batch to flush + print(count.value, double.value) + +asyncio.run(main()) - return ft.Column([ - ft.ElevatedButton("Increment", on_click=lambda _: self.counter.increment()), - MyReactiveText(value=self.counter) - ]) ``` -> The text content updates automatically each time the counter changes โ€” no manual update() needed. - -#### โœ… Example 2 โ€“ Reactive forms -```python -@reactive_form( - form_fields={ - 'email': 'rx_email', - 'password': 'rx_password', - }, - validation_rules={ - 'email': 'email_regex', # Will call self.email_regex with email input value - 'password': 'validate_pass', # Will call self.validate_pass with password input value - }, - on_submit = 'perform_submit', - on_submit_failed = 'show_erros', - auto_validate = True -) -class RegistrationForm(Column): - """Example of Reactive Form""" - + +> โœ… Only **one** UI refresh occurs after the batched updates โ€” improving performance. + +--- + +## ๐Ÿ“ `.value` vs. Direct Assignment + +Beginners often trip on this difference: + +```python +count = RxInt(0) + +# โœ… Correct - updates the reactive value +count.value = 10 + +# โŒ Wrong - breaks reactivity entirely +count = 10 +``` + +> โš ๏ธ **Warning:** Always update `.value`, never overwrite the reactive variable itself. + +**Another common mistake:** + +```python +count = RxInt(5) + +# โŒ Wrong - compares the object, not the value +if count > 3: + print("Greater than 3") + +# โœ… Correct - compares the actual value +if count.value > 3: + print("Greater than 3") +``` + +--- + +## ๐ŸŒ Async Operations + +FletX works great with async Python โ€” perfect for API calls and background tasks. + +```python +import flet as ft +import asyncio +from fletx.core.state import RxBool, RxStr +from fletx.core.controller import FletXController as Controller +from fletx.widgets.obx import Obx + + +class DataController(Controller): def __init__(self): - # Reactive Properties - self.rx_email = RxStr("") - self.rx_password = RxStr("") - - super().__init__(spacing=10) + super().__init__() + self.is_loading = RxBool(False) + self.data = RxStr("") + self.error = RxStr("") + + async def fetch_data(self): + self.is_loading.value = True + self.error.value = "" + try: + await asyncio.sleep(1) # simulate API call + self.data.value = "Data loaded!" + except Exception as e: + self.error.value = str(e) + finally: + self.is_loading.value = False + + +def main(page: ft.Page): + ctrl = DataController() + + def run_async_task(coro): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(coro) + + page.add( + ft.Column( + [ + Obx( + lambda: ft.Text( + "Loading..." + if ctrl.is_loading.value + else ctrl.data.value or f"Error: {ctrl.error.value}" + ) + ), + ft.ElevatedButton( + "Load", + on_click=lambda e: run_async_task(ctrl.fetch_data()), + ), + ] + ) + ) + + +ft.app(target=main) + +``` + +--- + +## ๐Ÿงช Testing State Logic - def validate_pass(self,value:str) -> bool: - """Example of password validation function""" - return True +FletX state is pure Python โ€” you can test it without a UI. - def email_regex(self,value): - """example of email validation function""" - import re - pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" - return re.match(pattern, value) is not None +#### Simple Test - def _build_form(self): - """Build form Controls.""" - - self.controls = [ - Text("Login Form", size = 24, weight = FontWeight.BOLD), - - ReactiveTextField( - label = "Your email", - rx_value = self.rx_email - ), - - ReactiveTextField( - label = "password", - password = True, - rx_value = self.rx_password - ), - ElevatedButton( - text = "Register for free", - on_click = lambda _: self.submit(), - disabled = lambda: not self.rx_is_valid.value - ), +```python +def test_counter(): + count = RxInt(0) + count.value += 1 + assert count.value == 1 +``` + +#### Testing Controllers + +```python +def test_controller(): + ctrl = CounterController() - ] + assert ctrl.count.value == 0 + ctrl.increment() + assert ctrl.count.value == 1 ``` +> ๐Ÿ’ก **Pro Tip:** This makes your controllers and reactive logic easily unit-testable โ€” no GUI needed. + --- -### ๐ŸŽฏ Side-effects and logic triggers +## โš ๏ธ Common Pitfalls -Reactive objects can also trigger **non-UI behaviors**: +### 1๏ธโƒฃ Breaking Reactivity with Reassignment ```python -self.ctrl.logged_in.listen(lambda value: navigate('/home') if value else None) +tasks = reactive_list([1, 2, 3]) + +# โŒ Wrong - loses reactivity +tasks = [4, 5, 6] + +# โœ… Correct - mutate in place +tasks.clear() +tasks.extend([4, 5, 6]) + +# โœ… Also correct +tasks[:] = [4, 5, 6] ``` -* Service calls -* Conditional business logic -* etc.. +### 2๏ธโƒฃ Creating Lambdas in Loops + +```python +# โŒ Wrong - all will show the same value +for i in range(5): + page.add(obx(lambda: ft.Text(f"{i}"))) + +# โœ… Correct - use a function factory +def make_text(index): + return lambda: ft.Text(f"{index}") + +for i in range(5): + page.add(obx(make_text(i))) +``` + +### 3๏ธโƒฃ Heavy Computation in obx() + +```python +# โŒ Bad - recalculates every render +obx(lambda: ft.Text(f"Result: {expensive_function(data.value)}")) + +# โœ… Better - use computed +result = computed(lambda: expensive_function(data.value)) +obx(lambda: ft.Text(f"Result: {result.value}")) +``` + +--- + +## ๐Ÿงญ Best Practices for State Management + +| โœ… Practice | ๐Ÿ’ฌ Why It Matters | +| ---------------------------------------------------- | -------------------------------- | +| Group related reactive variables inside a Controller | Keeps logic modular and testable | +| Use `computed()` for derived data | Automatically stays in sync | +| Wrap multiple updates with `@batch` | Reduces unnecessary rebuilds | +| Always mutate via `.value` | Prevents breaking reactivity | +| Keep `obx()` sections small | Improves UI performance | +| Use `watch()` for side effects only | Keeps UI updates clean | +| Dispose watchers when done | Prevents memory leaks | +| Use computed values for expensive operations | Caches results automatically | + +--- + +## โšก Quick Troubleshooting + +| Issue | Cause | Fix | +| -------------------- | ------------------------------ | ------------------------------- | +| UI not updating | Forgot `.value` | Always use `.value` | +| obx() not rebuilding | Logic outside lambda | Wrap reactive reads in `lambda` | +| Controller resets | Not stored in persistent scope | Keep reference at module level | +| Repeated rebuilds | Nested obx or frequent updates | Use `@batch` or reduce scope | +| Memory growing | Watchers not disposed | Call `.dispose()` on watchers | +| Wrong loop values | Lambda closure issue | Use function factory pattern | --- -### ๐Ÿ”ง Targeted Reactivity +## ๐Ÿ“Š Performance Guidelines -FletX gives you **fine-grained reactivity**, letting you update just a button, a field, or a panel โ€” without refreshing the entire page. This results in **better performance and user experience**. +| Metric | Recommendation | Notes | +| ---------------------------- | -------------- | ------------------------------- | +| Reactive vars per controller | 20-30 max | Split large controllers | +| `obx()` widgets per page | < 50 | Use `batch()` for bulk updates | +| `reactive_list` size | < 1000 items | Use pagination for larger lists | +| Nested `obx()` depth | Avoid nesting | Causes redundant rebuilds | +| Watcher callback duration | < 10ms | Move heavy work to async tasks | + +--- + +## ๐ŸŽฏ Real-World Pattern โ€” Todo App + +Here's a complete example showing everything working together: + +```python +import flet as ft +from fletx.core.controller import FletXController as Controller +from fletx.core.state import RxList, RxStr, Computed +from fletx.decorators.reactive import reactive_batch +from fletx.widgets.obx import Obx + + +class TodoController(Controller): + def __init__(self): + super().__init__() + self.todos = RxList([]) + self.filter = RxStr("all") + self._next_id = 1 + + # โœ… Computed values defined properly + self.filtered_todos = Computed(self._compute_filtered_todos) + self.active_count = Computed(self._compute_active_count) + + # -- Computed logic -- + def _compute_filtered_todos(self): + if self.filter.value == "active": + return [t for t in self.todos if not t["completed"]] + if self.filter.value == "completed": + return [t for t in self.todos if t["completed"]] + return list(self.todos) + + def _compute_active_count(self): + return sum(1 for t in self.todos if not t["completed"]) + + # -- Core actions -- + def add_todo(self, text): + if text.strip(): + self.todos.append({ + "id": self._next_id, + "text": text.strip(), + "completed": False, + }) + self._next_id += 1 + + def toggle_todo(self, todo_id): + for todo in self.todos: + if todo["id"] == todo_id: + todo["completed"] = not todo["completed"] + self.todos.notify() + break + + def delete_todo(self, todo_id): + self.todos[:] = [t for t in self.todos if t["id"] != todo_id] + + def clear_completed(self): + with reactive_batch(): + self.todos[:] = [t for t in self.todos if not t["completed"]] + + +def main(page: ft.Page): + ctrl = TodoController() + + def build_todo_list(): + return ft.Column([ + ft.Text(f"Active: {ctrl.active_count.value}"), + ft.Column([ + ft.Checkbox( + label=todo["text"], + value=todo["completed"], + on_change=lambda e, tid=todo["id"]: ctrl.toggle_todo(tid), + ) + for todo in ctrl.filtered_todos.value + ]) + ]) + + page.add(Obx(build_todo_list)) + + +ft.app(target=main) + +``` --- ## ๐Ÿง  Next Steps -* Explore [Controllers](controllers.md) -* Learn about the [Architecture](architecture.md) -* Dive into [dependency injection](guides/dependency-injection.md) \ No newline at end of file +Now that you've mastered **state management**: + +- Explore [Controllers](controllers.md) +- Learn about the [Architecture](architecture.md) +- Dive into [dependency injection](guides/dependency-injection.md)