Skip to content

ADFA-4182: Project-resource rendering + full @Preview support#1370

Merged
Daniel-ADFA merged 5 commits into
stagefrom
ADFA-4182
Jun 5, 2026
Merged

ADFA-4182: Project-resource rendering + full @Preview support#1370
Daniel-ADFA merged 5 commits into
stagefrom
ADFA-4182

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

  • Render with the project APK's resources/theme/config (images, uiMode, locale, fontScale)
  • All @Preview attrs, multi-preview, and @PreviewParameter (one card per value)
  • Contain composition errors inline; stabilize classloader/daemon/asset lifecycle
  • Feed editor edits into a live preview
Screen_recording_20260604_235821.webm

  - Render with the project APK's resources/theme/config (images, uiMode, locale, fontScale)
  - All @Preview attrs, multi-preview, and @PreviewParameter (one card per value)
  - Contain composition errors inline; stabilize classloader/daemon/asset lifecycle
  - Feed editor edits into a live preview
@Daniel-ADFA Daniel-ADFA requested a review from a team June 4, 2026 22:59
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26f053ec-093c-469f-ab75-7dadf1569b2c

📥 Commits

Reviewing files that changed from the base of the PR and between 314ba1f and 59cc374.

📒 Files selected for processing (2)
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt

📝 Walkthrough
  • New Features

    • Multi-preview rendering with full @PreviewParameter expansion (one preview card per parameter value).
    • Full @Preview attribute support: widthDp, heightDp, showBackground, backgroundColor, uiMode, fontScale, locale, group.
    • Synthesized support for common multi-preview annotations (PreviewLightDark, PreviewFontScale, PreviewScreenSizes).
    • Previews render using the project APK’s resources, theme, and Configuration (images, uiMode, locale, fontScale) via ProjectResourceContextFactory.
    • Live editor integration: source edits are debounced and forwarded (EventBus → ComposePreviewViewModel) into the live preview pipeline.
    • Inline composition-error reporting and stabilized lifecycle for classloader, compiler daemon, and asset managers.
    • Per-preview BoundedComposeView with explicit width control for accurate card sizing.
    • Rendering pipeline reworked to drive from computed PreviewInstance objects and to support ALL vs SINGLE preview rendering modes.
    • ComposableRenderer accepts resolved Class<?> + resource Context + parameter values and includes a render watchdog timeout to detect stalled renders.
    • ComposableInvoker can inject @PreviewParameter values and adjusts Compose default-parameter bitmasks to invoke target parameter correctly.
    • CompilerDaemon and ComposeClassLoader improved to reduce teardown races and unnecessary reinitialization; ProjectContext now surfaces a resourceApk where available.
  • Technical Improvements

    • ViewModel flow: debounced MutableSharedFlow for source edits, background parse/validation, PreviewState.Ready extended to include resourceApk and projectDexFiles.
    • PreviewSourceParser reworked to produce richer PreviewConfig objects (stable keys, displayName, group, parameter provider info, uiMode parsing, etc.) and to detect multi-preview siblings.
    • ProjectResourceContextFactory: caches AssetManager per-APK+timestamp, synchronizes construction/lookup, falls back to application configuration context when reflective APK asset wiring fails, and exposes release().
    • Classloader: lazy optimized-dir management, early-return on unchanged dex inputs, and release no longer deletes optimized-dir (defers cleanup to creation).
    • CompilerDaemon: mutex-protected idle timeout/stop logic; stop sequence performed asynchronously to avoid races with compile/dex operations.
    • Improved cleanup: Activity/Fragment destroy paths unregister EventBus, cancel load/render jobs, clear loadedClass/previewInstances/renderedKeys, and release resourceContextFactory.
  • Risks / Best-practice considerations

    • ⚠️ Reflection and platform-dependent behavior: ProjectResourceContextFactory uses reflective AssetManager.addAssetPath and may fall back to less-accurate rendering when reflection fails or APKs are unavailable.
    • ⚠️ Concurrency complexity: multiple coordinated async layers (loadJob, coroutine dispatchers, background daemon thread, mutexes) increase risk of subtle races under rapid edits, preview switching, or shutdown.
    • ⚠️ Resource accumulation potential: retained prior AssetManager instances and deferred optimized-dir deletion could lead to disk or memory/resource accumulation if not monitored or periodically cleaned.
    • ⚠️ Internal API fragility: ComposableInvoker’s default-parameter bitmask manipulation depends on Compose compiler/runtime internals and may break with Compose runtime changes.
    • ⚠️ Asynchronous shutdown semantics: CompilerDaemon.stopDaemon clears shared fields immediately and performs shutdown asynchronously—callers must tolerate transient cleared-state behavior.
    • ⚠️ High review & test burden: Broad refactor with signature changes (ViewModel.initialize, renderer APIs, invoker overloads) and cross-cutting behavior requires thorough integration and configuration testing (different APKs, locales, uiModes, font scales, obfuscation).
    • ⚠️ Error surface: richer error reporting added, but increased complexity means more failure modes to exercise (resource loading, class/dex resolution, reflective failures, parameter provider resolution).

