Metadata-Version: 2.4
Name: apkpy
Version: 1.0.0
Summary: Build native Android apps in pure Python — no Java, no Android Studio required.
Home-page: https://github.com/apkpy-project/repo-apkpy
Author: Martim
Project-URL: Source, https://github.com/apkpy-project/repo-apkpy
Project-URL: Changelog, https://github.com/apkpy-project/repo-apkpy/blob/main/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/apkpy-project/repo-apkpy/issues
Keywords: android,apk,python,mobile,app,framework,android-development,no-java,python-to-android,native-android,mobile-development,gui,sqlite,http,rest-api,hot-reload,compiler,build-tool
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Compilers
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Multimedia :: Graphics
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Requires-Dist: click
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# 🚀 ApkPy — Build Native Android Apps in Pure Python

Welcome to **ApkPy**, the revolutionary transpiler framework that empowers you to build lightning-fast, native Android applications using **only Python and CSS-style definitions**. We don't use heavy WebViews or slow runtime rendering engines. ApkPy parses your Python logic and directly generates pure **Android Java and XML components**, ready to be compiled into a `.apk`.

---

## 1. The Vision & Philosophy 🌟

Forget the steep learning curves of Java or Kotlin, and avoid the massive footprint of cross-platform engines. Why choose ApkPy over interpreted frameworks like Kivy or Flet?

- **Zero Bloat, Smaller APKs**: Because we transpile to pure Android projects, you avoid bundling heavy Python interpreters into your deployment.
- **Flawless Performance**: By generating true native `Activity`, `Button`, `ImageView`, and `EditText` elements under the hood, your app runs at the maximum possible speed the OS allows.
- **100% Native Look & Feel**: Because your components interact directly with the Android SDK, OS-level features like haptics, text-selection, and system animations are preserved automatically.

---

## 2. How it Works (The Technical Secret) 🛠️

**How it works:** ApkPy parses your Python code into an Abstract Syntax Tree (AST) and maps UI calls to their corresponding Android XML Layouts and Java Activity classes. It's not a simulation; it's code generation.

Here's what happens under the hood every time you run `apkpy build`:

1. **Parsing** — ApkPy reads your `writehere.py` file and builds a full AST.
2. **Component Mapping** — Each `label()`, `button()`, `inputs()`, `image()` call is translated to its native Android equivalent (`TextView`, `Button`, `EditText`, `ImageView`).
3. **CSS Translation** — Your `style = """ ... """` string is parsed and converted to Android XML drawables, `dp` values, and color attributes.
4. **Java/XML Generation** — Full Java Activity classes and XML layout files are generated from scratch.
5. **ZIP Packaging** — Everything is bundled into a `.zip` file you can open directly in Android Studio.

---

## 3. Full Installation & Setup 📥

Getting started is instantly accessible. You **do not** need any prior knowledge of Android Studio to start designing.

```bash
pip install apkpy
```
*(Need the new features? Run `pip install --upgrade apkpy`)*

### Your First App — Full CLI Workflow

```bash
# 1. Create a new project
apkpy start my_project

# 2. Enter your project folder
cd my_project

# 3. Open writehere.py and design your app
# (edit it with any editor — VS Code, PyCharm, Notepad, etc.)

# 4. Preview it instantly on your computer (no Android needed!)
python writehere.py

# 5. When you're happy, compile straight to an installable APK
apkpy run
# → No Android Studio needed — produces my_project-debug.apk right here
# → add --qr to install over Wi-Fi, or --usb to push it over a cable

# (Prefer Android Studio? `apkpy build` makes a .zip project to open there instead.)
```

> First run? Use `apkpy doctor` to check your toolchain (a JDK 17–21 + the Android
> SDK). If you have Android Studio, ApkPy reuses its bundled JDK/SDK automatically;
> otherwise run `apkpy setup` once to download them.

### 🎯 Jump-Start with a Ready-Made Example

Not sure where to begin? Use `apkpy examples` to instantly drop a complete, working app into any folder:

```bash
apkpy examples
```

```
What example do you want to use?

  [1] Hello World
  [2] Calculator
  [3] Notes
  [4] Settings
  [5] Login Screen
  [6] Location / GPS
  [7] Network Images
  [8] Loading Spinner
  [9] Secure Login
  [10] REST Client
  [11] DB Notes List

Enter a number: 3

Where do you want to create it? [.]: C:\Users\me\my_project

Done! "Notes" example created at:
  C:\Users\me\my_project\writehere.py

Preview it with:  python writehere.py
Build it with:    apkpy build
```

All 11 examples are fully working apps you can preview immediately and build for Android without changing a single line.

---

## 4. Complete Syntax Guide (v1.0.0 Standard) 🎨

ApkPy v1.0.0 introduces scrollable screens, a native list component, and a full suite of bug fixes — plus all the inputs added in v0.9.8–0.9.9.

### The Screen
Everything belongs to a `Screen`. Screens translate directly to native Android Activities.
```python
login_screen = Screen(id="login_container")
```

Pass `scroll=True` to make the whole screen vertically scrollable — all components, including long lists, will scroll together as one page:
```python
home = Screen(id="home", scroll=True)
```

### The Container (Nesting)
Containers allow you to group components together, creating complex layouts like cards, headers, or custom grids.
```python
# Create a container on the screen
header_box = container(id="header_style", screen=login_screen)

# Add a label inside that container
label("Welcome Back", parent=header_box)
```

### The CSS Engine
Say goodbye to complex dictionary configurations. ApkPy uses a multi-line string approach with standard CSS syntax. No quotes are needed around values!

```python
style = """
login_container {
    gap: 15px;
    flex-direction: column;
}
"""
```

### New Layout & Styling Features (v0.9.3+)
Our Android-generation engine has been completely rewritten to use **XML-driven layouts**, ensuring 1:1 parity between your code and the native Android rendering.
- **`gap: 20px;`** – Automatically places calculated `dp` margins between elements in your layout container.
- **`flex-direction: row; / column;`** – Orients your components mapped directly to `android:orientation="horizontal"` or `"vertical"`.
- **`padding: 10px 20px;`** – Pushes text content away from your component edges.
- **`border-radius: 15px;`** – Generates native XML shape drawables for smooth rounded corners.
- **`border-color: #000;`** and **`border-width: 2px;`** – Easily define component outlines.
- **`pressed-color: #cccccc;`** – Defines the feedback color when a button is clicked (native `<selector>`).
- **`focus-border-color: #2196F3;`** – When a user taps your inputs, ApkPy dynamically swaps the border colors via native XML states!
- **`font-size: 18px;`** – Set text size in pixels (automatically converted to `sp` for Android).
- **`font-weight: bold;`** – Makes your text thicker. Supports `bold` or numeric values (`700`, `800`, `900`).
- **`font-family: 'sans-serif';`** – Choose between native font styles.

### Typography Support 🔠
ApkPy ensures your text looks great and consistent across platforms by mapping CSS generic families to native system fonts:
- **`sans-serif`**: The modern look. Maps to **Roboto** (Android) and **Segoe UI** (Windows) or **Helvetica** (macOS).
- **`serif`**: The classic look. Maps to **Times New Roman**.
- **`monospace`**: Perfect for code or alignment. Maps to **Consolas** (Windows) or **Courier**.

*Note: You can also use `font-style: italic;` to add emphasis to your labels.*

---

## 5. Component & Logic Workflow ⚙️

Adding elements and routing interactions is declarative and incredibly clean.

**Adding Components:**
Simply call the component and attach it to your screen. Give it an `id` to map it to your CSS string.
```python
btn = button("Login", id="primary_btn", screen=login_screen)
inputs("Your email", type="text", id="email_input", screen=login_screen)
```

**Nesting with `parent`:**
Every component (`label`, `button`, `inputs`, `container`) now supports a `parent` parameter. If set, the component will be rendered inside that container instead of directly on the screen.
```python
card = container(id="card_bg", screen=my_screen)
label("Title", parent=card)
button("Click Me", parent=card)
```

**Supported Input Types:**

| Type | What it does | How to use |
| :--- | :--- | :--- |
| `"text"` | Single-line text field | `inputs("Placeholder", type="text", id="…", screen=…)` |
| `"password"` | Text field that hides what you type | same as text |
| `"search"` | Text field with a ✕ clear button | same as text |
| `"textarea"` | Multi-line text area | same; add `rows: 6;` in CSS to control height |
| `"checkbox"` | A tick-box (checked = `"true"`, unchecked = `"false"`) | `inputs("Accept terms", type="checkbox", …)` |
| `"switch"` | A toggle switch — ideal for on/off settings | `inputs("Dark mode", type="switch", …)` |
| `"range"` | A slider from 0 to 100 | `inputs("Volume", type="range", …)` |
| `"radio"` | Pick one option from a group | `inputs("A\|B\|C", type="radio", …)` |
| `"select"` | Dropdown / spinner — pick one from a menu | `inputs("A\|B\|C", type="select", …)` |

**How get_value() and set_value() work for each type:**

| Type | `get_value()` returns | `set_value()` accepts |
| :--- | :--- | :--- |
| `text` / `password` / `search` / `textarea` | The text the user typed | Any string |
| `checkbox` / `switch` | `"true"` or `"false"` | `"true"` or `"false"` |
| `range` | A number between `"0"` and `"100"` | A number string, e.g. `"75"` |
| `radio` | The text of the selected option | The exact text of the option to select |
| `select` | The text of the selected option | The exact text of the option to select |

**CSS properties specific to `type="switch"`:**
- `color:` — the color of the label text next to the toggle
- `background-color:` — the color of the track when the switch is **ON** (defaults to green `#4CAF50` if not set)

```python
# Example: settings screen with two toggle switches
sw_dark  = inputs("Dark mode",     type="switch", id="sw_dark",  screen=settings)
sw_notif = inputs("Notifications", type="switch", id="sw_notif", screen=settings)

# Reading state
is_dark = sw_dark.get_value()   # → "true" or "false"

# Restoring saved state on startup
saved = storage.get("dark_mode", "false")
sw_dark.set_value(saved)  # pass "true" to turn it on, "false" to turn it off
```

```css
sw_dark {
    color: #F8FAFC;              /* label text color */
    background-color: #4F46E5;  /* track color when ON */
}
```

**Linking Logic & Navigation:**
Instead of fighting with Android `Intent` classes, handle navigation with a single method.
```python
login_screen.on_click_navigate(button=btn, to=dashboard_screen)
```

**The Application Lifecycle:**
Your file must end by invoking the execution layer with `run()`.
```python
run(start_screen=login_screen)
```

---

## 5a. Dropdown (Select) 🔽

The `type="select"` input creates a native **dropdown menu** — the user taps it, a list of options slides out, and they pick one. Perfect for language selectors, country pickers, category filters, and any setting where there are more than 2-3 options.

### How to use it

Pass the options as a single string separated by `|` (pipe character):

```python
from apkpy_lib import Screen, inputs, button, label, toast, storage, run

settings = Screen(id="settings")

# Options separated by |
language = inputs("Português|English|Español|Français", type="select", id="lang", screen=settings)
country  = inputs("Portugal|Brasil|Spain|France|Mexico", type="select", id="country", screen=settings)

# Auto-load the last saved choice
saved_lang    = storage.get("lang", "")
saved_country = storage.get("country", "")
if saved_lang    != "": language.set_value(saved_lang)
if saved_country != "": country.set_value(saved_country)

def save():
    storage.set("lang",    language.get_value())   # e.g. "Português"
    storage.set("country", country.get_value())    # e.g. "Portugal"
    toast("Saved!")

button("SAVE", id="btn", command=save, screen=settings)

if __name__ == "__main__":
    run(start_screen=settings)
```

### CSS styling

```css
lang {
    color: #F8FAFC;              /* text color of the selected item */
    background-color: #1E293B;  /* background of the dropdown box */
    border-color: #334155;      /* border around the box */
    border-width: 1px;
    border-radius: 10px;        /* rounded corners */
}
```

### Behaviour

| Method | What it does |
| :--- | :--- |
| `language.get_value()` | Returns the currently selected option, e.g. `"English"` |
| `language.set_value("English")` | Selects the option with that exact text |

> [!TIP]
> The **first option in the list** is selected by default when the app starts. Use `set_value()` at startup to restore the user's last saved choice.

---

## 5b. Multi-line Textarea 📝

The `type="textarea"` input creates a **multi-line text field** — just like a text input, but it grows vertically and lets the user write paragraphs, notes, or long descriptions.

### How to use it

```python
from apkpy_lib import Screen, inputs, button, label, toast, storage, run

my_screen = Screen(id="notes")

note = inputs("Write your note here...", type="textarea", id="note_field", screen=my_screen)

# Auto-load saved note
saved = storage.get("note", "")
if saved != "":
    note.set_value(saved)

def save():
    text = note.get_value()
    storage.set("note", text)
    toast("Note saved!")

button("SAVE NOTE", id="btn", command=save, screen=my_screen)

if __name__ == "__main__":
    run(start_screen=my_screen)
```

### CSS styling — control the height with `rows`

```css
note_field {
    rows: 6;                    /* height in lines (default is 4) */
    background-color: #1E293B;
    color: #F8FAFC;
    border-color: #334155;
    border-radius: 10px;
    border-width: 1px;
    padding: 14px;
}
```

### Behaviour

| Method | What it does |
| :--- | :--- |
| `note.get_value()` | Returns everything the user typed as a string |
| `note.set_value("Hello!")` | Replaces the full content of the textarea |

> [!TIP]
> Use `rows` in the CSS to make the field taller. `rows: 4` (default) is good for short messages, `rows: 8` or more for longer content like diary entries or descriptions.

---

## 5c. Toggle Switch 🔘

The `type="switch"` input creates a native **on/off toggle switch** — the kind you see in every Android settings screen. Much more natural than a checkbox for settings like "Dark mode", "Notifications", or "Auto-save".

### How to use it

```python
from apkpy_lib import Screen, inputs, button, label, toast, storage, run

settings = Screen(id="settings")

label("App Settings", id="title", screen=settings)

sw_dark  = inputs("Dark mode",      type="switch", id="sw_dark",  screen=settings)
sw_notif = inputs("Notifications",  type="switch", id="sw_notif", screen=settings)
sw_sound = inputs("Sound effects",  type="switch", id="sw_sound", screen=settings)

# Auto-load saved state when the app opens
if storage.get("dark",  "") != "": sw_dark.set_value(storage.get("dark",  ""))
if storage.get("notif", "") != "": sw_notif.set_value(storage.get("notif", ""))
if storage.get("sound", "") != "": sw_sound.set_value(storage.get("sound", ""))

def save():
    storage.set("dark",  sw_dark.get_value())   # "true" or "false"
    storage.set("notif", sw_notif.get_value())
    storage.set("sound", sw_sound.get_value())
    toast("Settings saved!")

button("SAVE", id="btn", command=save, screen=settings)

style = """
settings { background-color: #0F172A; padding: 24px; gap: 12px; }
title    { color: #F8FAFC; font-size: 20px; font-weight: bold; margin-bottom: 8px; }
sw_dark  { color: #F8FAFC; background-color: #4F46E5; }
sw_notif { color: #F8FAFC; background-color: #4F46E5; }
sw_sound { color: #F8FAFC; background-color: #4F46E5; }
btn      { background-color: #4F46E5; color: #F8FAFC; border-radius: 12px; font-weight: bold; padding: 14px; pressed-color: #3730A3; }
"""

if __name__ == "__main__":
    run(start_screen=settings)
```

