Preview Tools (UI Layer)#
The Preview Tools let you iterate on Theme.plist (iOS) or theme styles in themes.xml / styles.xml (Android) and see the result on themed HAAPI screens without rebuilding the running app. Both platforms ship debug-only preview utilities that integrate with their native IDE tooling — Xcode’s Preview canvas on iOS, Android Studio’s Compose @Preview on Android (complemented by a Previewer Host Activity for production-fidelity validation; see Previewer Host Activity). Use them as the day-to-day surface for theming work; production validation still happens against the running flow.
For the theming surfaces themselves, see Theming.
Experimental API on both platforms. iOS — HaapiUIPreviewer, HaapiUIPreviewerRepresentable, HaapiUIPreviewerStyleProvider, HaapiUIPreviewerWebAuthnVariant are experimental; minor releases may change them without bumping the major version. Android — all public previewer types are annotated with @ExperimentalHaapiApi and require @OptIn(ExperimentalHaapiApi::class) at use sites. Feedback to Curity is welcomed.
What’s Available#
Both platforms cover the same set of HAAPI screen types and most component galleries. Android additionally ships a Previewer Host Activity that runs the full production Fragment pipeline for theme-fidelity validation.
| iOS UIKit | Android UIWidget | |
|---|---|---|
| IDE integration | Xcode #Preview macro / PreviewProvider | Android Studio Compose @Preview + Previewer Host Activity |
| Build configuration | DEBUG only (#if DEBUG) | src/debug/ source set only |
| Screen previews | 7 (Form, Selector, Polling, BankID, Problem, Generic, WebAuthn) | 7 (same set) |
| Component galleries | 9 (button, message, input, header, checkbox, loading, link, notification banner, expandable) | 10 (button, message, input, checkbox, header, link, loading, spinner-text, expandable, snackbar) ¹ |
| WebAuthn variants | Registration, Authentication, Additional Registration, Platform-Only | Same four variants |
| Production-fidelity validation | n/a — single rendering path | Previewer Host Activity runs real Fragments with real FragmentManager lifecycle |
¹ The platforms name their transient-feedback gallery differently — iOS calls it notificationBannerGallery, Android calls it SnackbarGallery — but they cover the same UI concept (success and error toast-style messages). Android additionally exposes a SpinnerTextViewGallery for the dropdown selector component, which is what brings the count from 9 to 10.
Setup and Usage#
Requirements#
IdsvrHaapiUIKit5.6.0 or later — earlier releases compiled the previewer out of the shipped framework, so no previewer product or pod exists for them- iOS 15.0+ runtime target — from
IdsvrHaapiUIKit5.7.0; 5.6.0 runs on iOS 14.0+ - Xcode 27+ from
IdsvrHaapiUIKit5.7.0; Xcode 16+ for 5.6.0 (the previewer sources compile in Swift 6 language mode) - The
IdsvrHaapiUIKitPreviewerproduct or pod added to your app (see below) — the previewer ships as Swift source that your project compiles - DEBUG builds only — the previewer compiles to an empty module in Release, so it never ships in your release binary; wrap all previewer usage in
#if DEBUG
Add the Previewer to Your App#
The previewer is distributed as Swift source next to the SDK binary and is compiled by your own project: your Debug builds get the previewer, and your Release builds compile it to an empty module — no previewer code ever ships in your app.
Swift Package Manager — declare both products on your app target:
.product(name: "IdsvrHaapiUIKit", package: "ios-idsvr-haapi-ui-kit-dist"),
.product(name: "IdsvrHaapiUIKitPreviewer", package: "ios-idsvr-haapi-ui-kit-dist"),Declaring only IdsvrHaapiUIKitPreviewer also works (it depends on the SDK), but declaring
both is the recommended, explicit form.
CocoaPods — add the previewer pod next to the SDK pod:
pod 'IdsvrHaapiUIKit'
pod 'IdsvrHaapiUIKitPreviewer'The previewer pod is pinned to the exact SDK version it shipped with. Scoping the pods with
:configurations => ['Debug'] is unnecessary — the previewer is already empty in Release
builds. If your project uses custom build-configuration names (for example Staging),
declare their mapping in the Podfile — otherwise CocoaPods applies its Release settings to
that configuration and the previewer is unavailable there:
project 'YourApp.xcodeproj', 'Staging' => :debugManual integration — copy the HaapiUIPreviewer/ folder from the distribution repository
into your app target, next to the manually embedded IdsvrHaapiUIKit.xcframework. Always
copy the folder from the same release tag as the framework.
Minimal Preview#
Create a Swift file in your app target (a Previews/ group is a common convention) and add:
#if DEBUG
import SwiftUI
import IdsvrHaapiUIKit
import IdsvrHaapiUIKitPreviewer
#Preview("Login Form") {
try! HaapiUIPreviewer.formViewController()
}
#endifThe #if DEBUG guard matters: in a Release build the previewer module is empty, so an
unguarded reference fails your compile — loudly, by design.
Open the Xcode canvas (Editor → Canvas or Cmd+Option+Return) and click Resume if needed. A login form renders with the default HAAPI theme.
Using Your Custom Theme#
Pass the plist name (without extension) to render with your app’s theme:
#Preview("Login Form — My Brand") {
try! HaapiUIPreviewer.formViewController(theme: "MyCustomTheme")
}In the preview context, Bundle.main points to Xcode’s preview host rather than your app. If the theme doesn’t appear, pass the bundle explicitly:
#Preview("Login Form — My Brand") {
try! HaapiUIPreviewer.formViewController(
theme: "MyCustomTheme",
bundle: Bundle(for: AppDelegate.self)
)
}Screen-Level Previews#
Seven factory methods cover every prebuilt view controller. Each maps to the matching production class documented in UI Extensibility’s Default Model → View Mapping table. All accept optional custom HAAPI JSON, a theme name, a bundle, and an embeddedInFlow flag (renders inside a flow container matching production HaapiFlowViewController layout):
formViewController— rendersFormViewControllerselectorViewController— rendersSelectorViewControllerproblemViewController— rendersProblemViewControllerpollingViewController— rendersPollingViewControllerbankIdViewController— rendersBankIdViewControllergenericViewController— rendersGenericViewControllerwebAuthnViewController— rendersWebAuthnViewController; also acceptsvariant: HaapiUIPreviewerWebAuthnVariantto choose between registration, authentication, additional registration, and platform-only (timeout/retry) layouts
Component Galleries#
Nine gallery methods render every style variant of a single component side-by-side:
actionableButtonGallery(Primary, Secondary, Text, LinkView variants)messageViewGallery(Error, Warning, Info, Heading, Content, Username, RecipientOfCommunication, UserCode)inputTextFieldGallery(Curity, Filled, Outlined — each with normal and error states)headerViewGallery(default header view)checkboxViewGallery(unchecked, checked)loadingIndicatorGallery(indeterminate progress + per-button variants)linkViewGallery(link view)notificationBannerGallery(default + success variants — toast-style transient feedback)expandableViewGallery(collapsed, expanded)
Custom HAAPI JSON#
Each factory method accepts a JSON fixture you can supply to render a non-default state — login form with prefilled errors, polling screen mid-countdown, etc.:
#Preview("Login Form — With Error") {
try! HaapiUIPreviewer.formViewController(
haapiJson: MyFixtures.loginWithError,
theme: "MyCustomTheme"
)
}Preview factory methods throw on misconfiguration — bad JSON, missing plist key, malformed style value. Use try! for fast feedback during development; the thrown error message points at the offending key.
Workflow Tips#
- Swift code changes in
#Previewblocks refresh automatically once the canvas resumes. - Theme.plist changes require the canvas to be rebuilt (the Resume button) — Xcode does not re-parse plists on every keystroke.
- Keep the Xcode console open the first time you wire a new theme. Plist parse errors and missing style keys log there with a descriptive message.
- Group preview files under a
Previews/folder per the convention; this also makes them easy to exclude from production-test code-coverage reports. - SwiftUI-only apps without an
AppDelegateclass can passBundle(identifier: "com.example.app")to thebundle:parameter instead ofBundle(for: AppDelegate.self).
WebAuthn Previews#
The WebAuthn screen is a special case: WebAuthnViewController inspects the JSON response and renders one of four visual layouts. Pass a variant to preview each built-in layout — no custom JSON needed:
public enum HaapiUIPreviewerWebAuthnVariant {
case registration // Options screen (registration) — default
case authentication // Options screen (authentication)
case additionalRegistration // Info message + Yes / No / Don't-ask buttons
case platformOnly // Retry message + platform button (simulated timeout)
}
#Preview("WebAuthn Registration") { try! HaapiUIPreviewer.webAuthnViewController() }
#Preview("WebAuthn Authentication") { try! HaapiUIPreviewer.webAuthnViewController(variant: .authentication) }
#Preview("WebAuthn Additional Reg") { try! HaapiUIPreviewer.webAuthnViewController(variant: .additionalRegistration) }
#Preview("WebAuthn Platform Only") { try! HaapiUIPreviewer.webAuthnViewController(variant: .platformOnly) }
| Layout | When it appears | What you see |
|---|---|---|
| Options screen | Both platform and cross-platform credentials available | Title, description, one button per authenticator type |
| Auto-trigger | Only one credential type, or the unified passkey format | Full-screen loading spinner; the ceremony triggers automatically |
| Additional registration | Server offers an optional device registration after auth | Info message + primary “Yes” + secondary “No, not now” / “Don’t ask again” |
| Platform only (timeout) | Only platform credentials, simulating a ceremony timeout | Retry info message + a single platform button |
The four built-in variants need no JSON. For the auto-trigger and discoverable-credential layouts, supply custom JSON — the key indicators per layout:
| Variant | Preview call | JSON key indicators |
|---|---|---|
| Options (registration) | webAuthnViewController() | platformCredentialCreationOptions + crossPlatformCredentialCreationOptions |
| Options (authentication) | webAuthnViewController(variant: .authentication) | platformCredentials + crossPlatformCredentials (with items) |
| Additional registration | webAuthnViewController(variant: .additionalRegistration) | messages + otherActions |
| Platform only (timeout) | webAuthnViewController(variant: .platformOnly) | only platformCredentialCreationOptions |
| Auto-trigger / loading | webAuthnViewController(json:) | only credentialCreationOptions (unified format) |
| Discoverable credentials | webAuthnViewController(variant: .authentication, json:) | both platformCredentials + crossPlatformCredentials empty [] |
Discoverable credentials (conditional UI) come back with empty credential arrays but still render the Options screen — the SDK treats empty arrays as “both types available”. Supply the response JSON to preview it:
#Preview("WebAuthn Discoverable Credentials") {
try! HaapiUIPreviewer.webAuthnViewController(
variant: .authentication,
json: """
{
"metadata": {"viewName": "authenticator/passkeys/authenticate-device/get"},
"type": "authentication-step",
"actions": [{
"template": "client-operation", "kind": "login", "title": "Login with passkeys",
"model": {
"name": "webauthn-authentication",
"arguments": {
"credentialRequestOptions": {"publicKey": {"challenge": "AAAA", "rpId": "example.com", "userVerification": "required"}},
"platformCredentials": [], "crossPlatformCredentials": []
},
"continueActions": [{"template": "form", "kind": "continue", "title": "Login with passkeys",
"model": {"href": "/authn/authenticate/passkeys", "method": "POST", "type": "application/json",
"fields": [{"name": "credential", "type": "context"}]}}]
}
}]
}
"""
)
}WebAuthn previews accept embeddedInFlow: true like other screens.
Preview safety: ASAuthorizationController is unavailable in Xcode Previews. The preview utility automatically swaps the credential ceremony for a static, preview-safe version, so auto-trigger layouts and credential-button taps never invoke the OS APIs (which would otherwise fail with .notSupported). No action needed on your part.
Full-Screen Flow Container#
By default a preview VC fills the canvas in isolation. In production these VCs are hosted inside HaapiFlowViewController — a scroll view with padding, a header (logo/branding), and a themed background. Pass embeddedInFlow: true to reproduce that layout (visual structure only — no flow logic, navigation, or close button):
// Isolated — focuses on the content itself
#Preview("Login Form — Isolated") {
try! HaapiUIPreviewer.formViewController(theme: "MyTheme")
}
// In flow — how it looks within the authentication flow
#Preview("Login Form — In Flow") {
try! HaapiUIPreviewer.formViewController(theme: "MyTheme", embeddedInFlow: true)
}embeddedInFlow composes with every other parameter (json, theme, bundle, showDiagnostics). Gallery methods do not support it — they are component-level, not screen-level.
Custom View Controller Subclasses#
To preview your own view controllers — whether subclassing BaseViewController or conforming to HaapiUIViewController — resolve theme styles with HaapiUIPreviewer.loadStyles(...) and wrap the VC in HaapiUIPreviewerRepresentable:
#Preview("Subclassed VC") {
let styles = try! HaapiUIPreviewer.loadStyles()
return HaapiUIPreviewerRepresentable {
let vc = MySubclassedFormVC(MyCustomModel(/* ... */),
style: try styles.formStyle,
commonStyle: styles.commonStyle)
vc.uiStylableThemeDelegate = styles.themeDelegate
return vc
}
}loadStyles() returns a HaapiUIPreviewerStyleProvider exposing commonStyle, the per-screen styles (formStyle, selectorStyle, pollingStyle, problemStyle, bankIdStyle, genericStyle, webAuthnStyle, flowStyle — each throws if resolution fails), and themeDelegate (assign to your VC’s uiStylableThemeDelegate). HaapiUIPreviewerRepresentable shows an error message in the canvas instead of crashing if the builder throws.
Diagnostics#
Every factory method — screen-level and gallery — accepts showDiagnostics: true, which overlays theme-resolution details at the bottom of the canvas:
#Preview("Login Form — Diagnostics") {
try! HaapiUIPreviewer.formViewController(theme: "CustomTheme", showDiagnostics: true)
}The overlay reports the Theme name, whether it was found in the preferred bundle, the preferred and resolved bundles, and the resolved plist path (a yellow warning if it wasn’t in the preferred bundle, a red error if not found anywhere). It also captures HaapiLogger output live at debug level for all UIKit tags — plist discovery/merging, font registration, style-resolution fallbacks, data mapping — colour-coded by level. This replaces print()/debugger use during theme work. Some Xcode versions also support Debug Preview for attaching the debugger; the overlay is the fallback where that isn’t available.
Troubleshooting#
- “Preview Error” message — verify the theme plist’s bundle and name, confirm the JSON is valid HAAPI format, and enable
showDiagnostics: trueto see resolution details. - Custom theme looks like the default — in previews
Bundle.mainis the preview host, not your app. The utility searchesBundle.allBundles+Bundle.allFrameworks, but if the plist still isn’t found, pass it explicitly:bundle: Bundle(for: AppDelegate.self). - Theme changes not reflected — plist edits aren’t auto-detected; press Cmd+Option+P (or make a trivial edit in the preview Swift file). If still stale, clean the build folder (Cmd+Shift+K) and resume, and confirm the plist is in the target’s “Copy Bundle Resources” phase.
Android offers two complementary preview mechanisms. Compose @Preview is for rapid theming iteration in the Android Studio preview pane. The Previewer Host Activity is for production-fidelity validation — it runs the real Fragment pipeline with real FragmentManager lifecycle.
Availability & Consumption#
ui.widget 5.6.0 or later is required — earlier releases published only the release variant, which excludes the previewer, so no previewer types exist in those artifacts.
The previewer ships in the debug variant of the published ui.widget artifact. A standard consumer with debug/release build types receives it automatically — no extra Gradle configuration and no new dependencies. Release builds structurally exclude it. Consume ui.widget with a build tool that reads Gradle Module Metadata (Gradle / AGP — the Android default); plain-Maven consumption of ui.widget is not supported.
If your app defines custom build types (for example staging or qa), Gradle can’t tell which ui.widget variant to resolve and the build fails with a clear No matching variant … BuildTypeAttr error. Add one matchingFallbacks line per custom build type — ['release'] for release-like types (no previewer) or ['debug'] to opt the previewer in:
// app/build.gradle
android {
buildTypes {
staging { initWith debug; matchingFallbacks = ['release'] } // release-like: no previewer
internalDebug { initWith debug; matchingFallbacks = ['debug'] } // debug-like: previewer included
}
}Consumers of only the sdk or driver artifacts are unaffected and need no changes.
Compose @Preview | Previewer Host Activity | |
|---|---|---|
| Use for | Day-to-day theming work | Final validation before shipping theme changes |
| Feedback speed | Instant on Kotlin changes | Build + install + launch |
| Rendering path | Manual layout inflation, no FragmentManager | Real Fragments with full lifecycle |
| Component galleries | Yes (10 types) | Screen-level only |
Compose @Preview — Setup#
Add the Compose compiler Gradle plugin and Compose dependencies as debugImplementation so they ship only in debug builds:
// app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.plugin.compose'
}
android {
buildFeatures { compose true }
}
dependencies {
// Compose compiler in release builds (no shipping cost):
compileOnly "androidx.compose.runtime:runtime:1.8.2"
// Compose libraries — debug only:
debugImplementation platform("androidx.compose:compose-bom:2025.05.01")
debugImplementation "androidx.compose.ui:ui"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-tooling-preview"
debugImplementation "androidx.compose.material3:material3"
debugImplementation "androidx.compose.foundation:foundation"
}Compose @Preview — Minimal Preview#
Create a file in your app’s src/debug/ source set (e.g., app/src/debug/java/.../previewer/MyPreviews.kt):
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import se.curity.identityserver.haapi.android.ui.widget.ExperimentalHaapiApi
import se.curity.identityserver.haapi.android.ui.widget.previewer.HaapiUIPreviewer
@OptIn(ExperimentalHaapiApi::class)
@Preview(showBackground = true)
@Composable
fun LoginFormPreview() {
HaapiUIPreviewer.FormFragment()
}The preview pane renders a login form with the default Curity theme.
Compose @Preview — Screen Previews#
Seven preview functions cover the HAAPI screens. Each maps to the matching production fragment documented in UI Extensibility’s Default Model → View Mapping table. All accept json: String? (custom HAAPI JSON; null uses a built-in default), @StyleRes themeResId: Int (theme resource), and showInContainer: Boolean (embed in full HaapiFlowActivity layout):
FormFragment— rendersFormFragmentSelectorFragment— rendersSelectorFragmentPollingFragment— rendersPollingFragmentBankIdFragment— rendersBankIdFragmentProblemFragment— rendersProblemFragmentGenericFragment— rendersGenericFragmentWebAuthnFragment— rendersWebAuthnFragment; also acceptsvariant: HaapiUIPreviewerWebAuthnVariant(REGISTRATION,AUTHENTICATION,PLATFORM_ONLY,ADDITIONAL_REGISTRATION)
Compose @Preview — WebAuthn Previews#
WebAuthnFragment inspects the JSON response and renders one of four layouts. Pass a variant to preview each built-in layout — no custom JSON needed; each variant loads a distinct default fixture:
| Variant | Visual layout | What you see |
|---|---|---|
REGISTRATION (default) | Two option buttons + descriptions | “Built-in” and “Security-Key” buttons for the registration flow |
AUTHENTICATION | Two option buttons + descriptions | Same layout, for the authentication flow |
PLATFORM_ONLY | Retry message + single button | “Operation was cancelled or timed out.” info message with a “Retry” button (platform-only credential, no discoverable credentials) |
ADDITIONAL_REGISTRATION | Info message + action buttons | Post-authentication device-registration upsell — “Do you want to register an additional device?” with Yes / No / Don’t-ask-again buttons |
import se.curity.identityserver.haapi.android.ui.widget.previewer.HaapiUIPreviewerWebAuthnVariant
@Preview(showBackground = true, name = "WebAuthn Registration")
@Composable
fun WebAuthnRegistrationPreview() {
HaapiUIPreviewer.WebAuthnFragment(variant = HaapiUIPreviewerWebAuthnVariant.REGISTRATION)
}
@Preview(showBackground = true, name = "WebAuthn Additional Registration")
@Composable
fun WebAuthnAdditionalRegistrationPreview() {
HaapiUIPreviewer.WebAuthnFragment(variant = HaapiUIPreviewerWebAuthnVariant.ADDITIONAL_REGISTRATION)
}A custom json overrides the variant’s default fixture.
Preview safety: WebAuthn previews run the production engine’s label-resolution logic (so buttons get correct labels from R.string.hui_webauthn_*) but block credential ceremonies — biometric prompts and security-key taps never fire, because those platform APIs are unavailable in the preview renderer. For single-option (platform-only) models, the preview simulates a user cancellation to show the retry error state.
Compose @Preview — Component Galleries#
Ten gallery functions render every style variant of one component side-by-side:
ButtonGallery(Primary, Secondary, Text)MessageViewGallery(Error, Warning, Info, Heading, Content, Username, RecipientOfCommunication, UserCode)InputTextFieldGallery(Curity, Filled, Outlined — each with normal and error states)CheckboxGallery,HeaderViewGallery,LinkViewGallery,LoadingIndicatorGallery,SpinnerTextViewGallery,ExpandableInfoViewGallery,SnackbarGallery
Compose @Preview — Custom Theme, Dark Mode, JSON#
Theme via themeResId; dark mode via @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES); custom HAAPI JSON via the json parameter. Custom themes must extend Theme.Haapi.Ui.Widget.BaseTheme:
@Preview(showBackground = true, name = "Form (Dark)",
uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun FormDarkPreview() {
HaapiUIPreviewer.FormFragment(themeResId = R.style.MyCustomHaapiTheme)
}Note: XML resource changes (colors, styles, dimensions, themes) require a project rebuild before previews update — Cmd+Shift+F5 (macOS) or Ctrl+Shift+F5 (Windows/Linux). Kotlin code changes in @Preview functions refresh automatically.
Compose @Preview — Flow Container#
By default a preview renders the screen in isolation. Pass showInContainer = true to embed it in the full HaapiFlowActivity layout — header, scroll view, padding, and themed background — to verify how the theme affects the overall flow, not just the individual fragment:
@Preview(showBackground = true, showSystemUi = true, name = "Form — In Flow")
@Composable
fun FlowContainerPreview() {
HaapiUIPreviewer.FormFragment(
themeResId = R.style.MyCustomHaapiTheme,
showInContainer = true,
)
}Compose @Preview — Custom JSON#
Inject custom HAAPI JSON to preview a specific screen state; when json is null the built-in default fixture is used. The JSON must conform to the HAAPI response format expected by the SDK’s production parser (see the HAAPI data model):
@Preview(showBackground = true)
@Composable
fun CustomFormPreview() {
val customJson = """
{
"type": "authentication-step",
"actions": [{
"template": "form", "kind": "login",
"model": {
"href": "/login", "method": "POST",
"type": "application/x-www-form-urlencoded", "actionTitle": "Sign In",
"fields": [
{"name": "email", "type": "text", "label": "Email Address"},
{"name": "code", "type": "text", "label": "Verification Code"}
]
}
}]
}
""".trimIndent()
HaapiUIPreviewer.FormFragment(json = customJson)
}Compose @Preview — Device Configuration#
Use @Preview annotation parameters to simulate device sizes and font scales:
@Preview(showBackground = true, widthDp = 320, heightDp = 568, name = "Small Phone")
@Composable fun SmallPhonePreview() { HaapiUIPreviewer.FormFragment() }
@Preview(showBackground = true, widthDp = 800, heightDp = 1280, name = "Tablet")
@Composable fun TabletPreview() { HaapiUIPreviewer.FormFragment() }
@Preview(showBackground = true, fontScale = 1.5f, name = "Large Font")
@Composable fun LargeFontPreview() { HaapiUIPreviewer.FormFragment() }Workflow Tips#
- Kotlin code changes in
@Previewfunctions refresh automatically in the preview pane. - XML resource changes (colors, styles, dimensions, themes) require a project rebuild before previews update — Cmd+Shift+F5 (macOS) or Ctrl+Shift+F5 (Windows/Linux).
- Place all preview files in
src/debug/to keep them out of release builds. - Use
@Preview(name = "…")to label previews clearly; group related previews in the same file for side-by-side comparison. - Run Preview (green play icon next to each
@Previewfunction) deploys the composable to a connected device or emulator; useful for verifying scrolling, ripple effects, and touch feedback that the static pane doesn’t capture.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Red error text in the preview pane | Invalid JSON, missing theme attribute, or layout-inflation failure | Open Android Studio’s Problems panel for the full exception and clickable stack trace |
| Preview shows nothing / blank | Missing showBackground = true, or the JSON produced an empty model | Add showBackground = true; verify the JSON contains actions |
| XML theme changes not reflected | Compose previews only auto-refresh on Kotlin changes | Rebuild and refresh: Cmd+Shift+F5 (macOS) / Ctrl+Shift+F5 (Windows/Linux) |
| Dark-mode preview looks like light | Theme has no night-mode resource qualifiers | Add values-night/ overrides; preview with @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) |
| Custom theme renders as the default | The theme doesn’t extend the base theme | Ensure the theme’s parent is Theme.Haapi.Ui.Widget.BaseTheme |
Previewer Host Activity#
When the visual result in @Preview looks fine but you want to verify the real production code path — real Fragments, real ViewModels, real lifecycle — launch the Previewer Host Activity from your app. See Previewer Host Activity for the full setup, scenario list, and limitations.
Both mechanisms render static UI from JSON fixtures — no network requests, no real authentication, no navigation between screens. Button / link taps either no-op (iOS Xcode preview, Android Compose @Preview) or enter a stuck loading state (Android Previewer Host Activity). Use these for visual validation; for flow-behavior verification, run the real app against a Curity Identity Server.
How to implement this: Theming · UI Extensibility · iOS UIKit · Android UIWidget