Lifecycle and activation
Lifecycle and application activation solve different lifetime problems.
Lifecycle owns an external effect for one composed scope; ApplicationHandle reports what caused the process or an existing window session to receive attention.
Neither API is a second state system: application-owned values remain controlled State and activation should be translated into routes or document state.
Own an external effect
Section titled “Own an external effect”Register an effect while composing the scope that owns it:
Lifecycle([document_id] { auto subscription = ObserveDocument(document_id); return [subscription = std::move(subscription)]() mutable { subscription.Close(); };}, document_id);Setup runs after the scope is committed, never during speculative composition. When a dependency changes, HuxerUI runs the active cleanup before the replacement setup; unmount runs the last cleanup exactly once. The move above is intentional: it transfers the subscription into the cleanup that now owns its lifetime.
Setup may return void when there is nothing to release.
Dependencies may be equality-comparable values, State, or StateList; the overload without dependencies behaves like mount/unmount work.
Use Lifecycle for subscriptions, registrations, observation tokens, and external resources.
Do not use it to copy one UI state value into another, launch work during every recomposition, or compensate for a missing controlled-value event.
Handle cold and warm activation
Section titled “Handle cold and warm activation”StartupActivation() is the immutable activation that created the current Runtime.
OnActivation(...) receives activation events delivered to that Runtime. URL/file startup is represented by StartupActivation; notification interactions use OnActivation even when they launched the process, so install the listener in the initial application tree.
This split prevents a startup URL or file from being replayed merely because a component recomposed.
The following application presents the startup reason and replaces it when a later URL or file activation arrives:
#include <huxerui/huxerui.h>
#include <string>#include <type_traits>
using namespace huxerui;
std::string DescribeActivation(const ApplicationActivation& activation) { return std::visit( [](const auto& value) { using Value = std::decay_t<decltype(value)>; if constexpr (std::is_same_v<Value, LaunchActivation>) { return std::string{"Ordinary launch"}; } else if constexpr (std::is_same_v<Value, UrlActivation>) { return "URL: " + value.url.ToString(); } else if constexpr (std::is_same_v<Value, FileActivation>) { return value.files.empty() ? std::string{"No files"} : "File: " + value.files.front().Name(); } else { return "Notification: " + value.identifier; } }, activation );}
[[huxerui::composable]]View ActivationStatus() { const ApplicationHandle application = UseApplication(); auto latest = UseState(DescribeActivation(application.StartupActivation()));
application.OnActivation([latest](ApplicationActivation activation) { latest = DescribeActivation(activation); });
return Column { Text("Latest activation", TextRole::Title), Text(latest), }.With(Spacing(8.0F), Padding(24.0F));}
View App() { return ActivationStatus();}
const Application application{App};ApplicationActivation is a variant of:
LaunchActivationfor an ordinary start.UrlActivationwith the platform-deliveredUri; useToString()for display or serialization.FileActivationwith one or more capability-bearingFileReferencevalues.NotificationActivationwith a stable identifier and submitted PlatformPayload data.
Treat the variant as an input to application policy.
For example, parse a URL into a typed NavigationPath, or launch a scoped task that reads a FileReference and updates controlled document state.
Do not retain Android intents, Apple URLs, Win32 command-line handles, or other platform objects in shared UI.
Notification-driven activation
Section titled “Notification-driven activation”Notification clicks arrive through OnActivation with NotificationActivation, including clicks that launch the process. Inspect the variant explicitly, validate its data, then change the same application-owned navigation path used by other entry points. Notification data is a submitted snapshot, not current authorization or trusted live state. Dismissals and custom action buttons do not enter this contract.
See Local notifications for a complete listener and System API for Windows registration. Register URL schemes with windows::RegisterUrlScheme from <huxerui/system.h> when your Windows entry owns that integration; no hand-written example registry helper is required.
Route an activation once
Section titled “Route an activation once”Keep route translation deterministic and separate from presentation:
NavigationPath<AppRoute> RouteFor(const ApplicationActivation& activation) { if (const auto* url = std::get_if<UrlActivation>(&activation)) { return DecodeApplicationUrl(url->url).value_or(NavigationPath<AppRoute>{}); } if (const auto* files = std::get_if<FileActivation>(&activation); files && !files->files.empty()) { return NavigationPath<AppRoute>{{DocumentRoute{files->files.front().Name()}}}; } return {};}Apply RouteFor(application.StartupActivation()) when constructing the initial controlled path, and keep any FileReference required to open the document in document state rather than inside an equality-comparable route.
In OnActivation, replace or extend that same path according to application policy.
If the platform repeats an equivalent request, deduplicate it in application state when opening it twice would be harmful; HuxerUI does not guess whether repeated activations are meaningful.
File contents are asynchronous.
Capture the FileReference in a task launched by UseTaskScope(), handle IoResult failure explicitly, and let scope unmount cancel the work.
The Files guide covers that ownership path.
Read a file delivered by activation
Section titled “Read a file delivered by activation”This complete component handles startup and later file requests with the same operation. It keeps the latest requested document authoritative by canceling the previous read’s HuxerUI continuation:
#include <huxerui/huxerui.h>
#include <string>#include <variant>
using namespace huxerui;
Task<void> ReadActivatedText(FileReference file, State<std::string> content) { content = "Opening " + file.Name(); auto result = co_await file.ReadStringAsync(); content = result.Succeeded() ? result.Value() : "Could not open file: " + result.Error().message;}
[[huxerui::composable]]View ActivatedDocument() { auto application = UseApplication(); auto tasks = UseTaskScope(); auto pending = UseState(TaskHandle{}); auto content = UseState<std::string>("Open a text file with this application"); auto open = [tasks, pending, content](const ApplicationActivation& activation) { const auto* files = std::get_if<FileActivation>(&activation); if (files == nullptr || files->files.empty()) { return; } pending->Cancel(); pending = tasks.Launch(ReadActivatedText(files->files.front(), content)); }; Lifecycle([application, open] { open(application.StartupActivation()); }); application.OnActivation(open); return ScrollView(SelectionArea(Text(content))).With(Padding(16.0F));}The FileReference is copied into the coroutine so its provider authority survives the activation callback. This example chooses the first file and reads all its UTF-8 content; a production editor should explicitly decide how to handle multiple files and large/unsupported documents. A display name or .txt extension does not prove that the data is UTF-8. ReadStringAsync reports invalid encoding rather than silently converting it.
Startup work runs from Lifecycle after commit, not while building the view. OnActivation receives only warm requests. This UI still needs file associations in the platform shell; declaring an activation callback does not register an Android intent filter or desktop file type. Use the platform example and registration instructions before testing “Open with” from another application.
Observe foreground state
Section titled “Observe foreground state”LifecycleState() returns Active, Inactive, or Background and subscribes the composing scope to changes.
Use it when visible UI depends on the current state:
const ApplicationHandle application = UseApplication();const bool active = application.LifecycleState() == ApplicationLifecycleState::Active;
return Text(active ? "Connected" : "Paused");Use OnLifecycleChange(...) for an external side effect such as pausing a producer or saving a session.
It follows the declaring component’s lifecycle and accepts dependencies just like Lifecycle.
Background notification is not permission to keep running indefinitely; operating-system policy still controls execution.
Activation and lifecycle are independent. Moving to the foreground may occur without a new activation, and a new activation can arrive while an existing session is already active.
Platform delivery boundary
Section titled “Platform delivery boundary”The shared API begins after a platform shell has converted its native event:
| Platform | Typical source | Shared value |
|---|---|---|
| Windows | registered URL protocol or file command line | UrlActivation or FileActivation |
| Android | launch/view/edit intent | LaunchActivation, UrlActivation, or FileActivation |
| iOS and macOS | URL or document open callback | adapter-provided URL or file activation |
| Web | initial or later browser location | URL routing normally uses BrowserNavigationStack; activation depends on the Web shell integration |
Availability and exact registration remain platform-shell concerns. Do not claim a URL scheme, file association, or background mode until that shell has configured and verified it.
Ask for permissions at the point of use
Section titled “Ask for permissions at the point of use”Resolve ApplicationHandle and TaskScope during composition, then call RequestPermissionAsync(Permission::Camera) or Permission::Microphone from the feature’s user action. Keep platform declarations in Android manifests and Apple usage descriptions.
Distinguish denial, permanent denial, restriction, and unavailability; do not repeatedly prompt on every recomposition or return to the foreground. Opening settings is only a launch request, so recheck authorization when the user returns.
See the permission example. Web can expose query results where browser policy allows, but the shared request API does not fabricate capture access through a hidden media request.