### CSS styling

```css
sw_dark {
    color: #F8FAFC;              /* color of the label text */
    background-color: #4F46E5;  /* color of the track when the switch is ON */
}
```

Two CSS properties control the switch appearance:
- **`color`** — the label text next to the toggle (e.g. "Dark mode")
- **`background-color`** — the track color when the switch is **ON**. When OFF, the track is always grey. Defaults to `#4CAF50` (Material green) if not set.

### Behaviour

| Method | What it does |
| :--- | :--- |
| `sw_dark.get_value()` | Returns `"true"` if the switch is ON, `"false"` if OFF |
| `sw_dark.set_value("true")` | Turns the switch ON |
| `sw_dark.set_value("false")` | Turns the switch OFF |

> [!TIP]
> Always save and reload the switch state with `storage` so the user's settings are remembered between sessions. See the Storage section for the full pattern.

---

## 5d. Number Input 🔢

The `type="number"` input creates a text field that **only accepts numeric values** — on Android it opens the numeric keyboard automatically, so the user never has to switch. Works for integers, decimals, and negative numbers.

### How to use it

```python
from apkpy_lib import Screen, inputs, button, label, toast, run

my_screen = Screen(id="calculator")

inp_a = inputs("First number...",  type="number", id="inp_a", screen=my_screen)
inp_b = inputs("Second number...", type="number", id="inp_b", screen=my_screen)
result_lbl = label("Result: —", id="result", screen=my_screen)

def calculate():
    a = inp_a.get_value()   # always a string, e.g. "3.5"
    b = inp_b.get_value()   # e.g. "2"
    if a != "" and b != "":
        total = float(a) + float(b)
        result_lbl.set_value(f"Result: {total}")
    else:
        toast("Fill in both numbers!")

button("ADD", id="btn", command=calculate, screen=my_screen)

if __name__ == "__main__":
    run(start_screen=my_screen)
```

### CSS styling

```css
inp_a {
    color: #F8FAFC;
    background-color: #1E293B;
    border-color: #334155;
    border-width: 1px;
    border-radius: 10px;
}
```

Same CSS properties as a regular text input — `color`, `background-color`, `border-color`, `border-width`, `border-radius`, `padding`, `font-size`.

### Behaviour

| Method | What it does |
| :--- | :--- |
| `inp_a.get_value()` | Returns the typed value as a **string**, e.g. `"3.5"` or `"-10"` |
| `inp_a.set_value("42")` | Pre-fills the field with that value |

> [!IMPORTANT]
> `get_value()` always returns a **string** (like all other input types). Convert to `int()` or `float()` in your Python code before doing arithmetic.

> [!TIP]
> In the **Hot Previewer**, the field rejects any non-numeric character as you type — matching the numeric keyboard behaviour on Android. Decimals (`.`) and negatives (`-`) are allowed.

---

## 5e. Date & Time Pickers 📅🕐

The `type="date"` and `type="time"` inputs open the **native Android date/time dialogs** — the same polished pickers the user already knows from their phone's Calendar or Clock app. No custom UI to build.

### How to use them

```python
from apkpy_lib import Screen, inputs, button, label, toast, storage, run

my_screen = Screen(id="booking")

label("Book your appointment", id="title", screen=my_screen)

inp_date = inputs("Select date...", type="date", id="inp_date", screen=my_screen)
inp_time = inputs("Select time...", type="time", id="inp_time", screen=my_screen)

# Auto-load saved values
saved_date = storage.get("date", "")
saved_time = storage.get("time", "")
if saved_date != "": inp_date.set_value(saved_date)
if saved_time != "": inp_time.set_value(saved_time)

def confirm():
    date = inp_date.get_value()   # e.g. "25/12/2024"
    time = inp_time.get_value()   # e.g. "14:30"
    if date == "" or time == "":
        toast("Please select a date and time!")
        return
    storage.set("date", date)
    storage.set("time", time)
    toast(f"Booked for {date} at {time}!")

button("CONFIRM", id="btn", command=confirm, screen=my_screen)

if __name__ == "__main__":
    run(start_screen=my_screen)
```

### CSS styling

```css
inp_date {
    color: #F8FAFC;
    background-color: #1E293B;
    border-color: #334155;
    border-width: 1px;
    border-radius: 10px;
}

inp_time {
    color: #F8FAFC;
    background-color: #1E293B;
    border-color: #334155;
    border-width: 1px;
    border-radius: 10px;
}
```

Same CSS properties as any other input — `color`, `background-color`, `border-color`, `border-width`, `border-radius`, `padding`.

### Behaviour

| Method | What it does |
| :--- | :--- |
| `inp_date.get_value()` | Returns `""` if nothing selected yet, or `"DD/MM/YYYY"` after the user picks a date |
| `inp_date.set_value("25/12/2024")` | Pre-fills the field with that date |
| `inp_time.get_value()` | Returns `""` if nothing selected yet, or `"HH:MM"` after the user picks a time |
| `inp_time.set_value("14:30")` | Pre-fills the field with that time |

> [!TIP]
> Always check `get_value() != ""` before using the result — if the user hasn't tapped the picker yet, it returns an empty string.

> [!NOTE]
> In the **Hot Previewer**, clicking the field opens a small dialog with spinboxes for day/month/year (or hour/minute). On Android it opens the native `DatePickerDialog` / `TimePickerDialog`. Your code is 100% identical for both.

---

## 5f. Scrollable Screens 📜

Add `scroll=True` to any `Screen` to make it vertically scrollable. Every component on that screen — labels, inputs, lists, buttons — scrolls together as a single page. This is the right approach for forms, logs, or any screen where content might overflow the device height.

### How to use it

```python
from apkpy_lib import Screen, label, inputs, button, list_view, run

home = Screen(id="home", scroll=True)

label("Daily Check-in", id="title", screen=home)
inputs("How are you feeling?", type="textarea", id="notes", screen=home)
button("SAVE", id="btn", command=save, screen=home)
# ... add as many components as you need — they all scroll together
```

### In the Hot Previewer
The screen becomes a scrollable canvas. Scroll with the **mouse wheel** from anywhere on the screen — no need to hover over a specific element.

### On Android
The screen compiles to a `NestedScrollView` wrapping a `LinearLayout`. All components (including `list_view`) use native `TextView` containers instead of `ListView`, so there's no conflict between nested scrollable views and the outer scroll.

> [!TIP]
> `scroll=True` and `list_view` work seamlessly together. The list items become part of the scrollable page — you get infinite scroll for free, without any fixed-height list box.

---

## 5g. List View 📋

`list_view` renders a vertical list of items with an optional click handler. Items can be plain strings or dicts with `"title"` and `"subtitle"` keys.

### How to use it

```python
from apkpy_lib import Screen, list_view, toast, run

home = Screen(id="home", scroll=True)

items = [
    {"title": "08/06/2026", "subtitle": "Mood: Great — Energy: 85"},
    {"title": "07/06/2026", "subtitle": "Mood: Good — Energy: 70"},
    {"title": "06/06/2026", "subtitle": "Mood: Okay — Energy: 55"},
]

history = list_view(
    items,
    id="history_list",
    screen=home,
    on_click=lambda item: toast(item["title"] + ": " + item["subtitle"]),
)

if __name__ == "__main__":
    run(start_screen=home)
```

### Updating the list at runtime

Call `.set_items(new_list)` on the list view variable to replace its contents — for example after saving a new entry:

```python
def save():
    items.insert(0, {"title": "today", "subtitle": "Mood: Great"})
    history.set_items(items)
    toast("Saved!")
```

### Feeding the list from a database or API (`set_items` + JSON) 📊

`set_items` also accepts a **JSON string** directly — exactly what `db.query()` returns and what most REST APIs respond with. Use `title=` and `subtitle=` to pick which field of each JSON object to display:

```python
def refresh():
    rows = db.query("SELECT content, created FROM notes ORDER BY id DESC")
    notes_list.set_items(rows, title="content", subtitle="created")
```

The same works with an API response:

```python
def on_response(success, response):
    if success:
        # e.g. Supabase: [{"name": "Alice", "email": "a@x.com"}, ...]
        users_list.set_items(response, title="name", subtitle="email")
```

- Each row renders as `title — subtitle` (subtitle omitted when empty).
- Arrays of plain values (`["a", "b"]` as JSON) also work — each value becomes one row.
- Invalid JSON results in an empty list instead of a crash.
- Calling a refresh function at **module level** (e.g. `refresh()` at the bottom of the file) runs it on app start — in the Previewer and in the Android `onCreate` — so the list is filled when the app opens.

This closes the **data → UI** loop: `db.query`/`https` fetch the rows, `set_items` shows them, `on_click` + `on_click_navigate(data=...)` handle the taps.

### CSS styling

```css
history_list {
    background-color: #1E293B;   /* background of each item */
    color: #F8FAFC;              /* text color */
    border-color: #334155;       /* color of the divider lines between items */
    height: 200px;               /* fixed height when scroll=False; ignored in scroll screens */
}
```

| CSS property | What it controls |
| :--- | :--- |
| `color` | Text color of each list item |
| `background-color` | Background color of each item row |
| `border-color` | Color of the 1dp divider between rows |
| `height` | Fixed height of the list box (only applies when `scroll=False`). **Avoid using a tall fixed height with few items** — on Android, the empty space below the last item is filled with `background-color`, creating a visible coloured box. Either omit `height` (list sizes to its content) or use `scroll=True` on the screen instead. See example below. |

### Height warning

```css
/* ❌ Avoid — creates a 400dp coloured box even with only 3 items */
my_list {
    background-color: #1E293B;
    height: 400px;
}

/* ✅ Correct — list wraps to fit its items */
my_list {
    background-color: #1E293B;
}
```

### `on_click` lambda

The `on_click` parameter accepts an inline lambda. The argument `item` is the full string stored internally — for dict items it's `"title — subtitle"`:

```python
on_click=lambda item: toast(item["title"] + ": " + item["subtitle"])
```

`item["title"]` extracts everything before the ` — ` separator; `item["subtitle"]` extracts everything after it.

> [!NOTE]
> In scroll screens, `list_view` compiles to `TextViews` inside a `LinearLayout` rather than a native `ListView`. This avoids the classic Android conflict between two nested scrollable containers, and is the same approach used by apps like Gmail and WhatsApp for chat lists inside scroll views.

---

## 5h. Rich Lists, Carousels & Grids 🖼️

For media-heavy apps, ApkPy supports cards with a title, subtitle, remote image and optional `src` payload:

```python
tracks = [{
    "title": "Midnight Drive",
    "subtitle": "Neon Avenue",
    "image": "https://example.com/cover.jpg",
    "src": "https://example.com/song.mp3",
}]

def play_track(item):
    audio.play_background(item["src"], title=item["title"],
                          artist=item["subtitle"], art=item["image"])

results = list_view(tracks, screen=home, rich=True, on_click=play_track)
recent = carousel(tracks, screen=home, on_click=play_track)
genres = grid(tracks, screen=home, cols=2, on_click=play_track)
```

- `list_view(..., rich=True)` renders thumbnail + title + subtitle rows.
- `carousel(...)` renders a horizontally scrolling shelf of cards.
- `grid(..., cols=N)` renders a native N-column grid.
- `set_items(json, title=..., subtitle=..., image=...)` maps API or SQLite fields into rich rows.
- The complete dict is passed to `on_click`, including fields such as `src` that are not directly visible.

Remote images load asynchronously and declare Android's `INTERNET` permission automatically.

---

## 5i. Audio, Queues & Background Playback 🎵

ApkPy's `audio` API is designed for music, radio and podcast apps. Basic playback is available through `audio.play(src)`, `pause()`, `resume()`, `stop()` and `seek(seconds)`.

Use `play_background` for a track that must continue when the app leaves the foreground:

```python
audio.play_background(
    "https://example.com/song.mp3",
    title="Midnight Drive",
    artist="Neon Avenue",
    art="https://example.com/cover.jpg",
)
```

On Android this generates a foreground media service with a native media notification, lock-screen metadata and transport controls. It also handles audio focus, including pause, duck and resume behaviour.

### Playlists, next/previous, shuffle and repeat

```python
audio.play_playlist(
    ["https://example.com/one.mp3", "https://example.com/two.mp3"],
    titles=["First Track", "Second Track"],
    artists=["Example Band", "Example Band"],
    arts=["https://example.com/one.jpg", "https://example.com/two.jpg"],
    start=0,
)

audio.next()
audio.previous()
audio.shuffle()
audio.repeat()
```

`start` may also be the selected source URL, which makes a rich-list or carousel tap start the full queue at that item.

### Bind a full player UI

```python
progress = inputs("", type="range", screen=player)
time = label("0:00 / 0:00", screen=player)
cover = image("", screen=player)
title = label("", screen=player)
artist = label("", screen=player)
play_pause = button("▶", screen=player)
shuffle = button("Shuffle", screen=player)
repeat = button("Repeat", screen=player)

audio.now_playing(progress=progress, time=time, cover=cover,
                  title=title, artist=artist)
audio.controls(play_pause=play_pause, shuffle=shuffle, repeat=repeat)
```

`now_playing` keeps the components synchronized with the media service; moving the bound range input seeks through the track. `controls` updates ordinary buttons to reflect the current play/pause, shuffle and repeat state.

### Persistent mini-player

```python
mini_player(open=player)
```

The mini-player appears above `bottom_nav` across the app, follows the current track and opens the chosen full-player screen when tapped.

### Favourites and user playlists

```python
audio.like_button(like_btn, liked="❤️", unliked="🤍")
audio.liked_list(liked_tracks)
audio.add_to_playlist("Road trip")
audio.play_saved_playlist("Road trip")
audio.playlists_list(playlists)
audio.edit_playlist("Road trip")
audio.playlist_editor(editor)
audio.remove_from_playlist("Road trip")
audio.delete_playlist("Road trip")
```

These APIs persist the track source, title, artist and cover automatically. Bound lists refresh when their screens resume.

---

## 5j. Offline Downloads 📥

The `files` API downloads files asynchronously into app-private storage:

```python
def downloaded(success, path):
    if success:
        audio.play(path)
    else:
        toast("Download failed")

files.download("https://example.com/song.mp3", "song.mp3", on_result=downloaded)

if files.exists("song.mp3"):
    audio.play(files.path("song.mp3"))

files.delete("song.mp3")
```

No broad Android storage permission is required.

---

## 5k. OAuth Login — Google, Spotify & GitHub 🔐