Walkthrough

Refactors Compose preview parsing, state, loading, rendering, and resource wiring to support multiple preview instances with parameter-value expansion, APK-backed resource contexts, debounced source parsing, coordinated async dex/class loading, and per-instance rendering orchestration.

Changes

Compose Preview Multi-Preview and Parameter Support

Layer / File(s) Summary
Preview State & ViewModel pipeline
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewViewModel.kt
PreviewState.Ready includes resourceApk. PreviewConfig expanded. ViewModel accepts initial source, uses a MutableSharedFlow<String> for debounced raw source, parses on Default dispatcher, stores resourceApk, and triggers compile/refresh flows.
Source parsing & multipreview detection
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/domain/PreviewSourceParser.kt
Rewritten parser detects individual @Preview occurrences, synthesizes multipreview annotations, extracts @PreviewParameter provider info (FQN, limit, parameter index), and generates stable keys/displayNames; accepts packageName for provider resolution.
Project APK discovery & ProjectResourceContextFactory
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt, compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ProjectResourceContextFactory.kt
Adds resourceApk to ProjectContext; resolves assemble task output listings to a main APK and provides a cached APK-backed Resources Context with reflective AssetManager.addAssetPath, synchronized caching, fallback, and release.
Composable invocation & parameter injection
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposableInvoker.kt
invokeSafely/invokeWithComposer accept parameterValue and parameterIndex, inject parameter into args when provided, and clear the relevant Compose default-bit via helper; adds COMPOSE_PARAMS_PER_DEFAULT_INT.
ComposableRenderer refactor
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposableRenderer.kt
Renderer now accepts Class<*> + functionName and optional resourceContext/parameter inputs; wires CompositionLocalProvider for context/configuration/density, adds render-time watchdog timeout, and improved setup-error reporting and composition disposal.
Fragment async init & class loading
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewFragment.kt
Reads ARG_SOURCE_CODE before viewmodel initialization; constructs ComposableRenderer without fragment classLoader; loads dex/class on Dispatchers.IO and calls renderer with resolved Class.
Activity orchestration & EventBus integration
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt
Coordinates multi-preview via a single cancellable loadJob, subscribes to DocumentChangeEvent to forward source edits, builds PreviewInstance list with resolved contexts and parameter values, refreshes selector from previewInstances, and renders ALL or SINGLE previews with per-card BoundedComposeView + per-instance ComposableRenderer. Adds expanded cleanup and release logic.
Classloader, compiler daemon & infra
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposeClassLoader.kt, compose-preview/src/main/java/com/itsaky/androidide/compose/preview/compiler/CompilerDaemon.kt
ComposeClassLoader lazily creates optimizedDir and avoids reloads on unchanged dex inputs; release() no longer deletes optimized dir. CompilerDaemon idle shutdown is mutex-protected and stopDaemon performs async teardown.
UI measurement
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ui/BoundedComposeView.kt
BoundedComposeView adds explicitWidthPx and applies it in onMeasure to control measured width.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • jomen-adfa
  • jatezzz

Poem

🐰 In burrows of code the previews bloom,

Parameters hop in from room to room,
APKs lend textures, classes take flight,
Renderers watch through the soft timeout light,
A rabbit applauds each previewed delight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding project-resource rendering and comprehensive @Preview support (including multi-preview and @PreviewParameter).
Description check ✅ Passed The description is directly related to the changeset, detailing support for project APK resources, @Preview attributes, multi-preview, @PreviewParameter, error handling, and live preview from editor edits.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4182

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt (1)

402-418: ⚡ Quick win

Narrow the exception type from Throwable to Exception.

Catching Throwable includes Error subclasses (OutOfMemoryError, StackOverflowError) which generally should not be caught. For reflection operations, Exception or ReflectiveOperationException provides sufficient coverage.

Based on learnings: "prefer narrow exception handling that catches only the specific exception type reported in crashes (such as IllegalArgumentException) instead of a broad catch-all."

