548 lines
26 KiB
Markdown
548 lines
26 KiB
Markdown
# Activity & ActivityManager Migration Guide
|
|
|
|
This document explains the refactoring from the original per-activity render task model to the centralized `ActivityManager` introduced in [PR #1016](https://github.com/crosspoint-reader/crosspoint-reader/pull/1016). It covers the architectural differences, what changed for activity authors, and the FreeRTOS task and locking model that underpins the system.
|
|
|
|
## Overview of Changes
|
|
|
|
| Aspect | Old Model | New Model |
|
|
|--------|-----------|-----------|
|
|
| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` |
|
|
| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` |
|
|
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
|
|
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
|
|
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
|
|
| Forward flow | Parent stays on stack (`pushActivity`) | Parent destroyed on forward flow (`replaceWith*` + `ReturnHint`) |
|
|
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
|
|
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
|
|
|
|
## Architecture
|
|
|
|
### Old Model: Per-Activity Render Tasks
|
|
|
|
Each activity created its own FreeRTOS render task on entry and destroyed it on exit:
|
|
|
|
```text
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Main Task (Arduino loop) │
|
|
│ ┌───────────────────────────────────────────────────┐ │
|
|
│ │ currentActivity->loop() │ │
|
|
│ │ ├── handle input │ │
|
|
│ │ ├── update state (under RenderLock) │ │
|
|
│ │ └── requestUpdate() ──notify──► Render Task │ │
|
|
│ │ (per-activity)│ │
|
|
│ │ 8KB stack │ │
|
|
│ │ owns mutex │ │
|
|
│ └───────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ActivityWithSubactivity: │
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Parent │────►│ SubActivity │ │
|
|
│ │ (has render │ │ (has own │ │
|
|
│ │ task) │ │ render task) │ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
Problems with this approach:
|
|
|
|
- **8KB per render task**: Each activity allocated an 8KB FreeRTOS stack for its render task, even though only one renders at a time
|
|
- **Dangerous deletion patterns**: `exitActivity()` + `enterNewActivity()` in callbacks led to `delete this` situations where the caller was destroyed while its code was still on the stack
|
|
- **Subactivity coupling**: Parents stored callbacks to child results, creating tight coupling and lifetime hazards
|
|
|
|
### New Model: Centralized ActivityManager
|
|
|
|
A single `ActivityManager` owns the render task and manages an activity stack:
|
|
|
|
```text
|
|
┌──────────────────────────────────────────────────────────┐
|
|
│ Main Task (Arduino loop) │
|
|
│ │
|
|
│ activityManager.loop() │
|
|
│ │ │
|
|
│ ├── currentActivity->loop() │
|
|
│ │ ├── handle input │
|
|
│ │ ├── update state (under RenderLock) │
|
|
│ │ └── requestUpdate() │
|
|
│ │ │
|
|
│ ├── process pending actions (Push / Pop / Replace) │
|
|
│ │ │
|
|
│ └── if requestedUpdate: ──notify──► Render Task │
|
|
│ (single, shared) │
|
|
│ 8KB stack │
|
|
│ global mutex │
|
|
│ │
|
|
│ Activity Stack: │
|
|
│ ┌──────────┬──────────┬──────────┐ ┌──────────┐ │
|
|
│ │ Home │ Settings │ Wifi │ │ Keyboard │ │
|
|
│ │ (stack) │ (stack) │ (stack) │ │ (current)│ │
|
|
│ └──────────┴──────────┴──────────┘ └──────────┘ │
|
|
│ stackActivities[] currentActivity │
|
|
└──────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Migration Checklist
|
|
|
|
### 1. Change Base Class
|
|
|
|
If your activity extended `ActivityWithSubactivity`, change it to extend `Activity`:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
class MyActivity final : public ActivityWithSubactivity {
|
|
MyActivity(GfxRenderer& r, MappedInputManager& m, std::function<void()> goBack)
|
|
: ActivityWithSubactivity("MyActivity", r, m), goBack(goBack) {}
|
|
};
|
|
|
|
// AFTER
|
|
class MyActivity final : public Activity {
|
|
MyActivity(GfxRenderer& r, MappedInputManager& m)
|
|
: Activity("MyActivity", r, m) {}
|
|
};
|
|
```
|
|
|
|
Note that navigation callbacks like `goBack` are no longer stored — use `finish()`, `onGoHome()`, or a direct `activityManager.goHome()` / `goTo*()` / `replaceWith*()` call instead.
|
|
|
|
### 2. Replace Navigation Functions
|
|
|
|
The free functions `exitActivity()` / `enterNewActivity()` in `main.cpp` are gone. Use `ActivityManager` methods:
|
|
|
|
```cpp
|
|
// BEFORE (in main.cpp or via stored callbacks)
|
|
exitActivity();
|
|
enterNewActivity(new SettingsActivity(renderer, mappedInput, onGoHome));
|
|
|
|
// AFTER (from any Activity method)
|
|
activityManager.goToSettings();
|
|
// or for arbitrary navigation:
|
|
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
|
|
```
|
|
|
|
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.). See the "Navigation Flow" section after the checklist for when to use replace vs push, and how the `ReturnHint` mechanism restores a parent's prior state without keeping it resident.
|
|
|
|
### 3. Replace Subactivity Pattern
|
|
|
|
The `enterNewActivity()` / `exitActivity()` subactivity pattern is replaced by a stack with typed results:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
void MyActivity::launchWifi() {
|
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
|
[this](bool connected) { onWifiDone(connected); }));
|
|
}
|
|
// Child calls: onComplete(true); // triggers callback, which may call exitActivity()
|
|
|
|
// AFTER
|
|
void MyActivity::launchWifi() {
|
|
startActivityForResult(
|
|
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
|
[this](const ActivityResult& result) {
|
|
if (result.isCancelled) return;
|
|
auto& wifi = std::get<WifiResult>(result.data);
|
|
onWifiDone(wifi.connected);
|
|
});
|
|
}
|
|
// Child calls:
|
|
// setResult(WifiResult{.connected = true, .ssid = ssid});
|
|
// finish();
|
|
```
|
|
|
|
Key differences:
|
|
|
|
- **`startActivityForResult()`** pushes the current activity onto the stack and launches the child
|
|
- **`setResult()`** stores a typed result on the child activity
|
|
- **`finish()`** signals the manager to pop the child, call the result handler, and resume the parent
|
|
- The parent is never deleted during this process — it's safely stored on the stack
|
|
|
|
### 4. Update `render()` Signature
|
|
|
|
The `RenderLock` type changed from `Activity::RenderLock` (inner class) to standalone `RenderLock`:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
void render(Activity::RenderLock&&) override;
|
|
|
|
// AFTER
|
|
void render(RenderLock&&) override;
|
|
```
|
|
|
|
Include `RenderLock.h` if not transitively included via `Activity.h`.
|
|
|
|
### 5. Update `onEnter()` / `onExit()`
|
|
|
|
Activities no longer create or destroy render tasks:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
void MyActivity::onEnter() {
|
|
Activity::onEnter(); // created render task + logged
|
|
// ... allocate resources
|
|
requestUpdate();
|
|
}
|
|
void MyActivity::onExit() {
|
|
// ... free resources
|
|
Activity::onExit(); // acquired RenderLock, deleted render task
|
|
}
|
|
|
|
// AFTER
|
|
void MyActivity::onEnter() {
|
|
Activity::onEnter(); // just logs
|
|
// ... allocate resources
|
|
requestUpdate();
|
|
}
|
|
void MyActivity::onExit() {
|
|
// ... free resources
|
|
Activity::onExit(); // just logs
|
|
}
|
|
```
|
|
|
|
The render task lifecycle is handled entirely by `ActivityManager::begin()`.
|
|
|
|
### 6. Update `requestUpdate()` Calls
|
|
|
|
The signature changed to accept an `immediate` flag:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
void requestUpdate(); // always immediate notification to per-activity render task
|
|
|
|
// AFTER
|
|
void requestUpdate(bool immediate = false);
|
|
// immediate=false (default): deferred until end of current loop iteration
|
|
// immediate=true: sends notification to render task right away
|
|
```
|
|
|
|
**When to use `immediate`**: Almost never. Deferred updates are batched — if `loop()` triggers multiple state changes that each call `requestUpdate()`, only one render happens. Use `immediate` only when you need the render to start before the current function returns (e.g., before a blocking network call).
|
|
|
|
**`requestUpdateAndWait()`**: Blocks the calling task until the render completes. Use sparingly — it's designed for cases where you need the screen to reflect new state before proceeding (e.g., showing "Checking for update..." before calling a network API).
|
|
|
|
### 7. Remove Stored Navigation Callbacks
|
|
|
|
Old activities often stored `std::function` callbacks for navigation:
|
|
|
|
```cpp
|
|
// BEFORE
|
|
class SettingsActivity : public ActivityWithSubactivity {
|
|
const std::function<void()> goBack; // stored callback
|
|
const std::function<void()> goHome; // stored callback
|
|
public:
|
|
SettingsActivity(GfxRenderer& r, MappedInputManager& m,
|
|
std::function<void()> goBack, std::function<void()> goHome)
|
|
: ActivityWithSubactivity("Settings", r, m), goBack(goBack), goHome(goHome) {}
|
|
};
|
|
|
|
// AFTER
|
|
class SettingsActivity : public Activity {
|
|
public:
|
|
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
|
|
: Activity("Settings", r, m) {}
|
|
// Use finish() to go back (pops the stack, or returns via ReturnHint if empty),
|
|
// onGoHome() to exit the flow ("up and out"), or activityManager.goHome() for a hard reset.
|
|
};
|
|
```
|
|
|
|
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
|
|
|
|
## Navigation Flow
|
|
|
|
### Push vs Replace: Memory Matters
|
|
|
|
On an ESP32-C3, heap fragmentation is a real constraint — especially when the next activity is heavy (EPUB reader, TLS sync). There are two ways to launch a new activity, and the choice matters:
|
|
|
|
| Call | Current activity | Stack | When to use |
|
|
|----------------------------------------------------|---------------------------------|------------------|-------------------------------------------------------------|
|
|
| `replaceActivity()` / `goTo*()` / `replaceWith*()` | **destroyed** (onExit + delete) | cleared | Forward flow: the caller has no reason to stay resident |
|
|
| `pushActivity()` / `startActivityForResult()` | kept alive on stack | parent preserved | Modal result flow: the caller needs to resume with a result |
|
|
|
|
**Default to replace.** Push is only correct when you need to deliver a result back to a still-living parent (keyboard entry, confirmation dialog, chapter picker, etc.). For plain one-way transitions — opening a book, going to settings, switching tabs — use replace so the parent's memory is freed before the next activity runs.
|
|
|
|
### The `ReturnHint` Pattern
|
|
|
|
Replace destroys the parent, but the user still expects Back to return "where they came from" — not always Home. To bridge that, `ActivityManager` holds a small `ReturnHint`:
|
|
|
|
```cpp
|
|
enum class ReturnTo : uint8_t { Home, FileBrowser, RecentBooks };
|
|
|
|
struct ReturnHint {
|
|
ReturnTo target = ReturnTo::Home;
|
|
std::string path; // FileBrowser directory to restore
|
|
std::string selectName; // item to re-focus (file name, book path)
|
|
int selectIndex = -1; // combined-list index (Home selector, Recents row)
|
|
};
|
|
```
|
|
|
|
A parent records a hint before launching a forward flow. When the launched activity (or anything it chains to) eventually exits with an empty stack, `ActivityManager::returnFromChild()` consumes the hint and routes to the correct parent — restoring its prior selection. If no hint is set, it falls back to `goHome()`.
|
|
|
|
Two ways to set a hint:
|
|
|
|
**1. Dedicated wrappers** — for the common book-open paths:
|
|
|
|
```cpp
|
|
// FileBrowserActivity::onFileOpen
|
|
ReturnHint hint;
|
|
hint.target = ReturnTo::FileBrowser;
|
|
hint.path = basepath; // "/books/fiction"
|
|
hint.selectName = entry; // "war_and_peace.epub"
|
|
activityManager.replaceWithReader(fullPath, std::move(hint));
|
|
|
|
// RecentBooksActivity::onSelect
|
|
ReturnHint hint;
|
|
hint.target = ReturnTo::RecentBooks;
|
|
hint.selectIndex = selectorIndex;
|
|
activityManager.replaceWithReader(path, std::move(hint));
|
|
```
|
|
|
|
**2. `setReturnHint()` + any `goTo*()`** — for arbitrary transitions where a dedicated wrapper would be overkill:
|
|
|
|
```cpp
|
|
// HomeActivity::dispatchMenuAction
|
|
ReturnHint hint;
|
|
hint.target = ReturnTo::Home;
|
|
hint.selectIndex = selectorIndex; // restore focus on the same menu entry
|
|
activityManager.setReturnHint(std::move(hint));
|
|
activityManager.goToSettings(); // parent destroyed; hint survives the round trip
|
|
```
|
|
|
|
`goTo*()` helpers do **not** clear the hint — only `goHome()` (explicit hard-reset) and the `replaceWith*()` helpers (which overwrite it with their own hint) do. This lets a hint survive chained transitions: Home → Reader → KOReaderSync → Reader → back to Home, hint intact.
|
|
|
|
How `finish()` interacts with the hint:
|
|
|
|
- **Non-empty stack**: `finish()` pops to the parent on the stack (classic modal result flow). Hint is untouched.
|
|
- **Empty stack**: `finish()` falls through to `returnFromChild()` automatically. An activity launched via a `replaceWith*()` helper has no stack — so its Back-button `finish()` naturally routes via the hint.
|
|
|
|
`onGoHome()` is now semantically "up and out" — it calls `returnFromChild()`, so long-press Back in a reader returns to whichever view opened the book, not always Home. For an explicit hard-reset, call `activityManager.goHome()` directly.
|
|
|
|
## Technical Details
|
|
|
|
### FreeRTOS Task Model
|
|
|
|
The firmware runs on an ESP32-C3, a single-core RISC-V microcontroller. FreeRTOS provides cooperative and preemptive multitasking on this single core — only one task executes at any moment, and the scheduler switches between tasks at yield points (blocking calls, `vTaskDelay`, `taskYIELD`) or when a tick interrupt promotes a higher-priority task.
|
|
|
|
There are two tasks relevant to the activity system:
|
|
|
|
```text
|
|
┌──────────────────────┐ ┌──────────────────────────┐
|
|
│ Main Task │ │ Render Task │
|
|
│ (Arduino loop) │ │ (ActivityManager-owned) │
|
|
│ Priority: 1 │ │ Priority: 1 │
|
|
│ │ │ │
|
|
│ Runs: │ │ Runs: │
|
|
│ - gpio.update() │ │ - ulTaskNotifyTake() │
|
|
│ - activity->loop() │ │ (blocks until notified)│
|
|
│ - pending actions │ │ - RenderLock (mutex) │
|
|
│ - sleep/power mgmt │ │ - activity->render() │
|
|
│ - requestUpdate →────┼─────┼─► xTaskNotify() │
|
|
│ (end of loop) │ │ │
|
|
└──────────────────────┘ └──────────────────────────┘
|
|
```
|
|
|
|
Both tasks run at priority 1. Since the ESP32-C3 is single-core, they alternate execution: the main task runs `loop()`, then at the end of the loop iteration, notifies the render task if an update was requested. The render task wakes, acquires the mutex, calls `render()`, releases the mutex, and blocks again.
|
|
|
|
Do not use `xTaskCreate` inside activities. If you have a use case that seems to require a background task, open a discussion to propose a lifecycle-aware `Worker` abstraction first.
|
|
|
|
### The Render Mutex and RenderLock
|
|
|
|
A single FreeRTOS mutex (`renderingMutex`) protects shared state between `loop()` and `render()`. Since these run on different tasks, any state read by `render()` and written by `loop()` must be guarded.
|
|
|
|
`RenderLock` is an RAII wrapper:
|
|
|
|
```cpp
|
|
// Standalone class (not tied to any specific activity)
|
|
class RenderLock {
|
|
bool isLocked = false;
|
|
public:
|
|
explicit RenderLock(); // acquires activityManager.renderingMutex
|
|
explicit RenderLock(Activity&); // same — Activity& param kept for compatibility
|
|
~RenderLock(); // releases mutex if still held
|
|
void unlock(); // early release
|
|
};
|
|
```
|
|
|
|
**Usage patterns:**
|
|
|
|
```cpp
|
|
// In loop(): protect state mutations that render() reads
|
|
void MyActivity::loop() {
|
|
if (somethingChanged) {
|
|
RenderLock lock;
|
|
state = newState; // safe — render() can't run while lock is held
|
|
}
|
|
requestUpdate(); // trigger render after lock is released
|
|
}
|
|
|
|
// In render(): lock is passed in, held for duration of render
|
|
void MyActivity::render(RenderLock&&) {
|
|
// Lock is held — safe to read shared state
|
|
renderer.clearScreen();
|
|
renderer.drawText(..., stateString, ...);
|
|
renderer.displayBuffer();
|
|
// Lock released when RenderLock destructor runs
|
|
}
|
|
```
|
|
|
|
**Critical rule**: Never call `requestUpdateAndWait()` while holding a `RenderLock`. The render task needs the mutex to call `render()`, so holding it while waiting for the render to complete is a deadlock:
|
|
|
|
```text
|
|
Main Task Render Task
|
|
────────── ───────────
|
|
RenderLock lock; (blocked on mutex)
|
|
requestUpdateAndWait();
|
|
→ notify render task
|
|
→ block waiting for
|
|
render to complete → wakes up
|
|
→ tries to acquire mutex
|
|
→ DEADLOCK: main holds mutex,
|
|
waits for render; render
|
|
waits for mutex
|
|
```
|
|
|
|
### requestUpdate() vs requestUpdateAndWait()
|
|
|
|
```text
|
|
requestUpdate(false) requestUpdate(true)
|
|
───────────────── ─────────────────
|
|
Sets flag only. Notifies render task
|
|
Render happens after immediately.
|
|
loop() returns and Render may start
|
|
ActivityManager checks before the calling
|
|
the flag. function returns.
|
|
(Does NOT wait for
|
|
render to complete.)
|
|
|
|
requestUpdateAndWait()
|
|
──────────────────────
|
|
Notifies render task AND
|
|
blocks calling task until
|
|
render is done. Uses
|
|
FreeRTOS direct-to-task
|
|
notification on the
|
|
caller's task handle.
|
|
```
|
|
|
|
`requestUpdateAndWait()` flow in detail:
|
|
|
|
```text
|
|
Calling Task Render Task
|
|
──────────── ───────────
|
|
requestUpdateAndWait()
|
|
├─ assert: not render task
|
|
├─ assert: not holding RenderLock
|
|
├─ store waitingTaskHandle
|
|
├─ xTaskNotify(renderTask) → wakes render task
|
|
└─ ulTaskNotifyTake() ─┐
|
|
(blocked) │ RenderLock lock;
|
|
│ activity->render();
|
|
│ // render complete
|
|
│ taskENTER_CRITICAL
|
|
│ waiter = waitingTaskHandle
|
|
│ waitingTaskHandle = nullptr
|
|
│ taskEXIT_CRITICAL
|
|
│ xTaskNotify(waiter) ───┐
|
|
│ │
|
|
┌──────────────────────┘ │
|
|
│ (woken by notification) ◄────────────────────────┘
|
|
└─ return
|
|
```
|
|
|
|
### Activity Lifecycle Under ActivityManager
|
|
|
|
```text
|
|
activityManager.replaceActivity(make_unique<MyActivity>(...))
|
|
│
|
|
▼
|
|
╔═══════════════════════════════════════════════════╗
|
|
║ pendingAction = Replace ║
|
|
║ pendingActivity = MyActivity ║
|
|
╚═══════════════════════════════════════════════════╝
|
|
│
|
|
▼ (next loop iteration)
|
|
ActivityManager::loop()
|
|
│
|
|
├── currentActivity->loop() // old activity's last loop
|
|
│
|
|
├── process pending action:
|
|
│ ├── RenderLock lock;
|
|
│ ├── oldActivity->onExit() // cleanup under lock
|
|
│ ├── delete oldActivity
|
|
│ ├── clear stack
|
|
│ ├── currentActivity = MyActivity
|
|
│ ├── lock.unlock()
|
|
│ └── MyActivity->onEnter() // init new activity
|
|
│
|
|
└── if requestedUpdate:
|
|
└── notify render task
|
|
```
|
|
|
|
For push/pop (subactivity) navigation:
|
|
|
|
```text
|
|
Parent calls: startActivityForResult(make_unique<Child>(...), handler)
|
|
│
|
|
▼
|
|
╔══════════════════════════════════════╗
|
|
║ pendingAction = Push ║
|
|
║ pendingActivity = Child ║
|
|
║ parent->resultHandler = handler ║
|
|
╚══════════════════════════════════════╝
|
|
│
|
|
▼ (next loop iteration)
|
|
├── Parent moved to stackActivities[]
|
|
├── currentActivity = Child
|
|
└── Child->onEnter()
|
|
|
|
... child runs ...
|
|
|
|
Child calls: setResult(MyResult{...}); finish();
|
|
│
|
|
▼
|
|
╔══════════════════════════════════════╗
|
|
║ pendingAction = Pop ║
|
|
║ child->result = MyResult{...} ║
|
|
╚══════════════════════════════════════╝
|
|
│
|
|
▼ (next loop iteration)
|
|
├── result = child->result
|
|
├── Child->onExit(); delete Child
|
|
├── currentActivity = Parent (popped from stack)
|
|
├── Parent->resultHandler(result)
|
|
└── requestUpdate() // automatic re-render for parent
|
|
```
|
|
|
|
### Common Pitfalls
|
|
|
|
**Calling `finish()` and continuing to access `this`**: `finish()` sets `pendingAction = Pop` but does not immediately destroy the activity. The activity is destroyed on the next `ActivityManager::loop()` iteration. It's safe to access member variables after `finish()` within the same function, but don't rely on the activity surviving past the current `loop()` call.
|
|
|
|
**Modifying shared state without `RenderLock`**: If `render()` reads a variable and `loop()` writes it, the write must be under a `RenderLock`. Without it, `render()` could see a half-written value (e.g., a partially updated string or struct).
|
|
|
|
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
|
|
|
|
**Using push for forward navigation**: `pushActivity()` / `startActivityForResult()` keeps the parent alive on the stack. For a heavy child (EPUB reader, TLS sync) on a fragmented heap, the parent's resident allocations can be the difference between a successful launch and OOM. Only push when you need the parent to receive a result — otherwise use `replaceActivity()` / `goTo*()` / `replaceWith*()` and let the parent be freed first. If you do need "back to where I came from" semantics, record a `ReturnHint` before the replace instead of pushing.
|
|
|
|
**Stale `ReturnHint`**: A hint set by one activity persists until either `returnFromChild()` / `goHome()` clears it, or a `replaceWith*()` helper overwrites it. If you record a hint but the flow aborts down an unusual path (error screen, boot transition), the next unrelated `finish()` could consume it. Prefer setting the hint immediately before the transition, and call `activityManager.clearReturnHint()` if you abort the flow without launching the intended target.
|
|
|
|
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
|
|
|
|
```cpp
|
|
// WRONG — blocks render for the entire network call
|
|
void MyActivity::doNetworkStuff() {
|
|
RenderLock lock;
|
|
state = LOADING;
|
|
auto result = http.get(url); // blocks for seconds with lock held
|
|
state = DONE;
|
|
}
|
|
|
|
// CORRECT — release lock before blocking
|
|
void MyActivity::doNetworkStuff() {
|
|
{
|
|
RenderLock lock;
|
|
state = LOADING;
|
|
}
|
|
requestUpdate(true); // render "Loading..." immediately, before we block
|
|
auto result = http.get(url); // lock is not held
|
|
{
|
|
RenderLock lock;
|
|
state = DONE;
|
|
}
|
|
requestUpdate();
|
|
}
|
|
```
|