`auth` implements OAuth 2.0 Authorization Code flow with PKCE and includes provider defaults for Google, Spotify and GitHub:

```python
def signed_in(user):
    name.set_value(user["name"])

auth.login(
    provider="spotify",
    client_id="YOUR_SPOTIFY_CLIENT_ID",
    scopes=["user-read-email", "user-read-private"],
    then=home,
)

auth.user(on_result=signed_in)   # {"name", "email", "picture"}
token = auth.token()
logged_in = auth.is_logged_in()
auth.logout()
```

Android returns from the browser through `apkpy://auth`; the Previewer uses a loopback URL such as `http://127.0.0.1:8888/callback`. Register both redirects with the provider. PKCE avoids embedding a client secret in the APK.

> ApkPy provides the UI, playback and authentication primitives for a Spotify-style app, but does not bundle Spotify's catalogue. Only use tracks, artwork and APIs that you are authorized to access.

---

## 5l. Bottom Navigation Bar 🗂️

`bottom_nav` adds a native bottom tab bar that links multiple screens together — the standard Android pattern for apps with 2–5 top-level sections (think Instagram, WhatsApp, YouTube).

### Basic usage

```python
from apkpy_lib import Screen, label, bottom_nav, run

home     = Screen(id="home")
explore  = Screen(id="explore")
profile  = Screen(id="profile")

label("Home screen",    id="lbl1", screen=home)
label("Explore screen", id="lbl2", screen=explore)
label("Profile screen", id="lbl3", screen=profile)

bottom_nav(
    [home, explore, profile],
    labels=["Home", "Explore", "Profile"],
    icons=["home", "search", "person"]
)

if __name__ == "__main__":
    run(start_screen=home)
```

### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `screens` | `list[Screen]` | Screens to link — 2 to 5 |
| `labels`  | `list[str]` | Tab label text. Defaults to the screen `id` if omitted |
| `icons`   | `list[str]` | Icon name per tab. Defaults to `"circle"` if omitted |

### Available icons

`home` · `person` · `settings` · `search` · `list` · `add` · `star` · `bell` · `chart` · `message` · `heart` · `camera` · `info` · `circle`

### Complete example — 3-screen app

```python
from apkpy_lib import Screen, label, button, inputs, list_view, toast, bottom_nav, run

# ── Screens ───────────────────────────────────────────
home     = Screen(id="home")
explore  = Screen(id="explore", scroll=True)
profile  = Screen(id="profile")

# ── Home ──────────────────────────────────────────────
label("Dashboard", id="dash_title", screen=home)
button("Get started", id="btn_start", command=lambda: toast("Let's go!"), screen=home)

# ── Explore (scrollable with a list) ──────────────────
label("Trending", id="exp_title", screen=explore)
explore_list = list_view(
    [
        {"title": "Morning Routine",   "subtitle": "10 min · Productivity"},
        {"title": "Deep Focus",        "subtitle": "25 min · Work"},
        {"title": "Evening Wind-Down", "subtitle": "15 min · Wellness"},
    ],
    id="exp_list",
    screen=explore,
    on_click=lambda item: toast(item["title"])
)

# ── Profile ───────────────────────────────────────────
label("Alex Johnson",        id="p_name",  screen=profile)
label("alex@example.com",    id="p_email", screen=profile)
button("Log out", id="btn_logout", command=lambda: toast("Bye!"), screen=profile)

# ── Bottom nav ────────────────────────────────────────
bottom_nav(
    [home, explore, profile],
    labels=["Home", "Explore", "Profile"],
    icons=["home", "search", "person"]
)

style = """
home    { background-color: #0F172A; padding: 24px; gap: 16px; }
explore { background-color: #0F172A; padding: 24px; gap: 14px; }
profile { background-color: #0F172A; padding: 32px; gap: 12px; }

dash_title { color: #F8FAFC; font-size: 26px; font-weight: bold; }
btn_start  { background-color: #6366F1; color: #FFFFFF; border-radius: 12px; padding: 14px; }

exp_title { color: #F8FAFC; font-size: 22px; font-weight: bold; }
exp_list  { background-color: #1E293B; color: #F8FAFC; border-color: #334155; height: 400px; }

p_name    { color: #F8FAFC; font-size: 22px; font-weight: bold; }
p_email   { color: #94A3B8; font-size: 14px; }
btn_logout { background-color: #1E293B; color: #F87171; border-radius: 12px; padding: 14px; }
"""

if __name__ == "__main__":
    run(start_screen=home)
```

### How it works on Android

- Each screen gets a `RelativeLayout` wrapper so content always sits **above** the bar — no content is hidden behind it.
- A single `res/menu/bottom_nav_menu.xml` is generated and shared by all screens.
- Each icon is an auto-generated `res/drawable/ic_nav_*.xml` vector — **no image assets or icon fonts needed**.
- Tab switches use `FLAG_ACTIVITY_REORDER_TO_FRONT` — existing Activities are reused, not recreated. Switching tabs is instant.
- `overridePendingTransition(0, 0)` removes the default slide animation so tabs feel like tabs, not page navigation.
- Each Activity overrides `onResume()` to restore the correct selected tab — so pressing the system back button and returning to a screen always shows the right tab highlighted.
- Active tab icon/label: **white** (`#FFFFFF`). Inactive: grey (`#64748B`). Bar background: `#1E293B`.

### Hot Previewer

Renders a 56 px dark bar at the bottom with one column per tab. The active tab shows a coloured indicator line and bold white label; inactive tabs are grey. Clicking any tab navigates instantly.

> [!NOTE]
> Call `bottom_nav` once, at the top level — outside any screen or function. It automatically applies to every screen in the list.

---

## 5m. Passing Data Between Screens 📦

Pass values from one screen to another using `on_click_navigate(screen, data={...})` and `screen.get_param("key")`. This works with both button clicks and list taps — on Android it compiles to `Intent.putExtra` / `getIntent().getStringExtra`; in the Hot Previewer it uses an in-memory dictionary.

### Quick example

```python
from apkpy_lib import Screen, label, button, list_view, on_click_navigate, run

list_screen  = Screen(id="list_screen")
detail_screen = Screen(id="detail_screen")

items = [
    {"title": "Apple",  "subtitle": "A red fruit"},
    {"title": "Banana", "subtitle": "A yellow fruit"},
]

# Tap a list item → navigate and pass data
item_list = list_view(
    items,
    id="item_list",
    screen=list_screen,
    on_click=lambda item: on_click_navigate(detail_screen, data={"title": item["title"], "desc": item["subtitle"]})
)

# Detail screen: read the incoming data
lbl_name = label("", id="lbl_name", screen=detail_screen)
lbl_desc = label("", id="lbl_desc", screen=detail_screen)

# Populate labels on load using get_param
lbl_name.set_value(detail_screen.get_param("title"))
lbl_desc.set_value(detail_screen.get_param("desc"))

# Optional default value if the param is missing
lbl_name.set_value(detail_screen.get_param("title", "Unknown"))
```

You can also pass data via a regular button click:

```python
btn_go = button("Open Detail", screen=list_screen,
                command=lambda: on_click_navigate(detail_screen, data={"title": "Manual"}))
```

### `on_click_navigate(screen, data={})`

| Parameter | Type | Description |
|-----------|------|-------------|
| `screen` | `Screen` | The destination screen to navigate to |
| `data` | `dict` | Key/value pairs to pass. Values are converted to strings. |

### `screen.get_param(key, default="")`

| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `str` | The key to look up (must match a key passed in `data=`) |
| `default` | `str` | Value to return if the key was not passed (default: `""`) |

### Technical notes

**Android**: `on_click_navigate(screen, data={"k": v})` compiles to:
```java
Intent intent = new Intent(this, TargetActivity.class);
intent.putExtra("k", String.valueOf(v));
startActivity(intent);
```
`screen.get_param("k")` compiles to `getIntent().getStringExtra("k")`.

**Hot Previewer**: params are stored on `screen._params` before navigation; `get_param` reads from that dict.

**Setting component values on load**: Assign the component to a variable and call `set_value` with `get_param` at module level — the compiler places these `setText()` calls in the correct Activity's `onCreate`:
```python
lbl_title = label("", screen=detail_screen)
lbl_title.set_value(detail_screen.get_param("title"))  # → onCreate: lbl1.setText(getIntent().getStringExtra("title"))
```

---

## 5n. Loading Spinner ⏳

`spinner()` adds a native circular **loading indicator** — the spinning ring you see in every app while it's fetching data. It's the natural companion to `https.get()`, `db.query()`, `camera.capture()`, or any work that takes a moment: show it when the work starts, hide it when the result arrives.

### How to use it

```python
from apkpy_lib import Screen, button, label, spinner, https, json_get, run

main = Screen(id="main")
result = label("Press the button", id="result", screen=main)

# Start hidden — we only show it while loading.
loading = spinner(id="loading", screen=main, visible=False)

def on_response(success, response):
    loading.hide()                       # work finished → hide the spinner
    if success:
        result.set_value(json_get(response, "title"))
    else:
        result.set_value("Request failed")

def fetch():
    loading.show()                       # work starting → show the spinner
    https.get("https://jsonplaceholder.typicode.com/todos/1", on_response=on_response)

button("FETCH", id="btn", command=fetch, screen=main)

if __name__ == "__main__":
    run(start_screen=main)
```

### Parameters & methods

| | Description |
| :--- | :--- |
| `spinner(id=..., screen=..., visible=True)` | Creates the spinner. Pass `visible=False` to start hidden. |
| `.show()` | Shows the spinner and starts it spinning. |
| `.hide()` | Hides the spinner and stops it. |

### CSS styling

```css
loading {
    color: #6366F1;   /* color of the spinning ring */
    width: 48px;      /* size (optional — defaults to the platform's standard size) */
    height: 48px;
}
```

### How it works

| Environment | What `spinner` uses |
| :--- | :--- |
| **Hot Previewer** (your computer) | An animated rotating arc drawn on a Tkinter Canvas; `.show()` / `.hide()` start and stop the animation. |
| **Android Build** (real device) | An indeterminate `ProgressBar` (`color` → `android:indeterminateTint`). `.show()` / `.hide()` compile to `setVisibility(View.VISIBLE)` / `setVisibility(View.GONE)`; `visible=False` starts it as `android:visibility="gone"`. |

> [!TIP]
> The pattern is always the same: `loading.show()` right before the async call, and `loading.hide()` as the **first line** of your `on_response` / `on_result` callback — so it disappears the moment the result is back, whether it succeeded or failed.

---

## 6. The Two-Step Execution Flow 🔄

ApkPy makes the development lifecycle effortless via two distinct phases:

### Phase 1: Development (Hot Previewer)
Simply run your Python file normally:
```bash
python writehere.py
```
This instantly boots up our Tkinter-based Hot Previewer on your computer. You get immediate visual feedback for your screens, interactive inputs, focus colors, buttons, and navigation without needing an emulator!

### Phase 2: Production (Native Compilation)
When you're ready to deploy, run the CLI tool in your project folder:
```bash
apkpy build
```
This triggers the transpiler! ApkPy generates all Java classes, Manifests, **XML Layouts**, and Drawables completely from scratch. This new XML-driven approach ensures that complex layouts, margins, and alignments are handled natively by the Android OS for maximum stability. Open the resulting bundled `.zip` project and generate your production `.apk`!

---

## 7. What's New & Bug Fixes (v1.0.0) 🚀