♻️ Proposed fix
-    } catch (e: Throwable) {
+    } catch (e: Exception) {
         LOG.error("Failed to resolve `@PreviewParameter` values from {}", providerFqn, e)
         emptyList()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt`
around lines 402 - 418, The method resolveParameterValues currently catches
Throwable which also captures Errors; change the catch to a narrower type (e.g.,
catch(Exception) or catch(ReflectiveOperationException)) so only
reflection/checked exceptions are handled. Locate resolveParameterValues and
replace the "catch (e: Throwable)" clause with "catch (e: Exception)" or the
more specific "catch (e: ReflectiveOperationException)" and keep the existing
logging call (LOG.error("Failed to resolve `@PreviewParameter` values from {}",
providerFqn, e)) and return emptyList() as before; ensure this covers exceptions
thrown by providerClass.getDeclaredConstructor(), newInstance(), and
providerClass.getMethod("getValues").invoke(...).
compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt (1)

104-110: 💤 Low value

Consider handling absolute outputFile paths explicitly.

The outputFile value from the JSON listing may be an absolute path. While File(listing.parentFile, outputFile) happens to work on POSIX systems (the absolute child path overrides the parent), this is a subtle and non-obvious behavior. Consider checking File(outputFile).isAbsolute first for clarity.

♻️ Optional improvement
                 val outputFile = elements.optJSONObject(i)?.optString("outputFile").orEmpty()
                 if (outputFile.endsWith(".apk")) {
-                    val candidate = File(listing.parentFile, outputFile)
+                    val raw = File(outputFile)
+                    val candidate = if (raw.isAbsolute) raw else File(listing.parentFile, outputFile)
                     if (candidate.exists()) {
                         return candidate
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt`
around lines 104 - 110, The code currently constructs candidate =
File(listing.parentFile, outputFile) without handling absolute paths; update the
logic in ProjectContextSource.kt around the outputFile handling so that you
first check File(outputFile).isAbsolute and, if true, use File(outputFile) as
the candidate, otherwise use File(listing.parentFile, outputFile); then perform
the existing candidate.exists() check and return if present. This keeps the
existing variable names (outputFile, listing, candidate) and behavior but makes
absolute paths explicit and clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposeClassLoader.kt`:
- Around line 33-35: The current comparison uses unordered sets so reorderings
of projectDexFiles are treated as unchanged; update the equality check to
compare ordered lists instead of sets so DEX precedence changes trigger
release(): replace the set-based comparison of
existingFiles.mapTo(mutableSetOf()) { it.absolutePath } ==
projectDexFiles.mapTo(mutableSetOf()) { it.absolutePath } with an ordered
comparison (e.g., compare lists of absolutePath in the same order) in the
ComposeClassLoader code where existingFiles and projectDexFiles are used,
ensuring that when the ordered lists differ you call release() to reload
DexClassLoader.

In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ProjectResourceContextFactory.kt`:
- Around line 72-75: The companion-object reflection to obtain
AssetManager.addAssetPath (ADD_ASSET_PATH) runs at class init and can throw on
non-SDK restricted devices; move the reflective lookup out of eager
initialization and guard it with a try/catch (or make it lazy) so failures don’t
cause ExceptionInInitializerError—e.g., replace the direct getMethod(...) in the
companion with a nullable cached field initialized inside a try/catch or a lazy
block that returns null on failure, and update assetsFor/its invoke(...) usage
to check the nullable ADD_ASSET_PATH before attempting to invoke it and fall
back to appContext.createConfigurationContext(...) as already intended.

---

Nitpick comments:
In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt`:
- Around line 402-418: The method resolveParameterValues currently catches
Throwable which also captures Errors; change the catch to a narrower type (e.g.,
catch(Exception) or catch(ReflectiveOperationException)) so only
reflection/checked exceptions are handled. Locate resolveParameterValues and
replace the "catch (e: Throwable)" clause with "catch (e: Exception)" or the
more specific "catch (e: ReflectiveOperationException)" and keep the existing
logging call (LOG.error("Failed to resolve `@PreviewParameter` values from {}",
providerFqn, e)) and return emptyList() as before; ensure this covers exceptions
thrown by providerClass.getDeclaredConstructor(), newInstance(), and
providerClass.getMethod("getValues").invoke(...).

In
`@compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt`:
- Around line 104-110: The code currently constructs candidate =
File(listing.parentFile, outputFile) without handling absolute paths; update the
logic in ProjectContextSource.kt around the outputFile handling so that you
first check File(outputFile).isAbsolute and, if true, use File(outputFile) as
the candidate, otherwise use File(listing.parentFile, outputFile); then perform
the existing candidate.exists() check and return if present. This keeps the
existing variable names (outputFile, listing, candidate) and behavior but makes
absolute paths explicit and clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b76bb3d5-5118-4d71-b087-97a8a4ca8bde

📥 Commits

Reviewing files that changed from the base of the PR and between 1a8ef4e and dca1ed3.

📒 Files selected for processing (11)
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewActivity.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewFragment.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ComposePreviewViewModel.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/compiler/CompilerDaemon.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/data/source/ProjectContextSource.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/domain/PreviewSourceParser.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposableInvoker.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposableRenderer.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ComposeClassLoader.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/runtime/ProjectResourceContextFactory.kt
  • compose-preview/src/main/java/com/itsaky/androidide/compose/preview/ui/BoundedComposeView.kt

- ComposeClassLoader: compare ordered DEX path lists (preserve classpath precedence)
- ProjectResourceContextFactory: lazy/guarded addAssetPath lookup to avoid class-init crash on restricted devices
@Daniel-ADFA Daniel-ADFA requested a review from jatezzz June 5, 2026 13:41
@Daniel-ADFA Daniel-ADFA merged commit 98d3643 into stage Jun 5, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-4182 branch June 5, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants