Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/api/pythonnative.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ The reference is split per module so each page stays scannable:

| Area | Page | Key symbols |
|---|---|---|
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`SectionList`][pythonnative.SectionList], [`Modal`][pythonnative.Modal], [`Pressable`][pythonnative.Pressable], [`StatusBar`][pythonnative.StatusBar], [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView], [`RefreshControl`][pythonnative.RefreshControl], [`Picker`][pythonnative.Picker], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context], [`use_window_dimensions`][pythonnative.use_window_dimensions], [`use_safe_area_insets`][pythonnative.use_safe_area_insets], [`use_keyboard_height`][pythonnative.use_keyboard_height] |
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue] |
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`SectionList`][pythonnative.SectionList], [`Modal`][pythonnative.Modal], [`Pressable`][pythonnative.Pressable], [`StatusBar`][pythonnative.StatusBar], [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView], [`RefreshControl`][pythonnative.RefreshControl], [`Picker`][pythonnative.Picker], [`Fragment`][pythonnative.Fragment], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context], [`use_window_dimensions`][pythonnative.use_window_dimensions], [`use_safe_area_insets`][pythonnative.use_safe_area_insets], [`use_keyboard_height`][pythonnative.use_keyboard_height], [`memo`][pythonnative.memo] |
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue], [`use_animated_value`][pythonnative.use_animated_value] |
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
| Navigation | [Navigation](navigation.md) | [`NavigationContainer`][pythonnative.NavigationContainer], [`create_stack_navigator`][pythonnative.create_stack_navigator], [`create_tab_navigator`][pythonnative.create_tab_navigator], [`create_drawer_navigator`][pythonnative.create_drawer_navigator], [`use_navigation`][pythonnative.use_navigation] |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ convenience and are documented on their canonical pages:
| [`Element`][pythonnative.element.Element] | [Element](element.md) |
| [`ViewHandler`][pythonnative.native_views.base.ViewHandler] | [Native views](native_views.md) |
| [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`Color`][pythonnative.style.Color], [`Dimension`][pythonnative.style.Dimension], [`EdgeInsets`][pythonnative.style.EdgeInsets], [`EdgeValue`][pythonnative.style.EdgeValue], `FlexDirection`, `JustifyContent`, `Overflow`, `Position`, [`TransformSpec`][pythonnative.style.TransformSpec], [`style`][pythonnative.style.style] | [Style](style.md) |
| `parse_color_int`, `resolve_padding` | `pythonnative.native_views.base` |
| `parse_color_int` | `pythonnative.native_views.base` |

## Custom-component primitives