- 🎵 **Native audio and background playback**: `audio.play`, `pause`, `resume`, `stop`, `seek` and `play_background` now cover music, radio and podcast playback. Background audio uses a foreground Android media service with notification/lock-screen metadata, transport controls and audio-focus handling. See §5i.
- ⏭️ **Queues and full player UI**: `audio.play_playlist`, `next`, `previous`, `shuffle` and `repeat` manage a real queue. `audio.now_playing(...)` binds progress/time/cover/title/artist components and `audio.controls(...)` synchronizes play/pause, shuffle and repeat buttons. See §5i.
- 🎛️ **Persistent mini-player, favourites and playlists**: `mini_player(open=...)` adds a global now-playing bar. `audio.like_button`, `liked_list`, `add_to_playlist`, `play_saved_playlist`, `playlists_list`, `edit_playlist`, `playlist_editor`, `remove_from_playlist` and `delete_playlist` provide persistent library features. See §5i.
- 🖼️ **Rich lists, carousels and grids**: `list_view(..., rich=True)`, `carousel(...)` and `grid(..., cols=N)` render native media cards with remote covers, titles, subtitles and custom payloads such as an audio `src`. Dynamic rich lists accept `set_items(..., image="field")`. See §5h.
- 📥 **Offline downloads with `files`**: Download asynchronously with `files.download`, then inspect, play or remove app-private files with `exists`, `path` and `delete`—no broad storage permission required. See §5j.
- 🔐 **OAuth 2.0 + PKCE with `auth`**: Browser sign-in for Google, Spotify and GitHub (or custom providers), token persistence, normalized user profiles and logout through `auth.login`, `token`, `is_logged_in`, `user` and `logout`. Android uses a generated deep-link Activity; the Previewer uses a localhost callback. See §5k.
- 🗂️ **`bottom_nav(screens, labels=[], icons=[])`**: Native bottom navigation bar linking 2–5 screens. On Android, compiles to `BottomNavigationView` with auto-generated menu XML and vector drawables — zero assets to manage. Tab switches reuse Activities (`FLAG_ACTIVITY_REORDER_TO_FRONT`) for instant navigation with no re-creation. Each Activity has an `onResume()` guard so the correct tab is always highlighted when you return to a screen (e.g. after pressing the system back button). In the Hot Previewer, renders a dark bar at the bottom with active-tab indicator line and bold label.
- 📜 **Scrollable screens (`scroll=True`)**: Pass `scroll=True` to any `Screen` to make the entire page vertically scrollable — all components scroll together. In the Previewer, scroll with the mouse wheel from anywhere on the screen. On Android, compiles to `NestedScrollView` with a `LinearLayout` inside.
- 📋 **`list_view(items, id=..., screen=..., on_click=...)`**: New native list component. Accepts plain strings or dicts with `"title"` and `"subtitle"` keys. Supports CSS `color`, `background-color`, `border-color`, and `height`. Update the list at runtime with `.set_items(new_list)` — which also accepts a JSON string from `db.query()` or an `https` response, with `title=`/`subtitle=` field mapping. Lambda `on_click` callbacks are fully supported. In scroll screens, compiles to `TextViews` inside a `LinearLayout` (no nested scroll conflict).
- 🐛 **Fixed `type="range"` (SeekBar) Gradle errors**: `set_value()` now uses `setProgress(Integer.parseInt(...))` and `get_value()` uses `String.valueOf(getProgress())` — the previous code incorrectly called `setText()`/`getText()` on a SeekBar.
- 🐛 **Fixed `list_view` toast duplication**: `item["title"]` and `item["subtitle"]` in `on_click` lambdas now correctly extract the two parts of the stored string — the previous code duplicated the full item text.
- 🐛 **Fixed `list_view` text always black on Android**: A custom `ArrayAdapter` subclass with a `getView()` override is now generated to apply the CSS `color` and `background-color` to every item row.
- 🧩 **User-defined functions with arguments**: Define your own helper functions that take parameters — `def adjust(delta): ...` — and call them from any `command=` or `on_click=` lambda with arguments (`command=lambda: adjust("10")`). Compiles to a real Java method `pythonCallback_adjust(String delta)` with a typed call site, instead of being silently dropped. Identical behaviour in the Previewer and on Android.
- 🔢 **Comparison operators in `if` (`<`, `>`, `<=`, `>=`)**: Conditions now support the full set of comparisons alongside `==` / `!=`. Numbers compare numerically — floats included (`if price >= 1.5`) — and strings compare lexicographically, just like Python. Compiles to native `Double.parseDouble(...)` / `compareTo(...)` checks.
- 🔁 **Iterate `db.query()` directly**: `for row in db.query("SELECT level FROM tank"):` now works inline — no temporary variable needed. Previously only an assigned `rows = db.query(...)` would iterate on Android, while the inline form silently produced zero rows (it worked only in the Previewer). Both forms now compile to the same native SQLite read.
- 🚀 **`apkpy run` — compile straight to an installable APK**: One command transpiles `writehere.py`, compiles it with Gradle, and drops a ready-to-install `<app>-debug.apk` right next to your script — no Android Studio needed. The `.apk` is always written to disk; `--qr` additionally serves it over your Wi-Fi with a scannable QR code, and `--usb` installs it to a connected device via `adb`. (`apkpy build` still produces a `.zip` Android Studio project if you'd rather compile there.)
- 🩺 **`apkpy doctor`**: Reports whether your machine has a compatible toolchain — a JDK 17–21, the Android SDK, and Gradle — and points at what's missing. If you have Android Studio, ApkPy auto-detects and reuses its bundled JDK and SDK, so there's no `ANDROID_HOME` to configure.
- 📥 **`apkpy setup`**: For machines without Android Studio — downloads a compatible JDK, Gradle, and the Android SDK packages into `~/.apkpy` so `apkpy run` works out of the box.
- 🎲 **`random` — bundled in apkpy_lib**: `from apkpy_lib import random` (no stdlib import needed), then `random.randint(a, b)`, `random.choice(list)` and `random.random()`. Transpiles to native Android (`java.util.Random` / `Math.random()`) — perfect for dice, pickers, quizzes and games. Works the same in the Previewer (it wraps Python's real `random`) and on Android. The random *values* won't match between the two — that's expected: each side is independently random.
- 📱 **Preview on any device size — `device(...)`**: A Previewer-only helper to resize the preview window to a specific phone (`device("Pixel 8")`) or go full-window (`device("fullscreen")` borderless with Esc to exit, or `device("maximized")` which keeps the title bar). Accepts every Pixel from the Pixel 4 to the Pixel 10 Pro; the default stays Pixel 9. It's **ignored on Android** — there the real screen *is* the device — so leaving it in your code never changes the APK.
- 🧬 **f-strings (`f"...{value}..."`)**: Write `f"Olá {nome}! Total: {preco:.2f} €"` instead of gluing strings with `+` and `str()`. Inserts variables and inline expressions (`{a + b}`); a format spec like `{preco:.2f}` fixes the decimals and compiles to `String.format(Locale.US, "%.2f", ...)`, matching Python exactly. See §29.
- 🔁 **`while` loops**: `while i < 5:` now compiles to native Java, with the same condition set as `if` (`<`, `and`/`or`/`not`, …) and full `break`/`continue` support. An automatic safety limit stops an accidental infinite loop from freezing the Android UI thread. See §29.
- ➕ **Augmented assignment (`+=`, `-=`, `*=`, `/=`, `%=`)**: `total += 5`, `i -= 1`, `texto += "!"` — numeric maths *or* string concatenation, depending on the value. See §29.
- ✅ **`.isdigit()` — input validation**: `if valor.isdigit():` before `int(valor)` so empty/non-numeric input shows a message instead of crashing. Compiles to `matches("\\d+")` with the same truth value as Python. See §29.
- 🔤 **`.startswith(x)` / `.endswith(x)`**: Check prefixes and suffixes inside an `if` — `if url.startswith("https://")`, `if email.endswith("@gmail.com")`. Maps directly to Java's `.startsWith(...)` / `.endsWith(...)` for an exact match with Python. See §29.
- 🕐 **`datetime` — bundled in apkpy_lib**: `from apkpy_lib import datetime`, then `datetime.now()`, `datetime.date()`, `datetime.time()`, `datetime.hour()` (and `year`/`month`/`day`/`minute`/`second`). Everything returns a string; compiles to native `SimpleDateFormat`. See §30.
- 📦 **`from apkpy_lib import *`**: Import the whole library in one line — and `container` is now exported too (it was missing). See §31.
- 🐛 **Fixed string-literal escaping**: Strings containing a newline (`"a\nb"`), tab or backslash now compile correctly. Previously only quotes were escaped, so a `\n` could break the Android build with `error: unclosed string literal`. The fix covers labels, `set_value`, f-strings, comparisons, module constants and list items.
- 📦 **`apkpy.toml` — app name, icon, package id & version**: `apkpy init` scaffolds it; `apkpy run` / `apkpy build` apply it automatically. The `name` becomes the launcher label (it used to be hardcoded "ApkPy App"), the `icon` is generated in every density, and `application_id` / `version_*` flow into the build. No config and no `--name`? `apkpy run` asks for the name the first time and saves it. See §32.
- 🔑 **`apkpy release` — signed builds for the Play Store**: `apkpy release` produces a *signed* `.apk` (the unsigned ones don't install); `apkpy release --aab` produces an App Bundle for Google Play. The signing key is created once per app and reused for every update — exactly what the store requires. See §32.

### Previous Highlights (v0.9.9)

- 📅 **`type="date"`**: Opens the native Android `DatePickerDialog`. Returns `"DD/MM/YYYY"` from `get_value()`, or `""` if the user hasn't picked yet. In the Previewer opens a spinbox dialog (Day / Month / Year).
- 🕐 **`type="time"`**: Opens the native Android `TimePickerDialog`. Returns `"HH:MM"` from `get_value()`. In the Previewer opens a spinbox dialog (Hour / Minute).
- 🔢 **`type="number"`**: Shows the numeric keyboard on Android automatically. In the Previewer, rejects non-numeric characters as you type. Supports decimals and negatives. `get_value()` always returns a string — use `int()` or `float()` in your code.
- 🔘 **`type="switch"`**: Native `SwitchCompat` toggle. CSS `background-color` sets the "on" track colour. `get_value()` returns `"true"` or `"false"`.
- 🔽 **`type="select"`**: Native Android `Spinner` dropdown. Pass options as `"A|B|C"`. `get_value()` returns the selected option text.
- 📝 **`type="textarea"`**: Multi-line `EditText`. Control height with CSS `rows`.

### Previous Highlights (v0.9.8) 🚀

- 🗨️ **`alert(title, message)`**: Show a native informational dialog with an OK button. Fire-and-forget — no callback needed. Compiles to `AlertDialog.Builder` on Android. In the Previewer, opens a custom dialog with an English "OK" button (unaffected by OS language).
- ✅ **`confirm(title, message, on_result=callback)`**: Show a native confirmation dialog with OK and Cancel buttons. Calls `on_result(True)` if the user confirms, `on_result(False)` if they cancel — the same async `on_result` pattern as `camera.capture` / `gallery.pick`. Compiles to `AlertDialog.Builder` with positive/negative buttons on Android.

We've been hard at work making ApkPy the most reliable Python-to-Android framework. Here's what's new in **v0.9.8** (now superseded by v0.9.9):

- ⏱️ **Background Services API (`service`)**: Run code even when your app is closed. `service.every(run=fn, minutes=N, id=...)` schedules recurring work, `service.once(run=fn, after_minutes=N, id=...)` schedules a one-time delayed task, and `service.cancel(id=...)` stops a scheduled task. Compiles to native `WorkManager` (`PeriodicWorkRequest` / `OneTimeWorkRequest`) with real `only_on_wifi` / `only_when_charging` constraints — your background functions can use `storage`, `db`, `https`, `toast` and `notify` with **100% identical code** between the Previewer and Android.
- 🔔 **`notify(title, message, id=...)`**: Show native system notifications in the phone's notification bar — visible even when the app isn't open. The perfect companion to background services: tell the user something happened without requiring the app to be in the foreground. Compiles to `NotificationCompat.Builder` + `NotificationManager`, including the Android 13+ runtime permission request.
- 📤 **`share(text, title=None)`**: Open the native Android share sheet to send text to WhatsApp, Email, SMS, Bluetooth and more, with a single call. Compiles to `Intent.ACTION_SEND` + `Intent.createChooser(...)` and works from both screens and background services.
- 📋 **`clipboard.copy(text)`**: Copy text to the system clipboard with `clipboard.copy(...)`. Compiles to native `ClipboardManager`/`ClipData` on Android, and writes to the **real OS clipboard** in the Previewer — so `Ctrl+V` outside the app pastes the actual text.
- 📸 **`camera.capture(on_result=...)` & `gallery.pick(on_result=...)`**: Open the native camera or image picker and get the resulting photo's path back through an async `on_result(success, path)` callback — same async pattern as `https`. Compiles to `ActivityResultContracts.TakePicture()` / `GetContent()` with automatic `CAMERA` permission + `FileProvider` setup (zero manifest editing). Since your computer has no camera or gallery, the Previewer simulates both by opening your OS's file picker — your code stays 100% identical either way.
- 🐛 **Bug fixes**: Fixed `apkpy build` opening a blank Previewer window on every run; fixed `toast(f"...")` (f-strings) generating empty messages; fixed `notify()` not requesting the `POST_NOTIFICATIONS` runtime permission on Android 13+; fixed `service.cancel()`/`service.once()` being dropped inside nested button-handler code paths.
- **Cross-platform parity**: All new features are fully functional in Phase 1 (Tkinter Previewer) and compile to native Android code in Phase 2.

### Previous Highlights (v0.9.7.1)
- 🗄️ **SQLite Database API**: Build fully offline, data-persistent Android apps with the new `db` object. Use `db.execute()` for writes and `db.query()` for reads — with zero Java knowledge required.
- 🌐 **HTTPS Network API**: Connect your app to any REST API on the internet with `https.get()`, `https.post()`, `https.put()`, `https.patch()` and `https.delete()` — full CRUD. Supports custom `headers` for Bearer tokens and API keys. Runs in a background thread — no UI freezes. Compiles to native Android `HttpURLConnection`.
- 🔎 **`json_get()` Helper**: Safely navigate JSON results from `db.query()` or `https` responses using dot-notation paths (e.g., `json_get(result, "main.temp")`). Works identically in the Previewer and on real Android.

### Previous Highlights (v0.9.3)
- **XML Layout Engine**: Moved from programmatic Java UI to native Android XML layouts. This fixes 99% of layout inconsistencies.
- **Named Builds**: Running `apkpy build` now interactively asks you for the project name.
- **Pixel 9 Pro Previewer**: The Hot Previewer is now perfectly calibrated to match the Pixel 9 Pro screen.
- **Automatic Gravity & Centering**: Fixed bugs where elements wouldn't center correctly.
- **Animation Stability**: Fixed glitches in `@keyframes` animations.

---

## 8. FAQ (Quick Answers) ❓

**"Does it support Android APIs?"**
Yes! Native permissions, camera, gallery, background services, notifications, sharing, clipboard and GPS/location are already supported — see Sections 12, 18-22 and 24 below. We're expanding support for vibration and more in upcoming versions.

**"I clicked 'Take Photo' in the Previewer and it just opened a file picker — is that a bug?"**
No, that's by design! Your computer doesn't have a camera app or photo gallery, so `camera.capture()` and `gallery.pick()` simulate the flow in the Previewer by opening your OS's file explorer — pick any image and it's treated as "the photo you took" / "the image you chose". Your `on_result(success, path)` callback receives the real file path either way. On a real Android device (after `apkpy build`), the exact same code opens the actual native camera/gallery apps. See Section 22 for details.

**"Do I need the Android SDK installed?"**
To write and preview your app using `python writehere.py`, **no SDK is required**. However, `apkpy build` generates a native Android Studio project. To compile that project into a final `.apk`, you will need the Android SDK/Java installed on your machine (or just upload the generated folder to a CI/CD service like GitHub Actions).

**"Can I use images in my app?"**
Yes! See Section 10 below. Just put your image file next to `writehere.py` and use the `image()` component — ApkPy handles copying assets into Android's drawable folder automatically.

---

## 9. Spacing & Margins (Precision Layout) 📐

Margins allow you to create "invisible space" around your components. This is essential for moving buttons down, separating inputs, or creating breathing room between elements.

### Example Code:
```css
my_button {
    /* Move the button 50 pixels down from whatever is above it */
    margin-top: 50px;

    /* Push the button 20 pixels away from the left edge */
    margin-left: 20px;
}

header {
    /* Put a 30 pixel gap at the bottom of the title */
    margin-bottom: 30px;
}
```

### Margin Guide (For Absolute Beginners):

| Property | What it does (in simple terms) |
| :--- | :--- |
| **`margin`** | Adds space to **all four sides** (top, bottom, left, and right) at once. It's like putting a bubble around the whole component. |
| **`margin-top`** | Pushes the component **down**. It adds space to the top edge. Use this if you want something to move lower on the screen. |
| **`margin-bottom`** | Pushes the *next* component **away**. It adds space to the bottom edge. |
| **`margin-left`** | Pushes the component to the **right**. It adds space to the left side. |
| **`margin-right`** | Pushes the component to the **left**. It adds space to the right side. |

> [!TIP]
> In ApkPy, you can use `px` (like `20px`). The compiler automatically converts this to `dp` for Android, so your app looks perfect on every phone screen size!

---

## 10. Images & Toast Notifications 🖼️

### Images
The `image()` component renders a native Android `ImageView`. Just place your image file (`.png`, `.jpg`) in the **same folder as `writehere.py`** and reference it by filename. ApkPy will automatically copy it into the Android `res/drawable` folder during the build step.

```python
from apkpy_lib import image, Screen

home = Screen(id="home_screen")

# Just place 'logo.png' next to your writehere.py file!
image("logo.png", id="app_logo", screen=home)
```

You can fully style the image through CSS just like any other component:
```css
app_logo {
    width: 150px;
    height: 150px;
    border-radius: 75px;      /* Makes it circular! */
    margin-top: 60px;
    box-shadow: 0 8px 20px #000;
    animation-name: fadeIn;
    animation-duration: 1000ms;
}
```

> [!IMPORTANT]
> ApkPy automatically handles the conversion of your image file into an Android-compatible resource. You do **not** need to manually add it to any resource folder — just place it next to your script and it works.

### Images from the internet (URLs) 🌐

`image()` also accepts a full **URL** — perfect for avatars, product photos, or any image coming from a REST API. ApkPy detects the `http://` / `https://` prefix automatically and loads the image **at runtime**, in a background thread, so the UI never freezes.

```python
from apkpy_lib import Screen, image, run

home = Screen(id="home")

# Local file → copied into res/drawable at build time
image("logo.png", id="logo", screen=home)

# Remote URL → downloaded on the device at runtime
image("https://picsum.photos/400/200", id="banner", screen=home)
```

| Environment | What happens with a URL |
| :--- | :--- |
| **Hot Previewer** (your computer) | The image is downloaded with `urllib` in a background thread; a "loading…" placeholder shows until it arrives, then it's drawn (with the same `object-fit`, `border-radius`, `opacity`, and filters as local images). |
| **Android Build** (real device) | Compiles to a background `Thread` + `HttpURLConnection` + `BitmapFactory.decodeStream(...)`, then `setImageBitmap(...)` on the UI thread. The `INTERNET` permission is declared automatically. No third-party library (Glide/Picasso) needed. |

All the same CSS works on remote images — `width`, `height`, `border-radius`, `object-fit`, `opacity`, `box-shadow`, animations:

```css
banner {
    width: 400px;
    height: 200px;
    border-radius: 12px;
}
```

> [!TIP]
> Pair this with `https.get()` + `list_view` to show images from an API — e.g. a product list where each row's image comes from a URL in the JSON response.

> [!NOTE]
> A remote `image()` has no `android:src` in the layout — it's blank until the download finishes (just like the "loading…" placeholder in the Previewer). If the URL fails (offline, 404), the image simply stays blank; it never crashes the app.

---

### Toast Notifications 🍞

Toasts are small, temporary pop-up messages that appear at the bottom of the screen — a staple of Android UX. In ApkPy, triggering one is a single line of Python:

```python
from apkpy_lib import toast

def on_save():
    toast("Your data was saved! ✅")
```

In the **Hot Previewer** (Phase 1), toasts appear as an overlay message at the bottom. On a **real Android device** (Phase 2), they compile to the native `Toast.makeText(...)` Java call — identical to what professional apps use.

You can call `toast()` from **any function** — button clicks, storage operations, permission results, or even on app startup:
```python
from apkpy_lib import toast, run, Screen

home = Screen(id="home")
run(start_screen=home)
toast("Welcome back! 👋")  # Shows when the app launches
```

---

## 11. Storage & Persistence 💾

ApkPy provides a powerful, built-in `storage` API that allows you to **persistently save data** across app sessions — things like user preferences, saved form values, login tokens, and more.

### How It Works Under the Hood

| Environment | What `storage` uses |
| :--- | :--- |
| **Hot Previewer** (your computer) | A local `.json` file saved next to your script |
| **Android Build** (real device) | Native Android `SharedPreferences` — the fastest and most battery-efficient persistent storage on Android |

This means your code is **100% identical** in both environments. No special cases, no `if platform:` checks.

> [!NOTE]
> **Everything you store is encrypted automatically.** `storage.set()` encrypts every value before it touches the disk (AES-256 with an Android Keystore key on Android), and `storage.get()` decrypts transparently — your code doesn't change at all. If someone steals the preferences file, all they see is `enc1$…` gibberish. Values saved by older versions in plain text are still read normally. See section **25. Crypto** for details.

### Importing Storage

```python
from apkpy_lib import storage
```

### The Full API

| Method | Description | Example |
| :--- | :--- | :--- |
| `storage.set(key, value)` | Saves a value permanently | `storage.set("username", "Alice")` |
| `storage.get(key, default)` | Reads a value (returns `default` if not found) | `storage.get("username", "Guest")` |
| `storage.delete(key)` | Removes a single key | `storage.delete("token")` |
| `storage.clear()` | Wipes **all** stored data | `storage.clear()` |

### Basic Example — Saving a User's Name

```python
from apkpy_lib import Screen, inputs, button, toast, storage, run

my_screen = Screen(id="main")

name_field = inputs("Enter your name", type="text", id="name_field", screen=my_screen)

def save_name():
    name = name_field.get_value()
    if name != "":
        storage.set("username", name)
        toast(f"Saved: {name} ✅")

def load_name():
    saved = storage.get("username", "")
    if saved != "":
        name_field.set_value(saved)
        toast(f"Welcome back, {saved}! 👋")
    else:
        toast("No name saved yet.")

button("Save", id="btn_save", command=save_name, screen=my_screen)
button("Load", id="btn_load", command=load_name, screen=my_screen)

run(start_screen=my_screen)
```

### Advanced Example — Settings Screen with Auto-load

One of the most useful patterns is **auto-loading** saved preferences when the app starts — so the user never has to set things up twice:

```python
from apkpy_lib import Screen, inputs, button, label, toast, storage, run

settings_screen = Screen(id="settings")

# Dropdown for language
language_select = inputs("English|Portuguese|Spanish", type="select", id="lang_select", screen=settings_screen)

# Toggle switches for on/off settings
sw_notif = inputs("Notifications",  type="switch", id="sw_notif", screen=settings_screen)
sw_dark  = inputs("Dark mode",      type="switch", id="sw_dark",  screen=settings_screen)

status = label("", id="status", screen=settings_screen)

# ── Auto-load saved values when the app opens ────────────────────────────────
saved_lang  = storage.get("language", "")
saved_notif = storage.get("notifications", "")
saved_dark  = storage.get("dark_mode", "")

if saved_lang  != "": language_select.set_value(saved_lang)
if saved_notif != "": sw_notif.set_value(saved_notif)   # "true" turns it on
if saved_dark  != "": sw_dark.set_value(saved_dark)

# ── Save button ──────────────────────────────────────────────────────────────
def save_settings():
    storage.set("language",      language_select.get_value())  # e.g. "English"
    storage.set("notifications", sw_notif.get_value())         # "true" or "false"
    storage.set("dark_mode",     sw_dark.get_value())          # "true" or "false"
    toast("Settings saved! ⚙️")
    status.set_value("Saved: " + language_select.get_value())

button("SAVE SETTINGS", id="btn_settings", command=save_settings, screen=settings_screen)

style = """
settings {
    background-color: #0F172A;
    padding: 24px;
    gap: 16px;
}
lang_select { color: #F8FAFC; background-color: #1E293B; border-color: #334155; border-radius: 10px; }
sw_notif    { color: #F8FAFC; background-color: #4F46E5; }
sw_dark     { color: #F8FAFC; background-color: #4F46E5; }
status      { color: #38BDF8; font-size: 13px; }
btn_settings { background-color: #4F46E5; color: #F8FAFC; border-radius: 12px; font-weight: bold; pressed-color: #3730A3; }
"""

run(start_screen=settings_screen)
```

> [!IMPORTANT]
> The `.get_value()` and `.set_value()` methods work on **virtually all interactive inputs** — text fields, checkboxes, radio button groups, dropdowns, sliders, and even labels. String comparisons with `==` or `!=` work natively in both the Previewer (Python) and on the final Android device (Java).

> [!TIP]
> Use `storage.get("key", "default_value")` to safely provide a fallback for first-time users who have no saved data yet. This prevents crashes and unexpected empty states.

---

## 12. Native Permissions & Features 📱

ApkPy allows you to interact directly with Android system features using a simple Python API.

### Declaring Manifest Permissions
Declare what your app needs so the compiler can update `AndroidManifest.xml` automatically.
```python
from apkpy_lib import declare_permissions
declare_permissions(["CAMERA", "LOCATION_FINE", "INTERNET"])
```

### Runtime Permission Requests & Toasts
Prompt users for permissions and provide instant feedback with native Toasts.
```python
from apkpy_lib import permissions, toast

def ask_camera():
    def on_result(granted):
        if granted:
            toast("Camera access granted! 📸")
        else:
            toast("We need camera permission to continue.")

    permissions.request("CAMERA", on_response=on_result)
```

---

## 13. Master Example: "Coffee Haven" ☕

This is a complete, real-world multi-screen app built entirely with ApkPy. It demonstrates **images, navigation, radio buttons, text inputs, storage persistence, toast notifications, and a fully custom CSS design** all working together.

```python
from apkpy_lib import Screen, button, label, inputs, image, run, toast, storage

# 1. Setup Screens
welcome_screen = Screen(id="welcome_container")
order_screen = Screen(id="order_container")

# 2. Logic: Handle Order
def place_order():
    # Save preferences to storage so the user's choices are remembered
    storage.set("coffee_select", coffee_select.get_value())
    storage.set("special_notes", special_notes.get_value())
    toast("Order placed! Your coffee will be ready soon. ☕")

# 3. Welcome Screen UI
image("logo.png", id="welcome_logo", screen=welcome_screen)
label("COFFEE HAVEN", id="welcome_title", screen=welcome_screen)
label("The best brew in town.", id="welcome_subtitle", screen=welcome_screen)
btn_start = button("EXPLORE MENU", id="btn_primary", screen=welcome_screen)

# Navigate to Order Screen on button click
welcome_screen.on_click_navigate(button=btn_start, to=order_screen)

# 4. Order Screen UI
label("SELECT YOUR COFFEE", id="menu_title", screen=order_screen)

# Radio buttons for coffee selection
coffee_select = inputs("Espresso|Latte|Cappuccino|Mocha", type="radio", id="coffee_select", screen=order_screen)

# Free-text for special instructions
special_notes = inputs("Special instructions (e.g., extra sugar)", type="text", id="special_notes", screen=order_screen)

# Auto-load saved preferences from last session!
saved_coffee = storage.get("coffee_select", "")
if saved_coffee != "":
    coffee_select.set_value(saved_coffee)

saved_notes = storage.get("special_notes", "")
if saved_notes != "":
    special_notes.set_value(saved_notes)

# Place Order button
button("PLACE ORDER", id="btn_order", command=place_order, screen=order_screen)

# 5. Full Custom CSS
style = """
welcome_container {
    background-color: #2D1E17;
    flex-direction: column;
    gap: 0px;
}

order_container {
    background-color: #FDF8F5;
    flex-direction: column;
    padding: 30px;
    gap: 20px;
}

welcome_logo {
    width: 180px;
    height: 180px;
    border-radius: 90px;
    margin-top: 100px;
    box-shadow: 0 10px 20px #000;
    animation-name: fadeInDown;
    animation-duration: 1000ms;
}

welcome_title {
    color: #FDF8F5;
    font-size: 32px;
    font-weight: bold;
    margin-top: 40px;
    animation-name: fadeInDown;
    animation-duration: 1200ms;
}

welcome_subtitle {
    color: #D4A373;
    font-size: 18px;
    margin-bottom: 60px;
    animation-name: fadeIn;
    animation-duration: 2000ms;
}

btn_primary {
    background-color: #D4A373;
    color: #2D1E17;
    border-radius: 30px;
    font-weight: bold;
    font-size: 18px;
    padding: 18px 45px;
    pressed-color: #B88B5B;
    animation-name: fadeInUp;
    animation-duration: 1000ms;
}

menu_title {
    color: #2D1E17;
    font-size: 24px;
    font-weight: bold;
    animation-name: fadeIn;
    animation-duration: 1000ms;
}

coffee_select {
    color: #3E2723;
    font-size: 18px;
}

special_notes {
    border-color: #D4A373;
    border-radius: 12px;
    padding: 20px;
    focus-border-color: #2D1E17;
}

btn_order {
    background-color: #2D1E17;
    color: #FDF8F5;
    border-radius: 15px;
    font-weight: bold;
    font-size: 18px;
    padding: 20px;
    pressed-color: #1A110D;
    margin-top: 30px;
    animation-name: zoomIn;
    animation-duration: 800ms;
}

@keyframes fadeInDown {
    from { opacity: 0; margin-top: -40px; }
    to   { opacity: 1; margin-top: 0px; }
}

@keyframes fadeInUp {
    from { opacity: 0; margin-top: 40px; }
    to   { opacity: 1; margin-top: 0px; }
}

@keyframes zoomIn {
    from { opacity: 0; scale: 0.8; }
    to   { opacity: 1; scale: 1.0; }
}

@keyframes fadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}
"""

if __name__ == "__main__":
    run(start_screen=welcome_screen)
```

---

## 14. Master Example: Mini Profile App 👤

This example demonstrates inputs, permissions, toasts, and advanced CSS styling in a focused single-screen app.

```python
from apkpy_lib import Screen, button, label, inputs, run, toast, declare_permissions, permissions

# 1. Declare permission for the compiler
declare_permissions(["CAMERA"])

# 2. Setup the Screen
profile_screen = Screen(id="profile_container")

# 3. Logic: Handle Camera Request
def update_photo():
    def on_perm(granted):
        if granted:
            toast("Thanks! Accessing camera for your photo...")
        else:
            toast("We need camera access to take a photo!")

    permissions.request("CAMERA", on_response=on_perm)

# 4. Build the UI
label("Mini Profile App", id="header", screen=profile_screen)
inputs("Enter your Full Name", type="text", id="name_field", screen=profile_screen)
inputs("Short Bio", type="text", id="bio_field", screen=profile_screen)

btn_photo = button("Set Profile Picture", id="btn_outline", command=update_photo, screen=profile_screen)
btn_save  = button("Save Profile", id="btn_primary", screen=profile_screen)

# 5. Advanced CSS System
style = """
profile_container {
    flex-direction: column;
    gap: 20px;
    background-color: #ffffff;
    padding: 30px;
}

header {
    color: #1a1a1a;
    font-size: 24px;
    font-weight: bold;
}

name_field, bio_field {
    border-color: #e0e0e0;
    border-radius: 12px;
    padding: 14px;
    focus-border-color: #6200EE;
}

btn_outline {
    background-color: #ffffff;
    color: #6200EE;
    border-color: #6200EE;
    border-width: 2px;
    border-radius: 20px;
    pressed-color: #f3e5f5;
}

btn_primary {
    background-color: #6200EE;
    color: white;
    border-radius: 20px;
    pressed-color: #3700B3;
}
"""

run(start_screen=profile_screen)
```

---

## 15. Declarative CSS Animations 🎬

ApkPy supports **native declarative animations** using a syntax inspired by CSS Keyframes. You can define how a component should transition from one state to another, and the framework will generate the corresponding native Android `Animation` XML and Java logic.

### How it Works: The `@keyframes` Block

An animation is defined using the `@keyframes` keyword followed by a name. Inside, you define two states:
- **`from` (or `0%`)**: The starting state of the component when it appears.
- **`to` (or `100%`)**: The final state where the component should end.

```css
@keyframes slideUp {
  from {
      opacity: 0;
      margin-top: 50px;
  }
  to {
      opacity: 1;
      margin-top: 0px;
  }
}
```

### Applying the Animation

```css
my_button {
    animation-name: slideUp;
    animation-duration: 1500ms;
}
```

### Supported Properties:

| Property | Description | Example |
| :--- | :--- | :--- |
| **`opacity`** | Fades the component in or out (0.0 is invisible, 1.0 is solid). | `opacity: 0;` to `opacity: 1;` |
| **`margin-top`** | Moves the component vertically (Y-axis). | `margin-top: 100px;` to `0px;` |
| **`margin-left`** | Moves the component horizontally (X-axis). | `margin-left: -50px;` to `0px;` |
| **`scale`** | Resizes the component (1.0 is normal size). | `scale: 0.5;` to `scale: 1.0;` |

### Ready-to-Use Animation Presets:

#### 1. Smooth Fade-In
```css
@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}
```

#### 2. Slide Down From Top
```css
@keyframes fadeInDown {
    from { opacity: 0; margin-top: -40px; }
    to { opacity: 1; margin-top: 0px; }
}
```

#### 3. Slide Up From Bottom
```css
@keyframes fadeInUp {
    from { opacity: 0; margin-top: 40px; }
    to { opacity: 1; margin-top: 0px; }
}
```

#### 4. The "Pop" / Zoom Effect
```css
@keyframes zoomIn {
    from { scale: 0.5; opacity: 0; }
    to { scale: 1.0; opacity: 1; }
}
```

#### 5. Side Slide From Left
```css
@keyframes slideFromLeft {
    from { margin-left: -200px; opacity: 0; }
    to { margin-left: 0px; opacity: 1; }
}
```

> [!IMPORTANT]
> **Cross-Platform Support**: These animations are fully functional in the **Tkinter Previewer** (Phase 1) so you can test the "vibe" of your app, and they compile to **Native Android XML** (Phase 2) for maximum performance on real devices.

---

## 16. SQLite Database 🗄️

ApkPy now supports **native local databases** powered by SQLite. Build offline-first apps that store structured data permanently on the device — no internet required.

### How It Works Under the Hood

| Environment | What `db` uses |
| :--- | :--- |
| **Hot Previewer** (your computer) | Python's built-in `sqlite3` module, saved as `apkpy_app.db` next to your script |
| **Android Build** (real device) | Native `android.database.sqlite.SQLiteDatabase` — the same engine used by Google apps |

Your code is **100% identical** in both environments.

### Importing the Database

```python
from apkpy_lib import db, json_get
```

### The Full API

| Method | Description | Example |
| :--- | :--- | :--- |
| `db.execute(sql)` | Runs an SQL statement that **modifies** data | `db.execute("CREATE TABLE IF NOT EXISTS users (name TEXT)")` |
| `db.execute(sql, params)` | Same, but fills the `?` placeholders **safely** — use this whenever the SQL contains user data | `db.execute("INSERT INTO users (name) VALUES (?)", [name])` |
| `db.query(sql)` | Runs a `SELECT` and returns a **JSON string** | `result = db.query("SELECT * FROM users")` |
| `db.query(sql, params)` | Same, with safe `?` placeholders | `result = db.query("SELECT * FROM users WHERE name = ?", [name])` |
| `db.last_insert_id()` | The `rowid` (auto-increment id) of the **last inserted row** | `new_id = db.last_insert_id()` |
| `db.begin()` | Starts a **transaction** (everything after is pending) | `db.begin()` |
| `db.commit()` | **Saves** everything done since `db.begin()` | `db.commit()` |
| `db.rollback()` | **Undoes** everything done since `db.begin()` | `db.rollback()` |

### Getting the new row's id — `last_insert_id()` 🔑

Right after an `INSERT` into a table with an `INTEGER PRIMARY KEY AUTOINCREMENT` column, call `db.last_insert_id()` to get the id the database just generated — no extra `SELECT` needed:

```python
db.execute("INSERT INTO users (name) VALUES (?)", [name])
new_id = db.last_insert_id()     # e.g. 42
toast(f"User created with id {new_id}")
```

It works in any expression (f-strings, `set_value`, even as a `?` parameter for the next query). In the Hot Previewer it returns Python's `cursor.lastrowid`; on Android it transpiles to `SELECT last_insert_rowid()` on the same connection — the same value in both.

### Transactions — all or nothing 🏦

Some operations only make sense if they **all** succeed together. A bank transfer is the classic case: debit one account *and* credit the other — never just one. Wrap them between `db.begin()` and `db.commit()` and they're written to disk **atomically**. If anything goes wrong, `db.rollback()` undoes everything since `db.begin()`, leaving the database exactly as it was.

```python
def transfer(amount):
    db.begin()                                                              # start transaction
    db.execute("UPDATE accounts SET balance = balance - ? WHERE name = ?", [amount, "Ana"])
    db.execute("UPDATE accounts SET balance = balance + ? WHERE name = ?", [amount, "Bo"])
    db.execute("INSERT INTO movements (description) VALUES (?)", [f"Ana -> Bo: {amount}"])
    move_id = db.last_insert_id()
    db.commit()                                                             # save all 3 at once
    toast(f"Transfer done (movement #{move_id})")
```

Undoing instead of saving:

```python
db.begin()
db.execute("UPDATE accounts SET balance = balance - 50 WHERE name = ?", ["Ana"])
db.rollback()      # nothing changed — Ana's balance is untouched
```

> [!IMPORTANT]
> A transaction is one **connection**: every `db.execute()` / `db.query()` between `begin()` and `commit()` shares it, so it's all-or-nothing. Always close a transaction with either `commit()` or `rollback()`. On Android this transpiles to `SQLiteDatabase.beginTransaction()` → `setTransactionSuccessful()` → `endTransaction()` (the native, durable way); a `rollback()` simply ends the transaction without marking it successful.

> [!TIP]
> Besides atomicity, wrapping a batch of inserts in a single transaction is also **much faster** than committing each one separately — SQLite only flushes to disk once.

### Parameterized Queries — SQL Injection Protection 🛡️

Never build SQL by pasting user input into the string. This classic pattern is **dangerous**:

```python
db.execute(f"INSERT INTO users (name) VALUES ('{name}')")   # ❌ DON'T
```

Two things go wrong:

1. **Broken queries** — if the user types `O'Brien`, the apostrophe closes the SQL string and the statement crashes.
2. **SQL injection** — a malicious user types `x'); DROP TABLE users; --` and your table is gone.

The fix is one small change — put a `?` where the value goes and pass the values as a list:

```python
db.execute("INSERT INTO users (name) VALUES (?)", [name])                    # ✅ DO
rows = db.query("SELECT * FROM users WHERE name = ? AND age > ?", [name, idade])
```

The values are **bound by the SQLite engine itself** (never concatenated into the SQL), so apostrophes are just text and injection is impossible. It works identically in the Previewer (Python `sqlite3` placeholders) and on Android (`SQLiteDatabase.execSQL(sql, args)` / `rawQuery(sql, args)`).

> [!IMPORTANT]
> Rule of thumb: if any part of the SQL comes from `get_value()`, storage, or the network — use `?` placeholders. Only hardcoded SQL (like `CREATE TABLE`) is fine without them.

### Reading Data with `json_get()`

Since `db.query()` returns a JSON string, use `json_get()` to read values without complex parsing:

```python
result = db.query("SELECT * FROM users ORDER BY id DESC")
first_name = json_get(result, "0.name")   # First row, "name" column
first_id   = json_get(result, "0.id")     # First row, "id" column
```

> [!TIP]
> The dot-notation path works like: `"<row_index>.<column_name>"`. So `"0.name"` means "the `name` column of the first row (index 0)".

### Showing query results in a list

To display **all** rows instead of one value, pass the JSON from `db.query()` straight to a `list_view`:

```python
rows = db.query("SELECT content, created FROM notes ORDER BY id DESC")
notes_list.set_items(rows, title="content", subtitle="created")
```

See *Feeding the list from a database or API* in the `list_view` section (and `apkpy examples` → [11] DB Notes List / `examples/14_db_notes_list.py`) for the full pattern.

### Full Example — Offline User Manager

```python
from apkpy_lib import Screen, button, label, input_field, run, toast, db, json_get

# Create the table once on startup
db.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")

my_screen = Screen(id="main_container")

nome_input  = input_field("Enter a name", id="input_nome", screen=my_screen)
lbl_total   = label("Total users: 0", id="lbl_total", screen=my_screen)
lbl_last    = label("Last added: --", id="lbl_ultimo", screen=my_screen)

def refresh_ui():
    rows   = db.query("SELECT * FROM users ORDER BY id DESC")
    count  = db.query("SELECT COUNT(*) as total FROM users")
    lbl_last.set_value(f"Last: {json_get(rows, '0.name')}")
    lbl_total.set_value(f"Total users: {json_get(count, '0.total')}")

def add_user():
    name = nome_input.get_value()
    if name != "":
        # `?` placeholder = safe against SQL injection and apostrophes
        db.execute("INSERT INTO users (name) VALUES (?)", [name])
        toast("User added! ✅")
        nome_input.set_value("")
        refresh_ui()

button("ADD USER", id="btn_add", command=add_user, screen=my_screen)
refresh_ui()

if __name__ == "__main__":
    run(start_screen=my_screen)
```

> [!IMPORTANT]
> Always create your table with `CREATE TABLE IF NOT EXISTS` — this is safe to run every time the app starts and won't overwrite existing data.

---

## 17. HTTPS & Network Requests 🌐

ApkPy gives you a simple, non-blocking HTTP client that works identically in both environments. Connect your app to any REST API on the internet using `https.get()`, `https.post()`, `https.put()`, `https.patch()` and `https.delete()` — the five methods that make up full CRUD.

### How It Works Under the Hood

| Environment | What `https` uses |
| :--- | :--- |
| **Hot Previewer** (your computer) | Python's `urllib.request` running in a **background thread** — the UI never freezes |
| **Android Build** (real device) | Native `HttpURLConnection` running in a Java background thread via `AsyncTask` pattern |

### Importing

```python
from apkpy_lib import https, json_get
```

### The Full API

| Method | Description |
| :--- | :--- |
| `https.get(url, headers={}, on_response=callback)` | Reads a resource (GET) |
| `https.post(url, data={}, headers={}, on_response=callback)` | Creates a resource (POST) |
| `https.put(url, data={}, headers={}, on_response=callback)` | Replaces a resource entirely (PUT) |
| `https.patch(url, data={}, headers={}, on_response=callback)` | Partially updates a resource (PATCH) |
| `https.delete(url, headers={}, on_response=callback)` | Deletes a resource (DELETE) |

Together that's **full CRUD** against any REST backend — Supabase, Firebase, Django/FastAPI, anything that speaks HTTP.

The `on_response` callback always receives two arguments:
- `success` — `True` if the HTTP status was 2xx, `False` otherwise.
- `response` — The response body as a plain `String`. On a 4xx/5xx error, this is the **error body** the server sent back (most APIs return a useful JSON error message), so you can show the user what went wrong.

### Basic GET Request

```python
from apkpy_lib import Screen, button, label, run, toast, https, json_get

my_screen = Screen(id="main")
result_lbl = label("Press the button!", id="result", screen=my_screen)

def on_response(success, response):
    if success:
        title = json_get(response, "title")  # Reads the 'title' key from JSON
        result_lbl.set_value(title)
    else:
        toast("Request failed: " + response)

def fetch_data():
    result_lbl.set_value("Loading...")
    https.get("https://jsonplaceholder.typicode.com/todos/1", on_response=on_response)

button("Fetch Data", id="btn", command=fetch_data, screen=my_screen)

if __name__ == "__main__":
    run(start_screen=my_screen)
```

### Using Headers (API Keys & Bearer Tokens)

The `headers` parameter lets you pass any HTTP headers as a Python dictionary. This is how you authenticate with APIs that require tokens or keys:

```python
# Example: Calling an API protected by a Bearer token
def fetch_private_data():
    my_headers = {
        "Authorization": "Bearer YOUR_TOKEN_HERE",
        "Content-Type": "application/json"
    }
    https.get("https://api.example.com/profile", headers=my_headers, on_response=on_response)
```

### POST Request with JSON Body

```python
def send_data():
    payload = {"title": "My Post", "body": "Hello World", "userId": 1}
    headers = {"Content-Type": "application/json"}
    https.post(
        "https://jsonplaceholder.typicode.com/posts",
        data=payload,
        headers=headers,
        on_response=on_post_response
    )
```

### PUT, PATCH & DELETE — Updating and Removing Resources

`https.put` and `https.patch` work exactly like `https.post` (URL, body, headers, callback). `https.delete` works like `https.get` (no body):

```python
def on_done(success, response):
    if success:
        toast("Done!")
    else:
        toast("Failed: " + response)   # response = the server's error body

# PUT — replace the whole resource
def update_profile():
    https.put(
        "https://api.example.com/users/42",
        '{"name": "Alex", "email": "alex@example.com"}',
        headers={"Content-Type": "application/json"},
        on_response=on_done
    )

# PATCH — change only the fields you send
def rename_user():
    https.patch(
        "https://api.example.com/users/42",
        '{"name": "Alex J."}',
        headers={"Content-Type": "application/json"},
        on_response=on_done
    )

# DELETE — remove the resource
def delete_user():
    https.delete("https://api.example.com/users/42", on_response=on_done)
```

> [!NOTE]
> **PATCH on Android:** Android's `HttpURLConnection` doesn't accept `PATCH` natively, so ApkPy automatically falls back to `POST` with the standard `X-HTTP-Method-Override: PATCH` header. Most APIs (Firebase, many REST frameworks) honor it; if yours doesn't, use `PUT` instead. In the Hot Previewer, `PATCH` is always sent natively.

### Real-World Example — Weather App

```python
from apkpy_lib import Screen, button, label, inputs, run, toast, https, json_get

API_KEY = "your_openweathermap_api_key"

weather_screen = Screen(id="weather_screen")
city_input = inputs("Enter city...", type="text", id="city_input", screen=weather_screen)
temp_label  = label("-- °C", id="temp", screen=weather_screen)
desc_label  = label("---", id="desc", screen=weather_screen)

def on_weather(success, response):
    if success:
        temp = json_get(response, "main.temp")
        desc = json_get(response, "weather.0.description")
        temp_label.set_value(f"{temp} °C")
        desc_label.set_value(desc.capitalize())
    else:
        toast("Failed to get weather.")

def get_weather():
    city = city_input.get_value()
    if city != "":
        url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric"
        https.get(url, on_response=on_weather)

button("GET WEATHER", id="btn_weather", command=get_weather, screen=weather_screen)

if __name__ == "__main__":
    run(start_screen=weather_screen)
```

> [!IMPORTANT]
> The `https` API is **always non-blocking**. Requests always run in a background thread, so your UI will never freeze while waiting for a response. The `on_response` callback is always called safely back on the main UI thread.

> [!TIP]
> Use `json_get()` to read specific fields from API responses without complex JSON parsing. The dot-notation supports nested objects (`"main.temp"`) and list indices (`"weather.0.description"`).

---

## 18. Background Services ⏱️

Sometimes your app needs to do work even when nobody's looking at it — syncing data, checking for updates, sending a reminder. The `service` API lets you schedule Python functions to run in the background, with **the exact same code** running in the Hot Previewer (on a timer thread) and on a real Android device (compiled to native `WorkManager`).

### How It Works Under the Hood
- **In the Hot Previewer**: ApkPy runs your function on a background daemon thread on a timer, so you can see the effects (via `storage`, `db`, `notify`, etc.) while testing locally.
- **On Android**: ApkPy compiles your function into a dedicated `Worker` class and schedules it with `WorkManager` — Android's official, battery-friendly API for deferrable background work. `service.every(...)` becomes a `PeriodicWorkRequest`, `service.once(...)` becomes a `OneTimeWorkRequest` with `setInitialDelay(...)`, and constraints (`only_on_wifi`, `only_when_charging`) map to native `Constraints.Builder()` calls.

> [!IMPORTANT]
> Background functions run **without a UI** — they can use `storage`, `db`, `https`, `toast` and `notify`, but **cannot** call `set_value()`/`get_value()` on screen components directly. The recommended pattern is: write the result to `storage`, then read it from `storage` inside your normal UI-refresh function.

### Importing

```python
from apkpy_lib import service
```

### The Full API

| Function | Description |
| :--- | :--- |
| `service.every(run, minutes, id, only_on_wifi=False, only_when_charging=False)` | Schedules `run` to repeat every `minutes` minutes. `id` uniquely identifies the task (required to cancel it later). |
| `service.once(run, after_minutes, id)` | Schedules `run` to execute **exactly once**, after a delay of `after_minutes` minutes. |
| `service.cancel(id)` | Cancels a previously scheduled task by its `id` — works for both `every` and `once`. |

> [!NOTE]
> On Android, `WorkManager` enforces a **minimum interval of 15 minutes** for periodic work. ApkPy automatically respects this in the compiled app — small values like `minutes=0.05` are only meant for quickly testing the flow in the Previewer.

### Full Example — Background Sync + Cancel Button

```python
from apkpy_lib import Screen, button, label, run, toast, notify, storage, service

main_screen = Screen(id="main_screen")
status_label = label("Background sync: waiting for first run...", id="status", screen=main_screen)

def atualizar_ui():
    status_label.set_value(storage.get("sync_status", "Background sync: waiting for first run..."))

def sincronizar_em_background():
    count = int(storage.get("sync_count", "0")) + 1
    storage.set("sync_count", str(count))
    storage.set("sync_status", f"Background sync #{count} completed")
    notify("Sync complete", f"Background sync #{count} finished successfully.", id="bg_sync")

def parar_sincronizacao():
    service.cancel(id="bg_sync")
    storage.set("sync_status", "Background sync: stopped by the user")
    toast("Background sync cancelled.")
    atualizar_ui()

button("STOP BACKGROUND SYNC", id="btn_stop", command=parar_sincronizacao, screen=main_screen)

# Repeats every 15 minutes — only when on Wi-Fi and charging.
service.every(run=sincronizar_em_background, minutes=15, id="bg_sync",
              only_on_wifi=True, only_when_charging=True)

atualizar_ui()

if __name__ == "__main__":
    run(start_screen=main_screen)
```

---

## 19. System Notifications 🔔

`notify()` shows a real notification in the phone's **notification bar** — unlike `toast()`, which only appears while the app is open, a notification stays visible (and can re-open the app) even after the user has switched to another app or locked the phone. It's the natural companion to `service`: tell the user something happened, without needing the app in the foreground.

### How It Works Under the Hood
- **In the Hot Previewer**: shows a native OS-style popup so you can preview the title and message.
- **On Android**: compiles to `NotificationCompat.Builder` + `NotificationManager`, including a notification channel (required on Android 8+) and the runtime `POST_NOTIFICATIONS` permission request (required on Android 13+).

### Importing

```python
from apkpy_lib import notify
```

### Usage

```python
notify("New message", "You have 3 unread notifications", id="inbox")
notify("Sync complete", f"Background sync #{count} finished successfully.", id="bg_sync")
```

The `id` parameter uniquely identifies the notification — sending another `notify()` with the same `id` updates/replaces the existing notification instead of stacking a new one.

---

## 20. Native Sharing 📤

`share()` opens Android's native **share sheet** — the same system menu you see when you tap the "share" icon in any app — letting the user send text to WhatsApp, Email, SMS, Bluetooth, or any other app that accepts shared text.

### How It Works Under the Hood
- **In the Hot Previewer**: opens a popup that mimics the Android share sheet, listing common apps. Picking one just closes the popup (it doesn't actually send anything) — it's there so you can test the flow visually.
- **On Android**: compiles to `Intent.ACTION_SEND` wrapped in `Intent.createChooser(...)`, with `FLAG_ACTIVITY_NEW_TASK` so it works from both regular screens **and** background services.

### Importing

```python
from apkpy_lib import share
```

### Usage

```python
share("Check out this app I built with ApkPy! 🚀")
share(f"My app already has {total} registered users!", title="Share result")
```

| Parameter | Description |
| :--- | :--- |
| `text` | The text content to share (required). |
| `title` | Optional title shown at the top of the share sheet. Defaults to `"Share via"`. |

---

## 21. Clipboard 📋

`clipboard.copy()` copies text to the device's system clipboard — perfect for sharing links, generated codes, or query results that the user might want to paste somewhere else.

### How It Works Under the Hood
- **In the Hot Previewer**: writes to the **real OS clipboard** via Tkinter — pressing `Ctrl+V` in any other app on your computer pastes the actual copied text.
- **On Android**: compiles to native `ClipboardManager` / `ClipData.newPlainText(...)`.

### Importing

```python
from apkpy_lib import clipboard
```

### Usage

```python
clipboard.copy("https://my-app-link.example.com")
clipboard.copy(f"My app has {total} registered users!")
toast("Copied to clipboard!")
```

---

## 22. Camera & Gallery 📸

`camera.capture()` opens the device's native camera app to take a photo, and `gallery.pick()` opens the native image picker to choose an existing photo — both return the result asynchronously through an `on_result(success, path)` callback, following the exact same async pattern as `https.get/post`.

### How It Works Under the Hood
- **On Android**: `camera.capture()` launches the system camera app via `ActivityResultContracts.TakePicture()`, requesting the `CAMERA` runtime permission automatically and writing the photo to a `content://` URI through a `FileProvider` (so the camera app can write into your app's private storage safely). `gallery.pick()` launches the system image picker via `ActivityResultContracts.GetContent()` — this is scoped-storage compliant and **does not require any storage permission** on modern Android. Both deliver the resulting `content://` path to your `on_result` callback once the user finishes.
- **In the Hot Previewer**: your computer obviously doesn't have a "camera app" or a "photo gallery" — so both functions simulate the flow by opening your OS's file explorer (filtered to image files). Whatever image file you pick is treated as "the photo you took" / "the image you chose", and your `on_result` callback receives its real path on disk. This keeps your Python code **100% identical** between testing on your PC and running on a real phone — only what happens "behind the scenes" changes.

### Importing

```python
from apkpy_lib import camera, gallery
```

### Usage

```python
def foto_tirada(success, path):
    if success:
        lbl_foto.set_value(f"Photo taken: {path}")
        toast("Photo captured!")
    else:
        toast("No photo was taken.")

def imagem_escolhida(success, path):
    if success:
        lbl_foto.set_value(f"Picked from gallery: {path}")
        toast("Image picked!")
    else:
        toast("No image was picked.")

button("TAKE PHOTO", command=lambda: camera.capture(on_result=foto_tirada), screen=main)
button("PICK FROM GALLERY", command=lambda: gallery.pick(on_result=imagem_escolhida), screen=main)
```

| Function | Description |
| :--- | :--- |
| `camera.capture(on_result=callback)` | Opens the native camera, takes a photo, and calls `callback(success, path)`. Auto-declares the `CAMERA` permission and sets up `FileProvider` for you — no manifest editing required. |
| `gallery.pick(on_result=callback)` | Opens the native image picker and calls `callback(success, path)` with the chosen image's path. No extra permissions needed. |

> **Note:** `path` is a `content://` URI on Android (use it directly with `image()`, `https.post` for uploads, etc.) and a regular filesystem path in the Previewer — your callback code doesn't need to know the difference.

---

## 23. Alert & Confirm Dialogs 🗨️

ApkPy provides two functions for showing native dialog boxes — identical code between the Hot Previewer and a real Android device.

### `alert(title, message)` — Informational Dialog

Shows a dialog with a title, a message, and an OK button. Use it to inform the user of something that doesn't require a decision.

```python
from apkpy_lib import alert

def save_data():
    # ... save logic ...
    alert("Saved!", "Your data has been saved successfully.")

def show_error():
    alert("Connection Error", "Could not reach the server. Please check your internet connection.")
```

### `confirm(title, message, on_result=callback)` — Confirmation Dialog

Shows a dialog with OK and Cancel buttons. Calls `on_result(True)` if the user taps OK, `on_result(False)` if they tap Cancel. Follows the same async `on_result` pattern as `camera.capture` and `gallery.pick`.

```python
from apkpy_lib import confirm, storage, toast

def on_delete_confirmed(confirmed):
    if confirmed:
        storage.clear()
        toast("All data deleted.")
    else:
        toast("Cancelled.")

def delete_all():
    confirm(
        "Delete all data?",
        "This will permanently erase all saved data. This action cannot be undone.",
        on_result=on_delete_confirmed
    )
```

### How It Works Under the Hood

| Environment | What `alert` / `confirm` use |
| :--- | :--- |
| **Hot Previewer** (your computer) | A native Tkinter dialog window with English "OK" / "Cancel" buttons |
| **Android Build** (real device) | Native `androidx.appcompat.app.AlertDialog.Builder` — the standard Android dialog |

### Full Example

```python
from apkpy_lib import Screen, button, label, inputs, toast, alert, confirm, storage, run

main = Screen(id="main")

label("Settings", id="title", screen=main)
status = label("No action yet.", id="status", screen=main)
name_input = inputs("Your name", id="inp_name", screen=main)

def save():
    name = name_input.get_value()
    if name:
        storage.set("name", name)
        alert("Saved!", f"The name '{name}' was saved successfully.")
        status.set_value(f"Saved: {name}")
    else:
        alert("Missing name", "Please type a name before saving.")

def on_clear_confirmed(confirmed):
    if confirmed:
        storage.clear()
        status.set_value("All data cleared.")
        toast("Done!")
    else:
        status.set_value("Cancelled.")

def clear_data():
    confirm("Clear all data?", "This will erase everything. Are you sure?", on_result=on_clear_confirmed)

button("SAVE NAME",   id="btn_save",  command=save,       screen=main)
button("CLEAR DATA",  id="btn_clear", command=clear_data, screen=main)

style = """
main { background-color: #0F172A; }
title { color: #F8FAFC; font-size: 22px; font-weight: bold; margin-top: 28px; }
status { color: #38BDF8; font-size: 13px; margin-bottom: 8px; }
inp_name { background-color: #1E293B; color: #F8FAFC; border-radius: 10px; border-color: #334155; border-width: 1px; focus-border-color: #818CF8; }
btn_save { background-color: #4F46E5; color: #F8FAFC; border-radius: 12px; font-weight: bold; margin-top: 6px; pressed-color: #3730A3; }
btn_clear { background-color: #1E293B; color: #F87171; border-radius: 12px; font-weight: bold; margin-top: 6px; border-color: #F87171; border-width: 1px; pressed-color: #0F172A; }
"""

if __name__ == "__main__":
    run(start_screen=main)
```

---

## 24. Location / GPS 📍

`location.get_current(on_result=callback)` reads the device's current location and returns the **latitude, longitude and city name** asynchronously through an `on_result(success, lat, lng, city)` callback — the same async pattern as `camera.capture` and `https`.

### How It Works Under the Hood

| Environment | What `location` uses |
| :--- | :--- |
| **Hot Previewer** (your computer) | Your PC has no GPS, so it opens a small dialog where you type the coordinates to simulate (defaults to Lisbon `38.7223,-9.1393`). The city name is then resolved via OpenStreetMap (Nominatim) in a background thread. |
| **Android Build** (real device) | Native `LocationManager.getLastKnownLocation()` (GPS, falling back to network), then `android.location.Geocoder` to turn the coordinates into a city name — off the UI thread. The `ACCESS_FINE_LOCATION` and `INTERNET` permissions are declared automatically, and `ACCESS_FINE_LOCATION` is requested at runtime — no manifest editing needed. |

Your Python code is **100% identical** in both environments.

### Importing

```python
from apkpy_lib import location
```

### Usage

```python
from apkpy_lib import Screen, button, label, toast, location, run

main = Screen(id="main")
coords_lbl = label("Tap the button to locate me", id="coords", screen=main)

def on_location(success, lat, lng, city):
    if success:
        coords_lbl.set_value(f"{city} ({lat}, {lng})")
        toast("Location found! 📍")
    else:
        toast("Couldn't get your location.")

button("WHERE AM I?", id="btn_loc",
       command=lambda: location.get_current(on_result=on_location),
       screen=main)

if __name__ == "__main__":
    run(start_screen=main)
```

| Function | Description |
| :--- | :--- |
| `location.get_current(on_result=callback)` | Reads the current position and calls `callback(success, lat, lng, city)`. Auto-declares `ACCESS_FINE_LOCATION` + `INTERNET` and requests location at runtime. |

The `on_result` callback always receives four arguments:
- `success` — `True` if a location was obtained, `False` otherwise.
- `lat` — latitude as a **string**, e.g. `"38.7223"` (or `""` on failure).
- `lng` — longitude as a **string**, e.g. `"-9.1393"` (or `""` on failure).
- `city` — the resolved city name, e.g. `"Lisbon"`. May be `""` if the device has no geocoding backend or there is no network — `success` is still `True` in that case, since the coordinates were obtained.

> [!TIP]
> `lat` and `lng` are strings (like every `get_value()`). Convert with `float(lat)` before doing distance maths. Raw GPS only ever gives you numbers — the human-readable `city` comes from reverse geocoding the coordinates.

> [!NOTE]
> On Android, the first tap may return `success=False` while the permission dialog is showing — the user grants it, then taps again. This is the same flow as `camera.capture`.

---

## 25. Crypto / Password Hashing 🔒

Anyone can decompile an APK. If your app stores a password as plain text in `storage` (SharedPreferences) or in the SQLite `db`, it is exposed to anyone with a free decompiler. The built-in `crypto` module solves this with **salted password hashing** — and you don't need to import `hashlib`: it's part of ApkPy and compiles to native Java (`java.security.MessageDigest` + `SecureRandom`), with zero extra dependencies.

```python
from apkpy_lib import Screen, label, inputs, button, toast, storage, crypto, run

login = Screen(id="login")

pw_in  = inputs("Password", id="pw_in", type="password", screen=login)
status = label("Register first.", id="status", screen=login)

def register():
    senha = pw_in.get_value()
    # Stores "sha256$<random salt>$<hash>" — never the password itself.
    storage.set("pw_hash", crypto.hash_password(senha))
    toast("Registered!")

def do_login():
    senha = pw_in.get_value()
    ok = crypto.verify_password(senha, storage.get("pw_hash", ""))
    if ok:
        status.set_value("Welcome back!")
    else:
        status.set_value("Wrong password.")

button("REGISTER", id="btn_reg", screen=login, command=register)
button("LOGIN",    id="btn_log", screen=login, command=do_login)

if __name__ == "__main__":
    run(start_screen=login)
```

| Function | Description |
| :--- | :--- |
| `crypto.hash_password(password, algo="sha256", iterations=200000)` | Generates a random 16-byte salt and returns `"pbkdf2-algo$iterations$salt$hash"`. Uses **PBKDF2 key stretching**: every brute-force guess costs 200,000 hashes instead of 1, making GPU cracking ~200,000× slower. Use `algo="sha512"` for SHA-512. Store the result directly in `storage` or `db`. |
| `crypto.verify_password(password, stored)` | Returns `True` if the password matches a value created by `hash_password`, `False` otherwise (including malformed/empty stored values). Uses a constant-time comparison. |
| `crypto.encrypt(text)` | Two-way encryption with a per-device key. Returns `"enc1$…"` — store it in `storage` or `db` and read it back with `decrypt`. On Android, AES-256-GCM with the key in the **Android Keystore** (hardware-backed, non-extractable — even with root). |
| `crypto.decrypt(stored)` | Decrypts a value created by `encrypt`. Returns `""` if the value was tampered with, is malformed, or was created on another device. |

### How It Works Under the Hood

| Environment | What `crypto` uses |
| :--- | :--- |
| **Hot Previewer** (PC) | Python's `hashlib.pbkdf2_hmac` + `os.urandom` + `hmac.compare_digest`; encryption via a hash-based stream cipher + HMAC with a local device key file. |
| **Android** (`apkpy build`) | PBKDF2 via native `javax.crypto.Mac` (HMAC-SHA256/512), AES-256-GCM via `javax.crypto.Cipher` with an **Android Keystore** key — no external libraries, no new permissions. |

The hash format `"pbkdf2-algo$iterations$salt_hex$hash_hex"` is **bit-for-bit identical on both platforms** — a hash created in the Previewer verifies on Android and vice-versa. Encrypted values (`enc1$…`) are **per-device** by design: stealing the file is useless without the device's key.

### What this protects you from (honest threat model)

| Attack | Protected? |
| :--- | :--- |
| Decompiling the APK looking for passwords/keys | ✅ Nothing sensitive is in the code |
| Stealing `apkpy_storage.xml` / the SQLite file | ✅ Storage is encrypted automatically; db fields you wrapped in `crypto.encrypt` are unreadable |
| GPU brute-force against stolen password hashes | ✅ PBKDF2 makes each guess ~200,000× more expensive |
| Brute-forcing AES-encrypted values | ✅ Impossible in practice — the key is 256 random bits, not derived from a password |
| SQL injection through an input field | ✅ Use `db.execute(sql, [params])` / `db.query(sql, [params])` — values are bound by SQLite, never pasted into the SQL (see section 16) |
| Trivially weak passwords (e.g. `1234`) + stolen hash | ⚠️ PBKDF2 slows the attack massively but can't make a 4-digit PIN safe — encourage strong passwords |
| An attacker using the **unlocked phone with the app open** | ❌ No cryptography protects against that |

> [!IMPORTANT]
> Because the salt is random, **every call to `hash_password` produces a different string** — even for the same password. Never compare two hashes directly; always use `crypto.verify_password`.

> [!TIP]
> Hash what you only need to *verify* (passwords, PINs). Encrypt what you need to *read back* (notes, emails, tokens). `storage.set()` already encrypts automatically — use `crypto.encrypt` for sensitive **db** fields.

---

## 26. For Loops — real Python iteration 🔁

`for` loops now compile to native Java. Write normal Python; the same loop runs in the Hot Previewer and on Android.

**Three forms are supported:**

```python
# 1. Over a list (literal or variable)
for cidade in ["Lisboa", "Porto", "Braga"]:
    toast(cidade)

frutas = ["Maçã", "Pera"]
for f in frutas:
    db.execute("INSERT INTO itens (nome) VALUES (?)", [f])

# 2. Counting with range()
for i in range(5):          # 0..4  — range(2, 8) also works
    status.set_value(f"step {i}")

# 3. Over rows from the database or an API  ⭐
rows = db.query("SELECT nome, idade FROM pessoas")
for row in rows:
    toast(f"{row['nome']} tem {row['idade']} anos")
```

Form 3 is the big one: **`db.query()` results and `https` responses are iterable row-by-row**. Inside the loop, `row["column"]` reads each field — in the Previewer it's a real dict, on Android it compiles to safe JSON access (`_jsonGet`). The same works in an HTTP callback:

```python
def on_posts(ok, resp):
    if ok:
        for post in resp:                 # resp = JSON array from the API
            db.execute("INSERT INTO cache (title) VALUES (?)", [post["title"]])

https.get("https://example.com/api/posts", on_response=on_posts)
```

Loops work inside callbacks, at module level (they run on app start), nested, and combined with `if`/`else` in the body. **`break` and `continue` are supported** — they compile to native Java `break;`/`continue;`:

```python
for i in range(100):
    if i == 5:
        break            # stops the loop
    if i == 2:
        continue         # skips to the next item
    status.set_value(f"i = {i}")
```

| Where the data comes from | What each item is |
| :--- | :--- |
| List literal / list variable | The value as a string |
| `range(n)` / `range(a, b)` | The counter as a string |
| `db.query(...)` | A row — read fields with `row["column"]` |
| `https` response (JSON array) | An object — read fields with `item["field"]` |

> [!NOTE]
> If the JSON isn't an array (an object, plain text, an error page), the loop simply runs **zero times** on both platforms — it never crashes the app. `for` over dicts is not supported yet.

---

## 27. random — randomness bundled in apkpy_lib 🎲

`random` ships **with apkpy_lib** — import it straight from the library, no need for Python's stdlib `random`. It compiles to native Android (`java.util.Random` / `Math.random()`), with **no runtime bundled**.

```python
from apkpy_lib import random

n     = random.randint(1, 6)                      # integer in [a, b] (both ends included)
prize = random.choice(["Gold", "Silver", "Bronze"])  # random element of a list
p     = random.random()                           # float in [0.0, 1.0)
```

It pairs naturally with functions that `return` a value:

```python
def roll_dice():
    return random.randint(1, 6)

def play():
    result.set_value("You rolled " + str(roll_dice()))
```

| Call | Android equivalent |
| :--- | :--- |
| `random.randint(a, b)` | `java.util.Random().nextInt(...)`, result in `[a, b]` |
| `random.choice(list)` | a random element of the list |
| `random.random()` | `Math.random()` (0.0–1.0) |

> [!NOTE]
> Random **values won't match** between the Previewer (Python's `random`) and Android (`java.util.Random`) — and that's correct. Unlike arithmetic, the guarantee here isn't "same number", it's "both sides produce a valid random result in range".

---

## 28. Preview on any device size — `device(...)` 📱

A **Previewer-only** helper to see your app at different screen sizes while you build. It does **nothing on Android** — there the real screen already *is* the device, so `device(...)` is stripped from the build and never changes the generated APK.

```python
from apkpy_lib import device

device("Pixel 8")         # resize the preview window to that model
device("Pixel 9 Pro XL")  # a bigger screen
device("fullscreen")      # borderless full screen (press Esc to exit)
device("maximized")       # maximised window, keeps the title bar (X to close)
```

Call it once at module level (before `run()`). Without it, the preview defaults to the **Pixel 9**. Every Pixel from the **Pixel 4 to the Pixel 10 Pro** is accepted — models that share a screen size map to the same dimensions. In `fullscreen` / `maximized`, the content stays in a centred phone-width column.

---

## 29. More Python, natively 🐍

Several core Python constructs now transpile to native Java — write normal Python, get the same behaviour in the Hot Previewer and on Android.

### f-strings — `f"...{value}..."`

Drop variables and expressions straight into text, instead of gluing strings with `+` and `str()`:

```python
nome = "Ana"
preco = 12.5
recibo.set_value(f"Olá {nome}! Total: {preco:.2f} €")   # → "Olá Ana! Total: 12.50 €"
```

- `{var}` inserts the value; `{a + b}` runs the arithmetic inline.
- A format spec like `{preco:.2f}` fixes the decimals (great for money) — it compiles to `String.format(Locale.US, "%.2f", ...)`, so the result matches Python exactly.

### `while` loops

```python
i = 0
total = ""
while i < 5:
    total += str(i)
    i += 1
status.set_value(total)        # "01234"
```

Conditions support the same set as `if` (`<`, `>`, `==`, `and`/`or`/`not`, …). `break` and `continue` work inside. On Android, an automatic safety limit stops an accidental infinite loop from freezing the UI thread.

### Augmented assignment — `+=`, `-=`, `*=`, `/=`, `%=`

```python
total = 0
total += 5      # same as: total = total + 5
total -= 2
texto = ""
texto += "!"    # with strings it concatenates
```

It works as a loop counter (`i += 1`) and reads state straight from a label for a no-fuss counter:

```python
def mais():
    n = int(mostrador.get_value())
    n += 1
    mostrador.set_value(str(n))
```

### `.isdigit()` — validate input before converting

`int("")` and `int("abc")` crash on **both** platforms — but the Previewer just logs and survives, while Android **force-closes**. Always check first:

```python
valor = campo.get_value()
if valor.isdigit():
    n = int(valor)
    resultado.set_value(f"O dobro é {n * 2}")
else:
    resultado.set_value("Escreve um número válido!")
```

Compiles to `String.valueOf(valor).matches("\\d+")` — same truth value as Python (`"123"` → yes, `"12a"` / `""` → no).

### `.startswith(x)` / `.endswith(x)` — check prefixes & suffixes

Two string checks for validating and filtering — perfect for emails, links, codes and file names. Use them inside an `if` (like `.isdigit()`):

```python
url = url_in.get_value()
if not url.startswith("https://"):
    aviso.set_value("⚠️ Não é seguro (falta https://)")
elif url.endswith(".pt"):
    aviso.set_value("✅ Site português 🇵🇹")
else:
    aviso.set_value("✅ Seguro")
```

They map **directly** to Java's own `.startsWith(...)` / `.endsWith(...)` — so the result is identical to Python on both sides.

---

## 30. datetime — date & time bundled in apkpy_lib 🕐

Like `random`, `datetime` ships **with apkpy_lib** — no Python stdlib import. Everything returns a string and compiles to native `SimpleDateFormat` on Android.

```python
from apkpy_lib import datetime

datetime.now()     # "2026-06-16 14:30:45"
datetime.date()    # "2026-06-16"
datetime.time()    # "14:30:45"
datetime.hour()    # "14"   (also year/month/day/minute/second)
```

A greeting based on the current hour:

```python
def atualizar():
    hora = int(datetime.hour())
    if hora < 12:
        saudacao.set_value("Bom dia ☀️")
    elif hora < 20:
        saudacao.set_value("Boa tarde 🌤️")
    else:
        saudacao.set_value("Boa noite 🌙")
    relogio.set_value(datetime.date() + "\n" + datetime.time())
```

| Call | Android equivalent (pattern) |
| :--- | :--- |
| `datetime.now()` | `SimpleDateFormat("yyyy-MM-dd HH:mm:ss")` |
| `datetime.date()` | `SimpleDateFormat("yyyy-MM-dd")` |
| `datetime.time()` | `SimpleDateFormat("HH:mm:ss")` |
| `datetime.hour()` / `minute()` / `second()` | `HH` / `mm` / `ss` |
| `datetime.year()` / `month()` / `day()` | `yyyy` / `MM` / `dd` |

> [!NOTE]
> The **format** is identical on both sides, but the exact second won't match between the Previewer and Android — they're two clocks read a moment apart, just like `random`.

---

## 31. `from apkpy_lib import *` 📦

Import the whole library in one line:

```python
from apkpy_lib import *

home = Screen(id="home")
label("Hi", id="t", screen=home)
run(start_screen=home)
```

It brings every public name — `Screen`, `label`, `button`, `inputs`, `container`, `list_view`, `run`, `storage`, `crypto`, `db`, `https`, `random`, `datetime`, and the rest. (`container` — the box for grouping/nesting components and laying them out in a row with `display: flex; flex-direction: row` — is now exported too.)

---

## 32. Ship your app — name, icon & signed releases 📦🔑

Two pieces turn your code into a real, publishable app: an **`apkpy.toml`** that gives it an identity, and **`apkpy release`** that produces a *signed* build for the Play Store.

### `apkpy.toml` — your app's identity

Run `apkpy init` once to create it, then edit:

```toml
[app]
name = "Link Checker"                         # shown under the icon on the phone
application_id = "com.mycompany.linkchecker"   # unique Play Store id (don't change after publishing)
version_name = "1.0"                           # the version users see
version_code = 1                               # bump by 1 on every release
icon = "icon.png"                              # optional: a square PNG (needs Pillow)
```

`apkpy run` / `apkpy build` pick it up automatically — the `name` becomes the launcher label, the `icon` is generated in every density, and `application_id` / `version_*` flow into the build. If there's no `apkpy.toml` (and no `--name`), `apkpy run` simply **asks you for the name** the first time and saves it.

> Before this, every app was hardcoded to show "ApkPy App" under the icon — now you control it.

### `apkpy release` — a signed build for the Play Store

```bash
apkpy release          # signed .apk (installs on any phone — no "unsigned" error)
apkpy release --aab    # .aab App Bundle — the format Google Play requires
```

The first time, ApkPy creates a **signing key** (keystore) for your app and stores it under `~/.apkpy/keystores/`. Every future release reuses the **same key** — which is exactly what the Play Store needs to accept updates.

> [!IMPORTANT]
> **Back up that keystore file.** If you lose it, you can never publish another update of the same app on the Play Store. It's a binary file — opening it in a text editor shows gibberish (that's normal); inspect it properly with `keytool -list -v -keystore <path>`.

### A note on the install warnings

When you **sideload** a signed `.apk` (install it directly, not from the Play Store), Android still shows an "unknown developer" / Play Protect warning and offers to scan it. **That's normal for every app installed outside the Play Store** — signing makes the app installable and store-ready, but the warnings only disappear once the app is actually published on the store and installed from there.

---

## 🤝 Community & Support

**Found a bug?** [Open an issue on GitHub!](https://github.com/apkpy-project/repo-apkpy/issues)

**Want to contribute?** We are looking for contributors to expand our Native Component library! Join us in making Python a first-class citizen for Android development.

---

## 📚 Full Documentation

This README covers the core features, but ApkPy has much more to offer. Our full documentation goes deeper into:

- Advanced multi-screen architectures & navigation stacks
- Deploying to the Google Play Store
- Customizing `build.gradle` and `AndroidManifest.xml`
- Using GitHub Actions to build APKs in the cloud (no local SDK needed)
- Full API reference for every component and CSS property

**👉 [View the ApkPy Repository on GitHub](https://github.com/apkpy-project/repo-apkpy)**

---

*Made with ❤️ for the Python Community.*
