Modernize library with XML views, neumorphic components, and CI/CD - #11
Open
obieda-hussien wants to merge 38 commits into
Open
obieda-hussien wants to merge 38 commits into
obieda-hussien wants to merge 38 commits into
Conversation
… source, themes, and animations Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…ve unused code Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
Updated CI workflow to trigger on all branches and pull requests.
…-android-projects Modernize library: XML Views support, light source config, Material You themes, CI/CD workflow
Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…iable naming Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…Bar, CircularProgress, RadioButton, Checkbox, FAB) Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…ress with arc, and add draggable SeekBar Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…ve FAB styling Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com>
…-material-3 Upgrade to Material 3 Expressive with modern neumorphic components
- BlurMaker is now remembered once per composable instead of being recreated (and its RenderScript context re-created) on every recomposition/animation frame - this was the dominant cost. - BlurMaker reuses a persistent RenderScript + ScriptIntrinsicBlur instance instead of creating a new context per blur() call. - Added NeuShadowCache: process-wide LRU cache of generated shadow bitmaps keyed by size/elevation/colors/shape/light-source, so identical components (list items, buttons at rest) share bitmaps instead of each regenerating their own. Elevation/stroke are quantized to 0.5dp buckets so spring animations collapse into a handful of reusable bitmaps instead of one per frame. - Blur now runs on a 2x downsampled bitmap and upscales back, cutting CPU pixel work ~4x with no visible quality loss. - BlurMaker.release() wired into DisposableEffect (Compose) and onDetachedFromWindow (Views) to free the RenderScript context. - Bumped composeNeumorphism to 3.1.0 and neumorphismViews to 2.1.0. - README/README_AR: documented the performance work and switched install instructions to JitPack.
… icon logic Root cause of the app hanging on first launch: - Even after remember()-ing BlurMaker per composable, a screen with N neumorphic components (header, search bar, quick actions, cards, switches...) still meant N separate BlurMaker/RenderScript contexts all being created synchronously during the very first frame - each RenderScript.create() is heavy, and 15-20+ of them back-to-back on the main thread is enough to visibly freeze startup. - Fix: BlurMaker is now a single app-wide singleton (NeuBlurMakerHolder), shared by every neumorphic() call and every XML view. The RenderScript context is created at most once per app process, no matter how many neumorphic components exist. Added an optional warmUp(context) apps can call off the main thread to pre-create it before first use. - library-views (NeumorphicView/Button/CardView) switched to the same shared singleton instead of one BlurMaker per view instance. Demo app (APK size/quality): - Removed material-icons-extended (~5000 unused icons) in favor of material-icons-core, which covers every icon actually used. - Removed unused dependencies: navigation-compose and the three material3-adaptive artifacts (no NavHost/adaptive usage anywhere in the demo source). - Enabled minifyEnabled + shrinkResources for the release build type. - Fixed a dead conditional in HeaderSection where both branches of the notification icon picked the same Icons.Filled.Notifications regardless of state.
- AGP: 8.7.3 -> 8.13.0 (latest 8.x - deliberately not jumping to AGP 9.x, which requires migrating off the kotlin-android plugin and can't be verified without a real build environment here) - Gradle wrapper: 8.9 -> 8.13 to match - Kotlin: 2.0.21 -> 2.3.0 (AGP 8.13 explicitly documents Kotlin 2.3 support), applied consistently across library/app's compose compiler plugin - Compose BOM: 2024.12.01 -> 2026.08.00 (current official recommendation) - material3: 1.4.0-alpha07 -> 1.5.0-alpha26 (latest Expressive alpha) - activity-compose: 1.9.3 -> 1.12.3 (latest stable) - lifecycle-*: 2.8.7 -> 2.11.0 - appcompat: -> 1.8.0, com.google.android.material: -> 1.14.0 (unified across library/library-views/app, previously inconsistent) - Removed dead/unused ext vars (compose_version, compose_compiler_version, material3_adaptive_version) now that nothing references them NOTE: this could not be verified with an actual Gradle build in this environment (no Android SDK / no access to google() and dl.google.com here) - run ./gradlew build locally before publishing.
- NeuShadowCacheKeyTest (plain JUnit): key generation, elevation/stroke quantization into 0.5dp buckets, distinctness across pass/size/color/ light-source. Replaces the placeholder ExampleUnitTest. - NeuBlurMakerHolderInstrumentedTest (androidTest): confirms NeuBlurMakerHolder.get() returns the same instance across repeated calls (the guarantee the launch-freeze fix depends on), and that warmUp() leaves a usable BlurMaker behind. - NeuShadowCacheInstrumentedTest (androidTest): put/get/clear, miss on unknown key, miss on a recycled bitmap. - library-views: added an androidTest source set (didn't exist before) with NeumorphicViewsSharedBlurMakerTest, verifying NeumorphicView/Button/CardView all share one BlurMaker instance via NeuBlurMakerHolder instead of each owning its own. Added a @VisibleForTesting blurMakerForTest() accessor to each view to support this without making the field public. Also bumped library-views' test dependency versions to match library's.
NeuPerformanceConfig (library/src/main/java/.../NeuPerformanceConfig.kt): - blurDownsampling: Int (default 2) - how much shadow bitmaps are downsampled before blurring; wired into Drawing.kt's blurred() extension so it actually affects every blur call. - shadowCacheBudgetKB: Int (default 6144) - resizes NeuShadowCache's LruCache budget via a new NeuShadowCache.resizeBudget(), which uses LruCache's own resize() so existing entries are trimmed immediately if the budget shrinks. - Both setters validate their input (>= 1 / >= 0) and throw IllegalArgumentException on bad values instead of silently misbehaving. - Covered by NeuPerformanceConfigTest (plain JUnit). Added testOptions.unitTests.returnDefaultValues = true to library/build.gradle since resizeBudget() calls into android.util.LruCache, which the default unit-test android.jar stubs out. Baseline profiles: - library/src/main/baseline-prof.txt: hand-authored, class-level-only profile (no guessed Compose method signatures) covering the blur pipeline, shadow cache, config, and shape hierarchy classes. Bundled in the AAR, auto-merged by AGP into consuming apps. - app/src/main/baseline-prof.txt: same approach for MainActivity in the demo app. - Added androidx.profileinstaller:profileinstaller:1.4.1 to the library so bundled profiles actually get installed on-device. - Documented both (usage + honest scope/limitations - these are not Macrobenchmark-generated) in README.md and README_AR.md.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This is the structural fix for dependency-version drift across modules
(each of app/library/library-views was hardcoding the same version
strings independently, with no guarantee they'd stay in sync).
- Added gradle/libs.versions.toml as the single source of truth for
every AndroidX/Compose/testing dependency version used anywhere in
this project. Validated as syntactically correct TOML, and every
libs.* accessor used in the four build.gradle files was
cross-checked against the catalog's actual entries (no typos/drift).
- Migrated app/build.gradle, library/build.gradle, and
library-views/build.gradle's dependencies{} blocks to reference the
catalog instead of hardcoded version strings.
- Root build.gradle's buildscript{} classpath (AGP/Kotlin) is
deliberately kept as plain literals with a comment explaining why
(catalog accessors aren't reliably usable at that early resolution
scope) - documented as needing to stay in manual sync with the
catalog's agp/kotlin entries.
Fixed a real correctness bug found while auditing this:
- library/build.gradle used "implementation" for
androidx.compose.ui:ui, ui-graphics, and material3, even though
this library's own public API exposes their types directly
(Modifier/Color/Dp in Neumorphic.kt, ColorScheme in
NeuTheme.fromMaterial3ColorScheme()). "implementation" compiles fine
here but silently breaks external consumers who depend on this
library via JitPack without separately declaring those same
dependencies themselves ("unresolved reference" errors on their
end). Changed to "api" for exactly the types that leak into the
public surface; kept "implementation" for internal-only ones
(foundation, animation, ui-tooling-preview).
- Same issue in library-views/build.gradle: NeumorphicView/Button/
CardView expose me.nikhilchaudhari.library.LightSource as a public
property type, so its dependency on :library is now "api" instead
of "implementation".
Also added library-views/src/main/baseline-prof.txt (was missing -
the two other modules already had one) for consistency.
README_AR.md had drifted significantly out of sync with README.md: - The Requirements section was completely stale - minSdk 21, targetSdk 34, Compose 1.5.4+, Kotlin 1.9.20+ - none of which match the actual project config (minSdk 24, targetSdk 35, Compose BOM 2026.08.00, Kotlin 2.3.0). Now matches reality and the English version exactly. - Missing entire sections: Custom Color Scheme, all of Animation (Animated Press Effect / Clickable with Animation / XML Views Animation), Utility Extensions, Roadmap, Migration from v1.x, and Acknowledgments. - Missing badges (Awesome List / Awesome Kotlin) and the screenshot. Rewrote the file section-for-section against the current README.md - both now have exactly 34 top-level/sub sections in the same order. Kept the RTL wrapper and Egyptian-Arabic-with-English-technical-terms tone consistent with the rest of the file.
…import)
The Android CI workflow (Lint + Build & Generate APK) failed with actual
errors, not warnings. Fixing all of them:
1. Real compile error: "Unresolved reference: DisplayMetrics" in Blur.kt.
Lost this import when calculateDefaultBlurRadius() was moved from
Neumorphic.kt into NeuBlurMakerHolder (Blur.kt) in an earlier commit -
added the missing `import android.util.DisplayMetrics`.
2. AAR metadata failure: compose-bom 2026.08.00 pulls in Compose 1.12.0,
which requires compileSdk 37 + AGP 9.1.0+. This project is deliberately
staying on AGP 8.13.x for now (AGP 9 changes how the Kotlin Gradle
plugin is applied, and that migration hasn't been verified against a
real build). Fixed by:
- Downgrading compose-bom to 2026.04.01 (Compose 1.11 - the last line
before the compileSdk 37 requirement was introduced).
- No longer pinning material3 to an independent alpha version
(1.5.0-alpha26) - that pin required compileSdk 37 on its own,
regardless of what the rest of the BOM needed, which is exactly what
broke this. material3's version is now managed by the BOM instead,
so everything stays on one consistent, tested line.
- Bumped compileSdk/targetSdk from 35 to 36 across all three modules
(36 is the maximum AGP 8.13.0 itself recommends, per the CI error
text) - this also satisfies activity-compose 1.12.3's own
"compile against 36 or later" requirement.
3. Fixed the "Android Publication 'release' Misconfigured for Variant
'release'" warning in both library/build.gradle and
library-views/build.gradle by adding the
`publishing { singleVariant("release") {} }` block AGP asks for -
required by scripts/publish-module.gradle's `from components.release`.
Updated README.md and README_AR.md's Requirements/Roadmap sections to
document the compileSdk-37/AGP-9 constraint and why material3 is no
longer independently pinned, keeping both files in sync.
Same root cause as the compose-bom/material3 fix in the previous commit: lifecycle 2.11.0 (specifically since 2.11.0-beta01, confirmed via its own release notes: 'Updated Compose compileSdk to API 37') independently requires compileSdk 37 + AGP 9.2.0+, regardless of what compose-bom or material3 need. 2.10.0 is the last stable line before that change. Also cleaned up the RenderScript/ScriptIntrinsicBlur deprecation warnings flagged by the Lint job: moved the existing @Suppress("DEPRECATION") from just blurWithRenderScript() to the BlurMaker class level, since the deprecated types are also referenced in the class's own field declarations and release() - scattering per-member suppressions was both incomplete (missed those two spots, which is why they still showed up in CI) and redundant once applied at the class level.
Root cause of the "flat box instead of soft shadow" look reported from screenshots (Quick Actions icons, FAB circles, the Featured card's outer edge): Modifier.clip() was applied *before* .neumorphic() in many places. A raised/Punched shadow is drawn extending past the component's own bounds - clipping ahead of it in the modifier chain cuts that overflow away, leaving just the flat background fill with a hard edge instead of a soft shadow. Pressed (recessed) shapes are the opposite: their inner shadow is supposed to stay within bounds, so clip-before-neumorphic is correct there and was left alone. Library (library/src/main/java/.../components/NeuComponents.kt) - these bugs affect every app using this library, not just the demo: - NeuCard, NeuButton: clip only when neuShape is Pressed (both accept either shape from the caller). - NeuSwitch thumb, NeuSlider thumb, NeuFloatingActionButton, NeuSeekBar thumb: always Punched - clip removed entirely. - NeuIconButton, NeuChip, NeuRadioButton, NeuCheckbox: shape depends on selected/checked state - clip now only applied on the Pressed-shaped (selected/checked) branch. Pot shape (library/src/main/java/.../shapes/Pot.kt) - this one had an existing `// TODO: Fix the clipping of the shadows on foreground` comment acknowledging the same class of bug. Pot draws BOTH a raised outer shadow AND a recessed inner one in a single shape, so neither "always clip" nor "never clip" is correct for it - a single external Modifier.clip() can't apply to only one of the two draws, since both happen inside one draw() call. Fixed by clipping only the foreground (inner/recessed) draw internally, via a new clippedToCornerType() DrawScope helper, leaving the background (outer/raised) draw unclipped. This required widening drawOnForeground()'s receiver type from ContentDrawScope to DrawScope (a safe widening - it never used anything ContentDrawScope-specific beyond what DrawScope already provides, and all existing call sites still type-check since ContentDrawScope is a DrawScope). Demo app (app/src/main/java/.../MainActivity.kt) - same bug, three more spots not covered by the library fixes above (custom Boxes built directly with Modifier.neumorphic() rather than through a NeuXxx component): the Quick Actions icons (Home/Profile/Settings/Favorites), the checkmark circle on the Featured card, and the bottom navigation bar's outer pill container.
Clarify (in both README.md and README_AR.md, matching) that Modifier.clip() belongs only on Pressed shapes, never on Punched/Pot ones - directly tied to the shadow-clipping bug fixed in the previous commit.
Two internal, output-preserving optimizations to BlurMaker - neither changes what gets drawn, both just cut allocation churn on the hot path (every shadow-cache miss, and every frame during a spring animation before its shadows settle into a handful of cached buckets): 1. Working bitmap pool (workingBitmapPool): the downsampled ARGB_8888 "scratch" bitmap that blur() draws the shape into before blurring it was previously allocated fresh on every single call, then discarded. It's now pooled by (width, height) and reused - erased to transparent before reuse so no stale pixels from a previous shadow can show through. Since NeuShadowCache + downsampling mean most components on a screen land on a small handful of recurring sizes, this removes the most common allocation in the whole pipeline. Capped at 8 pooled sizes so it can't grow unbounded on an app with unusually varied shadow dimensions; released (recycled) in BlurMaker.release(). Getting the object-identity bookkeeping right here matters: stackBlur() always returns a *new* copy (it copies internally before blurring), while the RenderScript path mutates and returns the same bitmap object it's given. blur() now tracks this explicitly instead of assuming one or the other, so the pool never receives a bitmap that's already been handed to the caller (which would let a caller's "cached" shadow get silently overwritten and corrupted by a later unrelated blur() call - the failure mode this needed to avoid). 2. RenderScript Allocation reuse (allocationsBySize, API < 31 only): Allocation.createFromBitmap()/createTyped() were previously recreated on every blurWithRenderScript() call even though the RenderScript context itself was already persistent. Allocations are now cached by (width, height) and updated in place via copyFrom()/copyTo() instead, avoiding that allocate/destroy churn on the GPU-memory side too. Also capped (8 sizes) and properly torn down: release() now destroys every cached Allocation pair, closing a gap where they'd previously have been left dangling once the owning RenderScript context was destroyed.
## Crash: turning the Dark Mode switch back off closes the app
NeuSwitch's thumb used `Modifier.padding(start = thumbOffset)`, where
thumbOffset is animated with `spring(dampingRatio =
Spring.DampingRatioMediumBouncy)`. A bouncy spring overshoots past its
target before settling - animating từ 24.dp down to a 0.dp target
means the value swings slightly *below* zero for a few frames before
it settles back. Modifier.padding() throws
IllegalArgumentException("Padding must be non-negative") the instant
it receives a negative Dp, which crashes the app immediately during
that animation frame.
Turning the switch ON (0.dp -> 24.dp target) never crashes because an
overshoot there goes *above* 24.dp, which is still positive - this is
why the bug only ever showed up on the reverse direction.
Fixed by switching to `Modifier.offset(x = thumbOffset)` instead:
offset is the semantically correct modifier for animated positioning
in the first place (padding is meant for static spacing), and
Compose's offset explicitly allows negative values - a transient
undershoot just nudges the thumb very slightly past its resting edge
for a frame or two, which is invisible in practice and never crashes.
Applied the same fix to NeuSlider's thumb, which had the identical
`padding(start = <animated Dp>)` pattern - value isn't itself spring-
animated there today, so it wasn't actively crashing, but the same
class of bug would resurface the moment anyone changes that, so it's
fixed defensively too. NeuSeekBar was already using `.offset { IntOffset(...) }`
correctly and didn't need changes.
## Demo cards not responding to touch
The "Punched" and "Pressed" showcase cards (Interactive Controls
section) had no onClick/clickable at all - NeuCard doesn't accept an
onClick parameter, and the Pressed card was a bare Box with only a
draw modifier, no interaction. Rebuilt both using
`Modifier.neumorphicClickable(...)`, which the library already
provides for exactly this (animated press-in feedback + a real click
handler), matching how QuickActionItem elsewhere in this same file
already works. They now visibly react (elevation dips) on touch like
every other interactive element in the demo.
MainActivity.kt used Modifier.neumorphicClickable() in the previous commit's demo-card fix but never imported it - only expressiveNeumorphicClickable was imported. Added the missing 'import me.nikhilchaudhari.library.neumorphicClickable'. Also swept the rest of the file for any other me.nikhilchaudhari.library Modifier extension used without a matching import (animatedNeumorphic, springNeumorphic, themedNeumorphic, themedExpressiveNeumorphic, deepNeumorphic, subtleNeumorphic, boldNeumorphic) - none of those are actually called anywhere in this file, so no further imports were missing.
## FAB no longer matches the app's look NeuFloatingActionButton had a nested "soft outer neumorphic ring + solid flat accent-colored inner circle with a white border" - the inner circle was styled like a plain Material FAB bolted onto a neumorphic ring, not a neumorphic component in its own right, which is why it stood out against the rest of the app. Flattened this to a single neumorphic circle (soft backgroundColor + accent-tinted icon), matching NeuIconButton's unselected treatment. ## Radio buttons didn't match the app's look NeuRadioButton drew a hard 2dp solid-colored Modifier.border() ring around the circle - a very Material-standard look, not neumorphic (this library's whole visual language is soft dual-shadow depth, not hard strokes). Removed the border; selection is now conveyed the same way NeuChip/NeuIconButton already do it - a soft accent-tinted background (animateColorAsState) plus the existing raised/recessed shadow switch and animated inner dot. ## Smooth crossfade for chip/nav-bar selection changes NeuChip and NeuIconButton previously switched their entire shadow treatment (Punched/raised <-> Pressed/recessed, plus background color) in a single frame the instant `selected` flipped - color and shape both cut abruptly, and NeuChip's leading checkmark icon popped in/out with no animation at all. Rebuilt both to layer the unselected (raised) and selected (recessed) treatments as two overlapping Boxes whose alphas crossfade via one shared `animateFloatAsState` (selectedAlpha), instead of an instant Boolean-gated Modifier swap. The unselected shadow now visibly fades out exactly as the recessed one fades in - a real crossfade, not just an animated color on top of a hard shape cut. Content color is derived from the same selectedAlpha via androidx.compose.ui.graphics.lerp(), so text/icon tinting tracks the same motion instead of animating on its own separate timeline. NeuChip's leading checkmark now enters/exits via AnimatedVisibility (fade + expandHorizontally/shrinkHorizontally) instead of appearing/ disappearing in a single frame. This directly affects the "Categories" chip row and the bottom "Navigation Bar" in the demo app (both built on these two components) without any changes needed on the demo side - MainActivity.kt is untouched by this commit.
…ick taps
## Build fix
Modifier.graphicsLayer{} lives in androidx.compose.ui.graphics, not
androidx.compose.ui.draw - I imported the wrong package in the previous
commit's crossfade rewrite (`androidx.compose.ui.draw.graphicsLayer`),
which doesn't exist and failed the build in CI ("Unresolved reference
'graphicsLayer'" x4, both debug and release variants). Fixed to
`androidx.compose.ui.graphics.graphicsLayer`.
## "Punched button only responds to a hard/held press"
Root cause: every NeuXxx component (and neumorphicClickable/
expressiveNeumorphicClickable) drives its press feedback - a scale-down,
elevation change, etc. - directly off interactionSource.collectIsPressedAsState().
A quick, light tap can complete its full down+up cycle faster than the
feedback's spring animation has time to visibly ramp up: the tap still
registers and onClick still fires, but no frame ever renders with the
pressed state actually visible, so it looks like nothing happened. A
firm, held-down press easily outlasts the animation and looks correct -
which is exactly the "only shows for a hard press" symptom reported.
Added rememberMinHoldPressedState() (NeuExtensions.kt): wraps
collectIsPressedAsState() and holds the pressed value true for at
least 100ms after release even when the real press was shorter, via a
LaunchedEffect keyed on the raw pressed state. This doesn't change
when onClick fires or how responsive the button feels to input - only
guarantees the pressed *visual* gets enough time on screen to actually
be seen for a quick tap, same as a held one.
Swapped every `interactionSource.collectIsPressedAsState()` call
driving press-feedback animation to this new helper: neumorphicClickable,
expressiveNeumorphicClickable, and all seven NeuXxx components in
NeuComponents.kt (NeuButton, NeuSlider, NeuIconButton, NeuChip,
NeuRadioButton, NeuCheckbox, NeuFloatingActionButton) - this bug was
present everywhere in the library that has tap feedback, not just the
one button that got reported.
Three of Copilot's five findings were real:
1. NeuShadowCache.Color.toArgbHex() (High severity, correctly flagged):
was decimal-concatenating channel values ("$a$r$g$b") instead of
producing actual hex, with no padding or delimiter - genuinely
collision-prone (e.g. a=1,r=23,... and a=12,r=3,... both produce
"123..."), which could return the wrong cached shadow bitmap for a
colliding color. Fixed to zero-padded hex via String.format("%02x...").
2. NeuPerformanceConfig.shadowCacheBudgetKB's require(value >= 0)
didn't match NeuShadowCache.resizeBudget()'s actual 1KB floor
(.coerceAtLeast(1)) - passing 0 silently produced a working 1KB
cache instead of matching what the setter's contract implied.
Tightened the require() to >= 1 and updated
NeuPerformanceConfigTest to assert 0 is rejected too, not just
negative values.
3. NeuShadowCacheKeyTest's "max radius is capped" test used Kotlin's
assert(), which is a no-op unless JVM assertions are explicitly
enabled (off by default) - the test was passing without checking
anything. Switched to JUnit's assertTrue, which always runs.
The remaining two findings were not acted on:
- The drawOnForeground()/DrawScope.drawImage() one is a false
positive: `val drawScope = this` (a local alias) makes
`drawScope.drawImage(it)` a call on the DrawScope receiver itself
(drawImage is a core DrawScope method), not an access to some
nonexistent property - this already compiles, which the PR's own
passing CI checks confirm.
- The Compose BOM description mismatch is just the PR's written
description text being stale (written before the compileSdk-37
conflict forced a downgrade from 2026.08.00 to 2026.04.01) - the
actual code/README were already correct.
…READMEs Both published artifacts had drifted to different version numbers (library was 3.1.0, library-views trailed at 2.1.0) despite being released together from the same repo on the same schedule - purely confusing, not meaningful (there was never a reason for them to track separately). Bumped both to 4.0.0 (PUBLISH_VERSION, versionCode, versionName in both build.gradle files) so every future release carries one number for the whole repo. Added a "What's New in 4.0.0" section to both README.md and README_AR.md (kept in sync, section-for-section as usual) summarizing every fix/change/addition from this whole round of work: the launch- freeze and battery-drain fixes, the NeuSwitch/NeuSlider crash, the Punched-shape clipping bug (including the deeper Pot-shape fix), the quick-tap press-feedback fix, NeuPerformanceConfig, baseline profiles, the Gradle version catalog + api/implementation fixes, the FAB/radio- button restyle, and the chip/nav-bar crossfade transitions - so someone reading either README top-to-bottom actually knows what changed and why, not just how to install the new version number. Swept both files for any remaining stale 3.1.0/2.1.0 references outside that changelog section itself - none found.
## Crash on scrolling into the Categories section Real bug, exactly where it was reported: NeuChip's outer Box had no explicit width, and relied on Modifier.matchParentSize() on all three of its children (the two crossfading shadow layers *and* the Row holding the actual label text) to size itself. matchParentSize() is explicitly excluded from Box's own wrap-content sizing calculation - with every child opted out of contributing to that calculation and no explicit width modifier either, the Box had nothing left to size itself from, resolving to zero width. A zero-width shadow bitmap then crashes inside this library's own bitmap/blur pipeline (Bitmap.createBitmap requires width/height > 0). This didn't affect NeuIconButton (Navigation Bar) despite the same crossfade pattern - its outer Box has an explicit Modifier.size(size), so it never depended on children for sizing in the first place. Fixed by removing matchParentSize() from NeuChip's Row specifically - it's now a normal child that the Box measures and sizes around (via its icon/text/padding content), while the two shadow layers keep matchParentSize() to stretch to whatever size that resolves to. Swept the rest of the library for the same "every child is matchParentSize()" pattern - NeuIconButton's three matchParentSize() children are fine given its explicit .size(), and nothing else in the library uses matchParentSize() at all. ## Version/publish-metadata inconsistencies - library and library-views' versionCode had drifted to different numbers (5 vs 4) for no real reason - there's no shared-APK constraint requiring them to match (they're independently versioned library modules), but there's no reason for them not to either, and leaving them different reads as inattention. Both are now 5. - PUBLISH_GROUP_ID/PUBLISH_ARTIFACT_ID (in both build.gradle files) and the POM metadata in scripts/publish-module.gradle were still the original upstream author's identity (me.nikhilchaudhari group, CuriousNikhil's GitHub URLs/email) - leftover from before this became a fork. These are for an entirely separate, optional Maven Central publish path (`./gradlew publish`) that JitPack (what the README actually documents) never touches or derives coordinates from - so they were functionally inert, but incorrect/misleading as metadata if anyone did try to use that path. Updated group ID to io.github.obieda-hussien, artifact IDs to match the JitPack module names (library/library-views) for consistency, and the POM's url/developer/scm fields to point at this fork instead of upstream. Left the developer email blank with a comment rather than guessing one. Added comments in both build.gradle files clarifying this path is separate from and unused by JitPack, so it's clear why these values don't need to match the JitPack coordinates shown in the README.
Real, applied: - Replaced cornerType.toString() (used in NeuShadowCache cache keys) with a new CornerType.cacheDescriptor() extension that returns an explicit "Oval" / "Rounded(<radius>)" string instead of relying on CornerType.Oval's default Object.toString() (a plain, non-`data` Kotlin object, so its toString() includes an identity-hash suffix like "Oval@1a2b3c"). Applied at all three call sites (fg, bg-light, bg-dark cache keys), not just the two Copilot pointed at. - Fixed a KDoc block in NeuShadowCacheKeyTest.kt that described NeuShadowCache's storage behavior but was actually attached to BlurConfigTest (which only checks BlurConfig constants) - reworded to describe what that class actually tests. - README.md/README_AR.md: the screenshot and the License link both still hotlinked the upstream CuriousNikhil/neumorphic-compose repo. Verified static/complete_screen.png actually exists in this fork before repointing (so this doesn't trade one broken-link risk for another) - both now point at obieda-hussien/neumorphic-compose-pro. Investigated, not applied (false positives): - Copilot rated the cornerType.toString() issue "High severity" with "persistent cache misses" - this doesn't hold up: CornerType.Oval is a true Kotlin singleton (`object`, not multiple instances), so its identity-hash-based toString() is the *same string* on every call for the lifetime of the app process, meaning cache hits already worked correctly. The fix above is a readability/robustness improvement, not a functional cache-hit-rate fix. - Copilot claimed delay(minHoldMillis) in rememberMinHoldPressedState() "will throw if a caller passes a negative minHoldMillis" and suggested a require() guard. Checked kotlinx.coroutines' own delay() documentation: "If the given timeMillis is non-positive, this function returns immediately" - no exception for zero or negative values, so there's nothing to guard against. Left unchanged rather than adding a check for a scenario that doesn't occur. The PR-description-vs-actual-BOM-version note is unchanged for the same reason as the first review round - it's the PR's written description text being stale, not a code issue.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces significant modernizations and optimizations to the Android demo app and its CI/CD pipeline, focusing on improved build performance, up-to-date dependencies, and greatly enhanced documentation (including a comprehensive Arabic README). The most important changes are grouped below:
CI/CD Workflow Modernization:
.github/workflows/ci-build.ymlworkflow is completely revamped to support all branches/PRs, build both debug and release APKs as artifacts, run unit tests and lint checks, and use the latest GitHub Actions and JDK 17. The workflow now also uploads test and lint results for improved traceability.Build System and Dependency Upgrades:
app/build.gradleis updated to usecompileSdkandtargetSdk35, JDK 17, and enables resource shrinking and minification for release builds. Outdated dependencies and unused libraries are removed, replaced with Compose BOM, Material 3, and only the necessary icons, resulting in a smaller, more efficient APK. [1] [2]Performance and Profiling Enhancements:
baseline-prof.txtto the app module to improve startup performance by preloading critical classes. This complements the library's own baseline profile and can be replaced with a Macrobenchmark-generated profile for even better results.Theming and Visual Improvements:
Color.ktandShape.ktintroduce expressive Material 3 color palettes and shapes, with larger corner radii and new color definitions for both light and dark Neumorphic themes. This results in a more modern and visually appealing UI. [1] [2]Documentation:
README_AR.md) is added, detailing features, installation, usage for Compose and XML, performance improvements, and best practices. This makes the project accessible to Arabic-speaking developers and clearly explains the technical optimizations in this fork.These changes collectively modernize the codebase, improve build and runtime performance, and provide much better developer guidance and documentation.