Expand Down
14 changes: 13 additions & 1 deletion docs/concepts/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ pn.Column(
- [`ErrorBoundary(child, fallback)`][pythonnative.ErrorBoundary]:
catches render errors in child and displays fallback.

**Composition:**

- [`Fragment(*children)`][pythonnative.Fragment]: group siblings into a
parent's child list without an extra wrapping view (analogous to
React's `<>…</>`).

**Lists:**

- [`FlatList(data, render_item, key_extractor, item_height, ...)`][pythonnative.FlatList]:
Expand All @@ -86,7 +92,7 @@ pn.Column(

**Platform UI:**

- [`StatusBar(style, background_color, hidden)`][pythonnative.StatusBar]:
- [`StatusBar(bar_style, background_color, hidden)`][pythonnative.StatusBar]:
configure the device's status bar (light/dark icons, color, hidden).
- [`KeyboardAvoidingView(*children, behavior)`][pythonnative.KeyboardAvoidingView]:
shift content up when the software keyboard appears.
Expand Down Expand Up @@ -249,6 +255,9 @@ hook state.
persists across renders. When passed via the `ref=` prop, the
reconciler populates `ref["current"]` with the underlying native
view.
- [`use_animated_value(initial)`][pythonnative.use_animated_value]:
stable [`AnimatedValue`][pythonnative.AnimatedValue] across renders;
the canonical way to drive `Animated.View`.
- [`use_context(context)`][pythonnative.use_context]: read from a
context provider.
- [`use_navigation()`][pythonnative.use_navigation]: navigation
Expand All @@ -263,6 +272,9 @@ hook state.
reactive safe-area insets.
- [`use_keyboard_height()`][pythonnative.use_keyboard_height]:
reactive software-keyboard height.
- [`@memo`][pythonnative.memo]: decorator that skips a function
component's re-render when its props are shallowly equal and its
internal state is unchanged.

### Custom hooks

Expand Down
37 changes: 37 additions & 0 deletions docs/concepts/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ render_count = pn.use_ref(0)
render_count["current"] += 1
```

### use_animated_value

Create an [`AnimatedValue`][pythonnative.AnimatedValue] that's stable
across renders. Equivalent to wrapping `pn.Animated.Value(initial)` in
`use_memo(..., [])` but more discoverable:

```python
opacity = pn.use_animated_value(0.0)
pn.Animated.timing(opacity, to=1.0, duration=300).start()
```

### use_context

Read a value from the nearest `Provider` ancestor:
Expand Down Expand Up @@ -267,6 +278,32 @@ automatically batched; the framework drains any pending re-renders
after effect flushing completes, so you don't need `batch_updates()`
inside effects.

## Memoizing function components

Wrap a function component with [`@pn.memo`][pythonnative.memo] to skip
its body when neither its props nor its internal state have changed:

```python
@pn.memo
@pn.component
def ExpensiveRow(label: str, value: int):
return pn.Row(
pn.Text(label, style={"flex": 1}),
pn.Text(str(value)),
)
```

When a `memo`'d component is reconciled, the reconciler compares the
new props against the previous props using shallow equality. If they
match and none of the component's `use_state` / `use_reducer` setters
have fired since the last render, the previously-rendered subtree is
reused and the component body is not re-executed. This is the
component-level equivalent of [`use_memo`][pythonnative.use_memo].

`memo` is typically used on pure, prop-driven leaves that re-render
frequently as part of a larger tree, e.g. rows inside a list whose
identity doesn't change between renders of the parent.

## Error boundaries

Wrap risky components in
Expand Down
13 changes: 7 additions & 6 deletions docs/guides/animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ frame.

## Mental model

1. Create an [`AnimatedValue`][pythonnative.AnimatedValue] using
[`use_memo`][pythonnative.use_memo] (so it survives re-renders).
1. Create an [`AnimatedValue`][pythonnative.AnimatedValue] with
[`use_animated_value`][pythonnative.use_animated_value] (so it
survives re-renders).
2. Bind the value into the `style` of an `Animated.View`,
`Animated.Text`, or `Animated.Image`.
3. Drive the value with `Animated.timing`, `Animated.spring`, or
Expand All @@ -31,7 +32,7 @@ import pythonnative as pn

@pn.component
def FadeInBox():
opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
opacity = pn.use_animated_value(0.0)

def _fade_in():
pn.Animated.timing(opacity, to=1.0, duration=400).start()
Expand All @@ -57,7 +58,7 @@ def FadeInBox():
```python
@pn.component
def Bouncy():
scale = pn.use_memo(lambda: pn.Animated.Value(1.0), [])
scale = pn.use_animated_value(1.0)

def _press():
pn.Animated.spring(scale, to=1.2, stiffness=200, damping=8).start()
Expand All @@ -79,8 +80,8 @@ animation property.
## Sequencing and parallel composition

```python
opacity = pn.Animated.Value(0.0)
translate_y = pn.Animated.Value(20.0)
opacity = pn.use_animated_value(0.0)
translate_y = pn.use_animated_value(20.0)

pn.Animated.parallel([
pn.Animated.timing(opacity, to=1.0, duration=300),
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/platform-accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Mount [`StatusBar`][pythonnative.StatusBar] anywhere in the tree (it
renders nothing visible) to control style and visibility:

```python
pn.StatusBar(style="light", background_color="#000000")
pn.StatusBar(bar_style="light", background_color="#000000")
```

`style` is `"light"` (light icons, dark background), `"dark"` (dark
Expand Down
2 changes: 1 addition & 1 deletion examples/hello-world/app/screens/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _view_showcase() -> None:

return pn.ScrollView(
pn.Column(
pn.StatusBar(style="dark"),
pn.StatusBar(bar_style="dark"),
pn.Text("Settings", style=styles["title"]),
pn.Text(f"PythonNative v{pn.__version__}", style=styles["subtitle"]),
pn.Text(
Expand Down
29 changes: 24 additions & 5 deletions examples/hello-world/app/screens/showcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@

@pn.component
def AnimatedCard() -> pn.Element:
"""Demonstrates ``Animated.View`` driven by ``AnimatedValue`` + ``use_memo``."""
opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
scale = pn.use_memo(lambda: pn.Animated.Value(0.9), [])
"""Demonstrates ``Animated.View`` driven by ``use_animated_value``."""
opacity = pn.use_animated_value(0.0)
scale = pn.use_animated_value(0.9)

def _enter() -> None:
pn.Animated.parallel(
Expand Down Expand Up @@ -63,8 +63,11 @@ def _enter() -> None:
)


@pn.memo
@pn.component
def TypographyDemo() -> pn.Element:
"""Wrapped in [`pn.memo`][pythonnative.memo] so it skips re-render when parent state changes."""
print("[TypographyDemo] render (should only appear once)")
return pn.Column(
pn.Text("Headline", style={"font_size": 28, "font_weight": "700"}),
pn.Text(
Expand All @@ -84,6 +87,7 @@ def TypographyDemo() -> pn.Element:
)


@pn.memo
@pn.component
def BordersAndShadows() -> pn.Element:
return pn.View(
Expand All @@ -96,6 +100,7 @@ def BordersAndShadows() -> pn.Element:
)


@pn.memo
@pn.component
def Chips() -> pn.Element:
return pn.Row(
Expand All @@ -112,6 +117,20 @@ def Chips() -> pn.Element:
)


def section_heading(title: str, hint: str) -> pn.Element:
"""Compose two sibling [`pn.Text`][pythonnative.Text] nodes via [`pn.Fragment`][pythonnative.Fragment].

Returning a Fragment from a plain helper (not a ``@pn.component``)
lets the surrounding parent (here a [`pn.Column`][pythonnative.Column])
flatten the siblings into its own child list without an extra
wrapper view.
"""
return pn.Fragment(
pn.Text(title, style=styles["section_title"]),
pn.Text(hint, style=styles["hint"]),
)


@pn.component
def ShowcaseScreen() -> pn.Element:
nav = pn.use_navigation()
Expand All @@ -133,10 +152,10 @@ def go_back() -> None:
pn.Column(
pn.Text(message, style=styles["title"]),
AnimatedCard(),
pn.Text("Typography", style=styles["section_title"]),
section_heading("Typography", "Memoized via @pn.memo; renders only once."),
TypographyDemo(),
BordersAndShadows(),
pn.Text("Chips", style=styles["section_title"]),
section_heading("Chips", "Composed via pn.Fragment without an extra container."),
Chips(),
pn.Pressable(
pn.View(
Expand Down
21 changes: 21 additions & 0 deletions scripts/start-android-emulator.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Start the local Android emulator used for testing pythonnative apps.
#
# Usage:
# ./scripts/start-android-emulator.sh [avd-name]
#
# Defaults to the "Medium_Phone" AVD. List available AVDs with:
# ~/Library/Android/sdk/emulator/emulator -list-avds

set -euo pipefail

AVD_NAME="${1:-Medium_Phone}"
EMULATOR_BIN="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}/emulator/emulator"

if [[ ! -x "$EMULATOR_BIN" ]]; then
echo "Error: emulator binary not found at $EMULATOR_BIN" >&2
echo "Set ANDROID_SDK_ROOT or install the Android SDK emulator." >&2
exit 1
fi

exec "$EMULATOR_BIN" -avd "$AVD_NAME"
44 changes: 43 additions & 1 deletion src/pythonnative/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,39 +55,59 @@ def App():

from . import sdk
from .alerts import Alert
from .animated import Animated, AnimatedValue
from .animated import Animated, AnimatedValue, use_animated_value
from .components import (
ActivityIndicator,
ActivityIndicatorProps,
Button,
ButtonProps,
Column,
ErrorBoundary,
FlatList,
Fragment,
Image,
ImageProps,
KeyboardAvoidingView,
KeyboardAvoidingViewProps,
Modal,
ModalProps,
Picker,
PickerProps,
Pressable,
PressableProps,
ProgressBar,
ProgressBarProps,
RefreshControl,
Row,
SafeAreaView,
SafeAreaViewProps,
ScrollView,
ScrollViewProps,
SectionList,
Slider,
SliderProps,
Spacer,
SpacerProps,
StatusBar,
StatusBarProps,
Switch,
SwitchProps,
Text,
TextInput,
TextInputProps,
TextProps,
View,
ViewProps,
WebView,
WebViewProps,
)
from .element import Element
from .hooks import (
Provider,
batch_updates,
component,
create_context,
memo,
use_callback,
use_context,
use_effect,
Expand Down Expand Up @@ -152,6 +172,7 @@ def App():
"Column",
"ErrorBoundary",
"FlatList",
"Fragment",
"Image",
"KeyboardAvoidingView",
"Modal",
Expand All @@ -171,13 +192,33 @@ def App():
"TextInput",
"View",
"WebView",
# Built-in Props dataclasses
"ActivityIndicatorProps",
"ButtonProps",
"ImageProps",
"KeyboardAvoidingViewProps",
"ModalProps",
"PickerProps",
"PressableProps",
"ProgressBarProps",
"SafeAreaViewProps",
"ScrollViewProps",
"SliderProps",
"SpacerProps",
"StatusBarProps",
"SwitchProps",
"TextInputProps",
"TextProps",
"ViewProps",
"WebViewProps",
# Core
"Element",
"create_screen",
# Hooks
"batch_updates",
"component",
"create_context",
"memo",
"use_callback",
"use_context",
"use_effect",
Expand Down Expand Up @@ -225,6 +266,7 @@ def App():
# Animation
"Animated",
"AnimatedValue",
"use_animated_value",
# Imperative
"Alert",
# Native modules
Expand Down
Loading
Loading