Noice — XML → Jetpack Compose Migration

Branch dev/no-pbi/compose-phase3 (Phase 3 kickoff; forked from phase2) · auto-maintained by the migration agent
Last updated: 2026-06-18 06:40 WIB
~95%
Phase 1 — screens → Compose host
~44%
Phase 2/3 — full Compose UI
~53%
🌟 Overall journey to 100% Compose
184
/353 migrated · 51 removed · 15 deferred

📋 Activity log (newest first · updated after every change)

Wed, 18 Jun 2026
06:40trackingClosed the ultracode-review analytics finding: entitySubType back-fill on native episode rows. The legacy EpisodeView.checkData back-filled content.catalog/catalogTitle/entitySubType at bind time; the native EpisodeRow actions only set catalog, so content_opened / "content queued" events could fire with an empty entitySubType. Added a shared Content.ensureRowCatalogData(catalog) (exact checkData parity) and wired it into the catalog + AudioBook + AudioSeries episode-row action factories (replacing the bare catalog set). Build green; logic is a 1:1 port of the legacy back-fill (only fills null/empty fields), so trackings stay intact.
06:10Phase 3De-monolith #3 — Episode detail → flat Compose LazyColumn ✓. Detail family COMPLETE (3/3), ultracode-reviewed. Episode detail's body is one constraint-chained ConstraintLayout with the paged comments mid-chain, so (extract-and-rehost): pinned toolbar ▸ LazyColumn = [header item = parentLayout (cover/badges/title/duration/PlayerMediaButton/price/queue+download panel), body item = the NestedScrollView's ConstraintLayout (ad / date+comment-count / ReadMore / genre / follow-card / Kreator / Komentar header / input / paged commentRecyclerView / show-all)] ▸ ErrorView+skeleton. Comment RV set isNestedScrollingEnabled=false + body height WRAP_CONTENT so the Paging3 comments behave exactly as in the old NestedScrollView. No CoordinatorLayout/AppBar/NestedScroll. Ran an ultracode adversarial regression review (Workflow, 4 agents) across all 3 de-monolithed screens — verdict GO-conditional; it caught a real shared high-severity layout bug: the extracted header kept marginTop="?actionBarSize" from when the toolbar OVERLAPPED it → doubled top gap above the cover (AudioSeries + Episode). Fixed (zeroed headerLayout topMargin on both) + AudioBook "Chapter" heading → sans-serif bold + dropped Episode's redundant 56dp error/skeleton inset. On-device (fresh AVD, Kebangkitan → Eps 1): header tight under toolbar (gap fixed), Video badge/Putar/Antrean, Test Ad, date+comment-count(72), follow card, "Komentar"+"Lihat Semua (72)", comment input, paged comment rows (avatars/likes/Balas/Lihat Balasan), "Lihat Episode Lain" — all render + scroll, comments paginate, no crash. Follow-ups (noted, not de-monolith-specific): native comment/header sub-views; EpisodeView.checkData entitySubType back-fill in the shared EpisodeRow actions.
04:30Phase 3De-monolith #2 — AudioSeries detail → flat Compose LazyColumn ✓ (validated end-to-end). Same flat architecture as AudioBook, but AudioSeries is ViewBinding-based (~50 binding.xxx refs) + has the PopupFilterMenu filter. Rather than rewrite all binding refs, I extracted & re-hosted the inflated sub-trees: pinned toolbar (AndroidView) ▸ LazyColumn = [header subtree collapsingToolbar.getChildAt(0) as one AndroidView item (cover/badge/title/metrics/PlayerMediaButton/Subscribe/CustomAdView/ReadMore/genre/divider/Kreator), the filterLayout "Episode"+chip as an AndroidView item (kept as a real View so PopupFilterMenu.showAsDropDown still anchors to it), native EpisodeRow episodes, native "Lihat semua episode", RSS author-grid item] ▸ ErrorView + skeleton overlays. No CoordinatorLayout/AppBar/NestedScroll/ViewPager2. All binding.xxx wiring stays valid (same View instances, re-parented); episode actions reproduce EpisodeAdapter AUDIO_SERIES 1:1; filter re-sort / play-state / purchase recompose via an episodesVersion bump. On-device (fresh AVD, stable — Kebangkitan Seorang Penguasa Agung): header+blurred-cover+EXCLUSIVE+"204 Episode 23 Subscribers"+Subscribed+live Test Ad+ReadMore+Kreator avatars all render; toolbar title fade-in; "Episode/Terdahulu" filter chip → PopupFilterMenu popup anchors correctly (Terbaru/Terdahulu) and re-sort works with NO crash; native EpisodeRow (Eps 1/2, real covers, Putar/queue, video badges). Only Episode detail (comments paging) remains in #36.
03:05Phase 3De-monolith STARTED — AudioBook detail screen → flat Compose LazyColumn ✓. The detail screens used to host their ENTIRE legacy CoordinatorLayout body (collapsing AppBar + NestedScrollView + chapter RecyclerView) as ONE monolithic AndroidView. AudioBook detail (simplest of the three — no comments/filter, hardcoded variant) is now the proven flat-LazyColumn architecture the catalog screen shipped: no CoordinatorLayout / AppBarLayout / CollapsingToolbarLayout / NestedScrollView / nested-scroll interop. Structure: pinned toolbar (AndroidView, back+share, scroll-driven title fade-in) ▸ ONE LazyColumn = [header subtree as a single AndroidView item (cover/badge/title/metrics/PlayerMediaButton/Subscribe/CustomAdView/ReadMore desc/genre/Kreator — kept pixel- & tracking-identical), native sticky "Chapter" heading, native EpisodeRow chapters (replacing EpisodeAdapter), native "Lihat semua CHAPTER" button] ▸ ErrorView + skeleton overlays. Chapter row play/queue/download/price/row-tap reproduce the EXACT EpisodeAdapter AUDIO_BOOK behaviour (ascending-contentNumber sublist play, episode_page source, helper routing) — nothing re-implemented. Interop: 1 monolithic CoordinatorLayout AndroidView → ~3 leaf AndroidViews (toolbar/header/errorView), zero nested-scroll bridge. Build green. On-device: header (incl. live "Test Ad" CustomAdView + "Edukasi" genre + ReadMore description), toolbar title fade-in, native "Chapter" heading and native EpisodeRow (#1 with real Tarung Digital cover + Putar/queue/download) all render and scroll as one flat list. (QA caveat: the AVD was thrashing under host memory pressure — repeated SYSTEM-level ANRs incl. on system apps; the app itself logged zero FATALs and rendered/scrolled correctly between stalls.) AudioSeries + Episode detail (filter popup / comments) are the remaining two.
00:20QAAll four home tabs validated on-device (incl. live Firebase impression events). Cold-booted a fresh AVD (the previous one died under Mac memory pressure; dismissed two unrelated system-app ANRs — "Digital Wellbeing"/"Messages" — Noice itself never crashed). Walked Buat Kamu → Podcast → Film → Radio: every tab renders its native feed (banner pager, native content_horizontal carousels, HomeSegmentView-fallback segments incl. the ranked "Top Podcast" and the radio "GRID Segment" with LIVE badges), real cover art, see-all arrows, smooth scroll, no blank rows. Impression events captured live: segment_viewed/entity_viewed firing with the correct per-tab source — source=Podcast (banner pos 1, Top Podcast pos 2, Lagi Banyak Didengar pos 4 — note pos 3 skipped, a non-eligible top-highlight/no-genre row, exactly as legacy) and source=Audioseries (banner, Testing Film Segment, Vertical Romance) with catalog entities. The Audioseries source also confirms the latent-bug fix landed (was mis-tagged "ForYou_Page").
00:05P2.5Rolled out the native home feed to the 3 sibling tabs — Podcast / Film (AudioSeries) / Radio ✓. They were structurally identical to the old ForYou (same HomeSegmentPagingDataAdapter + ImpressionTrackerHelper over the same segmentPagingData flow, differing only in feed key + analytics source), so all three now host the SAME reusable ForYouScreen (native LazyColumn) + HomeImpressionTracking — the outer RecyclerView + adapter + manual impression helper are gone from each. Per-tab specifics preserved verbatim (feed keys PODCAST/RADIO/AUDIO_SERIES, analytics tags Podcast/Radio/Audioseries, lifecycle/getData placement, screen-view events, onLoginChangedEvent/sendImpressions public API, banner targets). Added a tiny refreshSignal param to ForYouScreen for programmatic refresh (the Paging3 items.refresh() equivalent of the old pagingAdapter.refresh()). Latent bug fixed: the AudioSeries tab was constructing its adapter with ForYouFragment.ANALYTICS_TAG ("ForYou_Page") — its cell/see-all click analytics were mis-tagged; reuse corrects them to "Audioseries" while impressions stay identical. Build green; on-device validated (above).
Tue, 17 Jun 2026
05:40QAForYou home tracking VERIFIED on-device via Firebase debug logging. Enabled debug.firebase.analytics.app + FA verbose, scrolled the feed, and captured the live event stream — proving the migrated home fires impressions identically to the old RecyclerView: segment_viewed for "Testing Segment Combination" (pos 1), "Lanjut Dengerin" (pos 2), "Test Banner Image" (pos 4), "Vertical Drama" (pos 5), all source=ForYou_Page, 1-based positions; entity_viewed for the native carousel cards (entityType=catalog), "Lanjut Dengerin" (content), and "Test Banner Image" (bannerimage) — confirming BOTH the native-LazyRow entity tracking AND the HomeSegmentView-fallback inner-RecyclerView entity tracking work. Visuals: native carousels (heading + see-all arrow + cards) and AndroidView-fallback segments (banner-image, vertical-drama with real covers) all render, smooth scroll, no blank rows, no crashes.
05:30P2.5Home "Buat Kamu" (ForYou) feed → native LazyColumn + faithful impression tracking ✓. The outer home RecyclerView (HomeSegmentPagingDataAdapter) is replaced by a native LazyColumn over the SAME segmentPagingData flow (collectAsLazyPagingItems) with PullToRefreshBox + paging append/retry footers. Per-segment dispatch is byte-identical: content_horizontal → native carousel (heading + LazyRow hosting the proven ContentHorizontalSegment cell via AndroidView), everything else (audiobook/radio/content_vertical/recently_played/banner-image/…) + banner/top-highlight/no-genre/ads → the existing Views via AndroidView. Click/see-all routing delegated 1:1 to the existing code (EventBus/OpenIndexEvent + ClickHandler + analytics) — nothing re-implemented. The hard part — impression analytics (revenue-tracked): the old ImpressionTrackerHelper (outer-RV-coupled) was faithfully reproduced in Compose (new HomeImpressionTracking.kt): segment_viewed fires once a row is ≥30% vertically visible; entity_viewed fires for inner cards ≥15% visible — for the native carousel via its LazyRow (settle-gated) and for fallback segments by scanning each HomeSegmentView/BannerView inner RecyclerView (initial + scroll-idle). Eligibility matches legacy exactly (banner/ad/NORMAL track segments; top-highlight/no-genre don't; entities only for NORMAL + banner). Re-track-on-return wired via sendImpressions() trigger. Adversarial parity review: all 9 checks PASS, then confirmed live on-device (above). Build green, on dev/no-pbi/compose-phase3.
Mon, 16 Jun 2026
04:00findingNext interop chunk is screen-rewrite-shaped, not row-swap-shaped (RootTitleRecycler "Kreator" investigated → reverted, no commit). Tried to port the shared Kreator creators row (RootTitleRecycler) into EpisodeDetailScreen. Blocker: that screen (and the audiobook/audioseries/episode detail family) hosts the entire legacy content XML as ONE monolithic AndroidView; RootTitleRecycler is an inline node deep inside it, with no discrete slot to swap — a native KreatorRow would be dead/unrendered code (zero visual change), so it was reverted rather than committed. Converting these needs the detail body de-monolithed into a Compose column first (delete the inline XML node, re-anchor constraints, render natively) — a per-screen rewrite. Same applies to the other detail sub-views (CustomAdView/PopupFilterMenu/ReadMoreTextView). The clean single-row RecyclerViewLazyColumn wins (Episode/Similar/RecentPlays/Notification/Schedule rows) are now done; remaining interop is concentrated in (a) these monolithic detail bodies and (b) multi-segment home/lihat-semua adapters — both larger, screen-level efforts.
03:25QAScheduleRow verified on-device with real schedule data (caveat cleared). Emulator had crashed (memory pressure) — cold-booted a fresh AVD, then opened Radio Jak 101 FM (the boss-supplied station with real schedules) → "Lihat Semua Jadwal". The native ScheduleListScreen/ScheduleRow renders the full schedule correctly: day tabs (Rabu/Kamis/Jumat/…), time strips "06:00:00 WIB - 10:00:00 WIB" (the verbatim getLocalTimeFromUTC HH:mm:ss format), 64dp thumbnails (real cover for "Jak Best Music", placeholder for others), camelCase titles ("Sarapan Seru", "Siaran Pulang Kerja"). Day-tab switch re-renders the list; no crash. (UAT note: most radios — Ardan, PrimaFM — have "Jadwal program belum tersedia"; Jak FM was the one with data.)
17:40P2.5ScheduleView → native ScheduleRow; ScheduleListScreen → LazyColumn ✓. The radio schedule row (schedule_radio_view.xml) is now native — a 36dp home_search time strip (start/end via DateUtils.getLocalTimeFromUTC verbatim), 64dp rounded thumbnail (GlideImageView), title (Utils.camelCase) + 2-line description. Non-interactive (no clicks/state in the legacy View) so no action lambda. ScheduleListScreen swapped its hosted RecyclerView for a LazyColumn over the same static list; ScheduleView+RadioScheduleAdapter untouched. Build green, adversarial review APPROVE (no fixes). QA caveat: on-device blocked this session by emulator cold-start ANRs + uncertain radio-schedule test data — flagged for a manual pass. Lowest-risk conversion (stateless row, validated utils).
commit 194a5cf5a
17:05fixAudiobook "Semua Chapter" — back button + header size fixed (boss/user QA on a real audiobook). On the audiobook all-chapters screen (EpisodeListScreen audiobook-header path): (1) back button was a no-op — the NoiceToolbar was built without an onBack lambda (default is {}); added an onBack param wired to onBackPressedDispatcher → back now returns to the audiobook detail. (2) header title too small vs podcast "Semua Episode" — toolbar used titleFontSize = 12.sp; removed the override → NoiceToolbar default 14.sp (= the ToolbarView sp14 the podcast header uses), so the two now match. Build green; on-device verified on the Tarung Digital audiobook (a2cd9122…): header matches "Semua Episode", back returns to detail. Also confirmed the audiobook chapter rows render via the native EpisodeRow (QA #3-adjacent) — old .qa vs new .development matched.
commit 7de5af982
16:10QANotification select-mode + DELETE verified end-to-end on-device ✓ (caveat cleared). After a clean emulator reboot: notification list renders via native NotificationRow (thumbnail/title/body/timestamp/chevron); tapped Hapus → select mode (checkboxes + "Pilih Semua"); per-row checkbox toggle works (recompose-safe selectedIds); selected 1 → Hapus → confirm dialog "Hapus Notifikasi?" → YADELETE /notification-api/v1/user-notification/me fired → the selected notification was removed (5→4 rows) and select mode exited cleanly. The destructive delete path is now validated, not just reviewed. (Emulator still cold-start ANRs once under Mac memory pressure, then recovers.)
15:55P2.4+P2.5NotificationView → native NotificationRow; notification center → LazyColumn ✓ (select/mark-read/delete preserved). The most behaviour-rich row yet (multi-select + mark-read + delete). Row chrome went native (36dp rounded thumbnail, read/unread bg, timestamp, chevron, divider, leading M3 Checkbox in select mode); the title + body are kept as AndroidView TextViews to reuse the EXACT legacy highlightMentions (clickable @mention spans) + ReadMoreOption (3-line expand + autoLink) verbatim — no reimplementation. Fragment drives mutableStateListOf + a recompose-safe selectedIds list + selectMode; the whole row-click entityType routing + notification_clicked analytics extracted verbatim; markAllRead / delete (empty=delete-all vs selected ids) / paging / toolbar Hapus·Pilih Semua·Batalkan semua all preserved. Build green, adversarial review clean (verified delete/select/paging/analytics faithful). QA caveat: on-device select-mode + delete QA was blocked by emulator system-ANRs this session (Mac memory pressure) — list render, multi-select, delete-selected/all, mention-tap flagged for a manual pass via the bell before relying on the destructive delete path.
commit 498464169
15:10fixTwo "Semua Episode" polish fixes (boss/user QA). (1) Price button "Buka 20 Coin" cart icon mis-centeredic_cart_small rendered at intrinsic height and scaled to the full ~22dp box, sitting above the text; constrained to size(16.dp) + dropped the price Text's includeFontPadding so icon + glyphs center together (EpisodeRow). (2) "Terdahulu" sort chip truncated to "Terdahu" — the chip label used a LinearLayout weight 0.8 inside a hard width(95.dp), clamping the text; fix: label → WRAP_CONTENT (maxLines 1) + arrow WRAP_CONTENT + chip padding (PodCastPagerFragment) and the chip host → widthIn(min=95.dp) (PodCastPagerScreen) so it grows to fit. Build green; on-device verified on Kebangkitan "Semua Episode": "Terdahulu" shows in full + premium rows' cart icon centered.
commit f432697c6
12:05P2.4+P2.5CatalogEpisodeScreen → native filter chips + LazyColumn of EpisodeRow ✓ — last EpisodeView-based screen converted. The audiobook/audioseries catalog "Episode" tab (CatalogEpisodeListFragment, in SimilarCatalogPagerAdapter) hosted TWO RecyclerViews — a CatalogFilterAdapter chip bar + a Paging3 ContentListPagingAdapter of EpisodeView. Now: a native LazyRow of filter chips (pixel port of filter_pills.xml) + a LazyColumn over contentPagingData.collectAsLazyPagingItems() rendering the merged EpisodeRow — the EXACT validated ChannelPodcastScreen paging pattern (refresh/error/empty/append-footer from loadState). episodeRowActions reproduces the adapter routing verbatim (POD_CAST_PAGE, play/queue/download/price/analytics); onQuickFilterClick/onResetFilterClick kept verbatim. Adversarial review fixed a missing import + confirmed no duplicate paging-collection (sole consumer of the shared flow). Build green, review clean. QA: reuses the on-device-validated EpisodeRow + the validated ChannelPodcastScreen paging/play-state patterns (per-row play-state self-recomputes; purchase→refresh — both already proven on the catalog). Host Episode-tab pager not reachable on this emulator (the test audio-series uses an inline list; the tabbed pager renders for audiobook catalogs) — flagged for a manual QA pass.
commit d29aa99af
11:25fixEpisodeRow transient row-collapse fixed (caught via boss/user QA of Semua Episode). On the catalog "Semua Episode" list (portrait poster thumbnails), a row could momentarily render malformed — tiny/collapsed thumbnail + price button but no title/date — during scroll-fling / first paint. Cause: the thumbnail is a GlideImageView (AndroidView) whose height follows the bitmap (portrait posters grow >60dp); inside a LazyColumn the AndroidView can momentarily measure tiny before the image binds, collapsing the row (element tree confirmed a 43px-tall thumbnail). Fix: heightIn(min=60.dp) floor on the thumbnail+title Row — stable min height during async load, image still grows for portrait, no crop/behavior change. On-device verified: fast-fling through premium Eps 8/9/10 now renders every row stably (badge/date/title/price + full thumbnail); "Season 1" heading intact. Shared EpisodeRow (catalog/Download/EpisodeList) unaffected once settled.
commit e901b556f
10:35P2.4+P2.5EpisodeListScreen ("Semua Episode") → native LazyColumn of EpisodeRow ✓. Converted the catalog all-episodes screen (EpisodeListFragment + SemuaEpisodesAdapterEpisodeView via AndroidView) to a native LazyColumn<Content> reusing EpisodeRow. episodeRowActions reproduces the adapter routing verbatim with the dynamic from page source — play queues the whole catalog from the tapped episode onward (sortedBy contentNumber, addCatalogContents(true)), queue is skipLogin/non-gated (adapter parity), download via EpisodeViewDownloadHelper, price via ClickHandler.openPurchaseDialog. isSemua season-heading rows render a native SemuaHeadingRow; paging off API episode count. Adversarial review caught + fixed a real crash: LazyColumn duplicate keys (all season headings share id '1000') → itemsIndexed composite key. Built green. QA note: reuses the already on-device-validated EpisodeRow (proven on catalog + Download + History); on-device nav to this nested screen was blocked by repeated emulator system-ANRs this session (Mac memory pressure) — the season-heading isSemua path is flagged for a manual QA pass (the crash itself is fixed by construction via composite keys).
commit 4b4889ef4
10:20P2.4+P2.5Liked (Konten disukai) screen → native LazyColumn of RecentPlaysRow ✓ — both collection screens sharing the row are now native. Mirrored the History conversion (self-implemented, since RecentPlaysRow already existed): LikedContentFragment drives a mutableStateListOf + recentPlaysRowActions reusing the EXACT legacy logic verbatim — row tap → OpenIndexEvent(OPEN_CONTENT_PAGE, "Collection_Liked_Page"); play → adapter actionButton path with .extraData(extraData).pageSource("Liked Content").queueTitle("Kontent disukai"). Explore-CTA empty state + ErrorView + like_index_opened/"liked page opened" analytics preserved. Built green. On-device QA: Liked renders real liked content via RecentPlaysRow (incl. "Selesai" done-state progress), no crash. RecentPlaysVertical + CollectionRecentAdapter now orphaned by both screens (kept, additive).
commit bd39a5672
10:15P2.4+P2.5RecentPlaysVertical → native RecentPlaysRow; History screen → LazyColumn ✓. Ported the shared "recent plays" row (recent_plays_vertical.xml, the Content+listener path used by CollectionRecentAdapter for History & Liked) to a native Compose RecentPlaysRow + RecentPlaysRowActions(row-tap, play). Converted the History collection screen from a hosted RecyclerView to a native LazyColumn<Content>; HistoryContentFragment drives a mutableStateListOf + recentPlaysRowActions reusing the EXACT legacy play/route logic verbatim (OpenIndexEvent row routing; adapter actionButton play path — ad guard, playWhenReady toggle, ContentPlayRequest addCatalogContents(true).contentFetchLimit(20)). Paging + EventBus state-list mutations preserved; empty state + ErrorView kept. On-device QA: History renders with real playback-history data (rows/thumbnails/progress "Tersisa N Menit"/play), scroll + play confirmed, no crash. Liked screen (same row) is a follow-up — the ultracode implement agent stalled (stall-watchdog) after finishing History, so Liked stays on the legacy path for now (the row already exists, so it's a small remaining edit).
commit f8d6fecfb
00:15P2.4+P2.5Download screen → native LazyColumn of EpisodeRow ✓ — first FULL RecyclerView→Lazy list conversion (real AndroidView host removed). The Download collection screen previously hosted a pre-built RecyclerView (EpisodeLoadMoreAdapterEpisodeView rows) via AndroidView; it now renders a native LazyColumn<Content> of the reusable EpisodeRow. DownloadFragment drives a mutableStateListOf<Content> and supplies episodeRowActions(content) reusing the EXACT download-context logic verbatim — play (DOWN_LOAD_PAGE clearQueue + trimList sublist + queueTitle/pageSource "downloads" + EVENT_CONTENT_QUEUED_AUTO), queue, download/remove (EpisodeViewDownloadHelper REMOVE_DOWNLOAD / EPISODE_VIEW_DOWNLOAD + content_download_clicked + stopDownload), price, row-tap. Paging reproduced via a trailing load-more (LIMIT 25 + hasMore/loading guards); REMOVE/SHOW/MARK_AS_PLAYED EventBus handlers mutate the snapshot list. Restore card + empty state + ErrorView preserved. Built green, adversarial review clean. On-device: confirmed working (user-verified); EpisodeRow rows already pixel-validated on the catalog. This both removes a hosted-RecyclerView AndroidView (P2.4) and reuses the native row (P2.5).
commit 778f627e9
Sun, 15 Jun 2026
22:50P2.5SimilarCatalogView → native Compose SimilarRow ✓ — second catalog row-View killed; the Konten Serupa tab is now 100% native. The catalog's "Konten Serupa" rows previously hosted SimilarCatalogView per row via AndroidView; now a pixel-port SimilarRow + SimilarRowActions (mirrors the EpisodeRow pattern). Row-tap routing delegated 1:1 to the fragment, reusing the exact EventBus.post(OpenIndexEvent) branching from the View verbatim (OPEN_AUDIO_BOOK_PAGE / OPEN_AUDIO_SERIES_PAGE / OPEN_CATALOG_PAGE with extraData.copy(similarCatalog=title)) — no analytics added or dropped (the View fired none). Fonts Roboto/roboto_bold per the XML, M3 white30 ripple @4dp, listens row hides at count 0. Built green; adversarial review verdict clean. QA: catalog Episode tab (EpisodeRow) intact, Episode↔Konten Serupa switch + empty state render, no code-level crash. Honest gap: the populated SimilarRow visual could not be reproduced on UAT — the similar/recommendation engine returns empty for every reachable test podcast — so populated-row pixel parity is pending a data-backed QA pass. The bigger standalone SimilarCatalogScreen paging rewrite the workflow also produced was reverted (kept the change to only what's QA-validated; that screen stays AndroidView-hosted until it can be QA'd with real similar data).
commit 409cb1d81
22:25P2.5EpisodeView → native Compose EpisodeRow ✓ — the most-used list row, and the first real row-View killed. The catalog's episode list previously hosted the entire ~600-line EpisodeView custom View per row via AndroidView (thumbnail, badges, progress, Putar pill, queue/download, price — dozens of nested Android Views). It's now a pixel-port composable built straight from episode_view.xml. Zero behaviour reimplemented: play/queue/download/price/row-tap delegate to action lambdas that reuse the exact existing logic verbatim — ContentPlayRequest builders, AnalyticsUtil EVENT_CONTENT_QUEUED_AUTO + MoEngage "content queued", EpisodeViewDownloadHelper routing (EPISODE_VIEW_CLICK/_DOWNLOAD/REMOVE_DOWNLOAD) + ExoplayerUtils.stopDownload + content_download_clicked, ClickHandler.openPurchaseDialog. Download state/progress observed reactively via the id-based DownloadProgressListener (DisposableEffect); only the tiny lottie EqualizerView stays AndroidView (animation kept identical). Built green; on-device QA old .qa baseline vs new .development on catalog "Acara Korea Indonesian": regular rows AND VIP/premium rows (VIP badge + "Buka N Coin" price pill) pixel-identical to baseline; Putar opens player & plays; Episode↔Konten Serupa switch + empty state; sticky tabs + single-page collapse; no crashes. Built via ultracode (map→implement→adversarial-review, builds in main thread).
commit 46b4f61c4
18:15trackerProgress re-grounded from the actual codebase. Phase 2 recomputed to ~42% (P2.3 ErrorView DONE, P2.4 paging-compose piloted on catalog, P2.6b player portrait DONE; P2.2 benchmark + P2.5 ~91 row Views remain — the bulk). Phase 2 table corrected (stale TODOs → DONE/IN-PROGRESS) and a dedicated Phase 3 tracker added. Honest interop count: 140 AndroidView calls / 75 files (≈ flat vs ~134 — recent screens are Compose-structured but still host rows via AndroidView; raw count drops with P2.5). ErrorView interop retired.
17:35phase 3Catalog (channel/podcast) screen → flat single-scroll Compose ✓ (collapsing-scroll regression FIXED & on-device validated). The catalog page had regressed to two independent scroll regions. Deep root-cause (incl. an Opus 4.8 investigation agent that byte-decompiled Material 1.7→1.14 — Material is innocent): a dependency bump regressed the legacy CoordinatorLayout+AppBarLayout+ViewPager2 nested-scroll coordination (persists even matching develop's exact recyclerview 1.3.0/viewpager2 1.0.0). Fix: eliminate nested scroll entirely — rewrote the screen as one native-Compose LazyColumn (header item + stickyHeader tabs + paging-compose rows). Header + tab strip + episode/similar rows reuse the real Views for pixel-identical UI; downloads reactively observed via the id-based DownloadProgressListener (no RecyclerView coupling); all play/subscribe/share/download/analytics wiring kept 1:1; Material 1.14.0 retained. On-device QA (catalog deeplink): header collapses → tabs pin → list continues as ONE page; rows incl. VIP/price pixel-identical; row-tap→detail; play; tab-tap registers; no crash. FIXED ✓ Episode↔Konten Serupa tab switch works; added a Konten Serupa empty state ("Tidak ada konten tersedia") per review feedback. Fully functional on-device.
commits fc259aa13c5bc003c5
14:00–17:30diagnosisRuled out, with evidence, what was NOT the cause (so the fix is correct, not a guess): not the Compose hosting (pure-View repro'd it too), not Material 1.14 (decompiled identical AppBar behavior — so it stays), not recyclerview/viewpager2 (matched develop's versions, still broke), not a code change (catalog code was byte-identical to the last-working commit). Built a develop-branch reference APK for true side-by-side, and an HTML scroll prototype to lock the target behavior before coding. Conclusion: the legacy nested-scroll geometry is a dead end on the current dependency stack → went fully Compose.
Sat, 14 Jun 2026
06:05phase 3P3 ErrorView → Compose (app-wide, exact API preserved) ✓ The progress / no-internet / error render states now draw via Compose (ErrorViewContent.kt in a ComposeView), while ErrorView stays a FrameLayout with its exact public API — so all 71 callers + 13 XML hosts (incl. toyota) are unchanged (zero blast radius). 3rd-party SkeletonLayout kept as View (no Compose equivalent). EventBus/MoEngage/callbacks/padding preserved; reviewer confirmed API parity + fixed 2 visual nits. Built green across all callers; app launches + all tabs + pull-to-refresh, zero crashes. Error-screen visual render pending on-device QA (offline error state unreachable on this guest/cached UAT build via automation).
commit 836cb1d63
05:35playerP3 player — FIRST real interop removal ✓ The portrait expanded-player video surface (a pure use_controller=false PlayerView) is now a full-Compose ComposeView hosting Media3 PlayerSurface (via NoicePlayerSurface). Same ExoPlayer; new getActiveVideoPlayer() mirrors the exact cast/exo selection of setPlayerView(). Built + adversarially reviewed (APIs checked vs decompiled media3-ui-compose 1.10.1). QA: old .qa baseline vs new .development installed side-by-side — launch/home/nav identical, zero crashes/PlayerSurface errors. Deferred (still AndroidView): landscape surface+fullscreen, IMA ad surface, transport controls. Portrait-video pixel render needs a manual on-device tap (home tiles don't route via UI-automation; UAT browse lists empty).
commit c590787fa
05:10phase 3Phase 3 kickoff — branched dev/no-pbi/compose-phase3 off phase2. Built the pre-Phase-3 QA APK as the "old" baseline (stashed for side-by-side compare: old .qa vs new .development). Started the flagship player conversion via ultracode (map→implement→adversarial-review); video surface → Media3 PlayerSurface, same ExoPlayer, IMA/subtitles stay AndroidView. Discipline: builds run in main thread (not in workflow — stall-watchdog lesson); each conversion is build- + QA-gated old-vs-new and only committed if green, else reverted with a report.
04:55docsKeep-as-XML views RE-VERIFIED for Compose-only (you asked for this). Verdict: all 4 are migratable — the old "4 deliberately-kept-as-XML" stance is obsolete. companion_ad_view = yes (do first, yields reusable CompanionAdSlot), fragment_player_detail = partial (flagship: PlayerSurface + state-holder controls), track_selection_dialog = partial, activity_home shell = partial (defer to Phase 3). Only 3 tiny 3rd-party sub-surfaces stay AndroidView forever (IMA ad FrameLayout, Media3 TrackSelectionView, Media3 SubtitleView) — down from 4 whole views. Full report → compose-migration/KEEP_XML_REVERIFY.md.
04:30validatedMinor-dependency refresh DONE + QA-validated on-device. Build green (compileQaDebugKotlin + assembleDevelopmentDebug) and dev-flavor APK smoke-tested on emulator against UAT: app launch, Hilt 2.58 DI graph, WorkManager 2.11 init, onNewIntent re-delivery, navigation, paging content load, image loading — all clean, zero crashes / ANR. Emulator booted on-demand then killed (RAM policy).
commit 8aa79e507 · pushed
04:20upgrade~20 minor-behind deps bumped to latest stable (same-major): lifecycle 2.5→2.10, navigation+safeargs 2.5→2.9.8, fragment 1.5→1.8.9, room 2.7→2.8.4, work 2.7→2.11.2, paging 3.1→3.5, hilt 2.56→2.58, material 1.7→1.14, accompanist→0.36, activity-compose→1.13, appcompat→1.7.1, camerax→1.6.1, coil→2.7, glide 5.0.5→5.0.7, core-ktx→1.17, constraintlayout→2.2.1, recyclerview off-alpha→1.4.0, window→1.5.1. 3 forced caps (out-of-scope blockers): Kotlin held 2.1.21 (hilt-work KAPT metadata maxes at 2.3.0), hilt held 2.58 (2.59.2 needs AGP 9), core-ktx held 1.17 (1.19 needs compileSdk 37). API-break fixes: WorkManager Configuration.Provider getter→val; onNewIntent(Intent?)→non-null; WebChromeClient.createIntent() null-guard; @EntryPointvarval (Hilt 2.58). MAJOR/EOL deps (billing/retrofit/okhttp/firebase/ktor/moengage/appsflyer/lottie/AGP, legacy ExoPlayer) remain flagged.
03:10infraEmulator policy: boot the Android emulator ONLY for QA, kill it otherwise — reserve Mac memory for the long gradle/workflow tasks (emulator was ANR/OOM-ing during marathon builds).
02:56upgradeP2.0 go-latest DONE + VALIDATED on-device: compileSdk 36, Compose 2026.05.01 (ui 1.11.1 / m3 1.4.0 / M3 Expressive), Media3 1.10.1, core-lib desugaring. Home renders, content-detail scrolls+clicks (nested-scroll fix holds on 1.11), episode playback works. Note: 2026.06.00 BOM not yet on Maven (docs ahead of artifact) → on 2026.05.01.
commit 05ffea52b
02:30bugfixgo-latest fixes: enabled core-library desugaring (Media3 1.10 IMA dep requires it); inlined the one Rounded.Search icon (material-icons artifact dropped from BOM); IMA ad click-through rewired (internal zzc class removed in IMA 3.39) — FLAG: ad CTA now via IMA-internal reflection, fragile, review later.
02:10docsLibrary freshness audit: 25/33 deps behind latest stable (majors: billing 7→9, retrofit 2→3, okhttp 4→5, firebase 31→34, ktor 2→3; legacy ExoPlayer 2.19.1 EOL). All minors queued to bump. See LIBRARY_AUDIT.md + Library-freshness section below.
commit d95e3eee5
02:08docsAligned plan + dashboard with Google Compose-first guidance: View libs in maintenance mode; Navigation Compose elevated to in-scope (Phase 3); CoordinatorLayout→Scaffold, adaptive, baseline-profiles added to roadmap.
commit 5985adf79
02:05docsDashboard upgraded to timestamped activity-log format (this view), wired for real-time updates.
~01:50validatedUser confirmed on-device: content-detail screens now scroll + click. Content-detail freeze fix verified working. ✅
01:35bugfixFixed frozen content-detail screens (no scroll/click) — added nestedScroll(rememberNestedScrollInteropConnection()) to 5 CoordinatorLayout detail screens (Episode, Radio, AudioSeries, Channel, AudioBook). Root cause: AppBar swallowed all touch.
commit c342630c9 · diagnosed via uiautomator + code correlation · compile+assemble green
~01:10infraRestored emulator network (wipe-data cold boot) — content finally loads from UAT; recovered from repeated ANRs / no-route state.
00:43bugfixFixed home tabs stuck on loading skeleton forever — paging itemCount race; drive hideLoading() off onPagesUpdatedFlow (4 home fragments). Validated home renders live content.
commit bf325f23a
00:00playerP2.6 pilot: clips video surface → Media3 PlayerSurface + reusable NoicePlayerSurface primitive. Dev flavor repointed api.devapi.uat (dead host). Validated Media3 1.8.1 playback (audio + video).
commit 97d13d4a3
Fri, 13 Jun 2026
23:27upgradeP2.1b: Media3 1.4.1 → 1.8.1 + added media3-ui-compose (PlayerSurface). One API break fixed (DownloadHelper callback). Launch + playback smoke verified.
commit 8db708345
23:07upgradeP2.1: Compose BOM 2023.04.01 → 2025.06.01 (ui 1.4.0→1.8.3, material3 1.0.1→1.3.2 — ~2 yrs newer). 8 API-break fixes (ripple, AnimatedContentScope, TextFieldDefaults). On-device QA passed.
commit e9f59187e · parallel fix workflow
19:10–19:30docsRewrote Phase-2 full-Compose plan + HTML; researched & documented the Media3 Compose-UI player path (PlayerSurface); pinned BOM/Media3 version targets.
commits 5e5ab3a13 e106503b0 99795b1ca
17:40removalRemoved the dead live-room feature entirely — ~280 files (249 Kotlin + Agora/PubNub SDKs + 51 layouts), −51,816 LOC. Rewired 32 cross-feature consumers (degrade-to-skip). Survived a power-loss mid-run (resumed). QA: no Live tab/nav, playback intact.
commits 017fcb914 95e80f154 (proguard cleanup)
15:16–15:41migrateEditProfile migrated (eager-host); toyota/car cluster deferred (variant pre-existingly un-compilable). Reached 184/353.
commits a5bf2b1cd 43be00cc8
14:26migrateHome / Beranda (fragment_home) migrated via TopRanking eager-host precedent — validated live (tabs, 0 crashes). 182/353.
commits 5520f5515 8f5cd0f81
12:46–13:45migrateSplash, Fullscreen player, ClipsActivity host + Compose skeletons; validation round 2 (4 fable-tier screens PASS with live data).
11:12–12:28bugfixFixed 2 systemic runtime crashes from validation — state-restore "child already has a parent" (detachedForCompose(), 112 sites) and painterResource on shape/layer drawables (rememberDrawablePainter()). OTP screen centering fixed.
commits d785bfb31 89c8feb7e
10:58–11:43docsBatch 10 (177/353); Phase-2 full-Compose roadmap drafted; validation round 1.
Thu, 12 Jun 2026
docsNo migration commits — buffer day between batch 9 (Wed) and the batch-10 resume on Fri. Work resumed 13 Jun 10:39.
Wed, 11 Jun 2026
08:29docsBatch 9 — onboarding/login long-tail completed: ReplyComment hybrid sheet, Collection (homeContainer host), MobileNumber + OTP (NoiceOtpView hosted), login dialogs, onboarding profile steps (username/birthday), CountryChooser; lihat-semua loader skeletons (incl. toyota Car) deleted; live-room sunset removal plan staged.
16 screens · cumulative 162/353 (140 remaining)
07:42removalLive-room layouts deprecated — 51 live-room XMLs excluded from scope, full removal planned (executed 13 Jun).
02:29–02:31migrateBatch 8 — coins + profile + dashboard core: CoinTransactionDetails, VIP subscription dialog, CoinTopUp (LazyVerticalGrid); then Profile, UserSettings, NotificationSettings, Dashboard dialogs, playlist editors, menu sheets, EpisodeDetail + menu, Download.
22 screens · cumulative 146/353
02:00migrateBatch 7 — content-detail + chat surfaces: CoinHistory, genre cluster (my/edit/selection/choose), TextInput dialog (emoji editor), chat sheets (CommentView hosted), EpisodeList sheet, Search, VideoAds sheet, AudioBookDetail, AudioSeries, profile screens, RadioDetail cluster, PlaylistDetail.
22 screens · cumulative 124/353
01:30migrateBatch 6 — live dialogs/sheets (info dialogs, filter/chat/participant options, Diamond/Gift/RoomCreated), InviteSpeaker, PinChat, LiveBlockList, DialogUtil → Compose (ViewTreeOwner fix), QueuePlayer. (most live items later removed in the 13 Jun live-room sunset)
17 screens · cumulative 102/353
01:08migrateBatch 5 — ThemePage + skeleton, Trending cluster (top_charts, trending_episode, top_ranking + shimmers), episode-list screens + shared skeleton, History + Liked content.
14 screens · cumulative 85/353
00:48migrateBatch 4 — home pager tabs via shared HomePagerTabScreen (for-you/radio/podcast/audiobook/audioseries + new variants), GenreDetail, HomeLihatSemua, 4 collection screens, SimilarCatalog, VideoPlayer YouTube host (trending-prep).
18 screens · cumulative 71/353
00:27migrateBatch 3 — onboarding/login/follow/clips: ClipFragment surface, Follow cluster (adapter+FollowView removed), MobileLogin + OnBoarding pager hosts, Sna/Intro/CompleteProfile, ForceUpdate, LoginConfirmation, radio schedule screens, DataObserver debug.
16 screens · cumulative 53/353
Tue, 10 Jun 2026
23:33migrateBatch 2 — dialogs + Report → LazyColumn: EmptyFragment, HtmlAd/VideoQuality/Share/AppReview/Clips/Update/CommentBanInfo dialogs, LiveType, ProfileMenu, and Report screen (list → LazyColumn).
cumulative 37/353
16:45migrateMid-batch cluster: CommunityGuidelines, InAppBrowser, OnBoardingSuccess, CoinHelp + coin/badge/guest/guidelines info dialogs, Notification screen, CoinsUpdate dialog. Tracker + model-tier policy for tiered agent workflows set up.
13:43–14:14migrateBatch 1 (kickoff) — PolicyTerms screen, delete-account flow (4 layouts); 9 unreferenced XML layouts deleted.
first migration batch

🎯 What's left (grounded in the codebase, 18 Jun)

DONE this stretch: all 4 home tabs (ForYou + Podcast/Film/Radio) → native LazyColumn + faithful impression tracking; all 3 content-detail screens de-monolithed (AudioBook/AudioSeries/Episode → flat LazyColumn, no CoordinatorLayout anywhere). Current interop: 154 AndroidView call-sites / 82 files (raw count is up because monoliths were split into many small leaf hosts — each is now a discrete, individually-replaceable View, and the scroll containers + nested-scroll bridges are gone).

NEXTDetail header sub-views → native (now unblocked by the de-monolith) — the Kreator/RootTitleRecycler row, ReadMoreTextView description, genre pills are now flat header items; convert them to native composables. (The checkData entitySubType back-fill is DONE — Content.ensureRowCatalogData.)
NEXTP2.4 — LoadMoreAdapter rollout~59 files still use LoadMoreAdapter; replace with the proven catalog paging-compose LazyColumn pattern. The single biggest interop-reducing chunk.
QUEUEDP2.5 long-tail — remaining shared custom Views: ~63 views/*.kt + 12 homesegmentviews/*.kt (home segment cells, e.g. ContentHorizontalSegment). Convert highest-traffic first.
QUEUEDP2.6b player — landscape + fullscreen rewire, IMA ad surface, mini-player (~10 sites; needs on-device video/fullscreen QA).
GATEP2.2 benchmark — scroll-jank measurement on the LazyColumn rewrites (deferred; validating clean so far).
CAPSTONEactivity_home shell → Scaffold/NavigationBar + Compose Navigation — highest blast radius, sequenced LAST.
FOREVER XMLIrreducible 3rd-party carve-outs (NOT debt): YouTube player, Google IMA ad FrameLayout, WebView, Media3 TrackSelectionView/SubtitleView. TrackSelectionDialog = low-value, deprioritized.

Phase 1 — screen migration (essentially complete)

pie showData "Migrated to Compose" : 184 "Live-room REMOVED" : 51 "Remaining (Phase-2 components)" : 99 "Toyota/car DEFERRED" : 15 "Keep-as-XML" : 4

Phase 2 — full Compose UI

IDTaskStatusDetail
P2.0compileSdk 36 + go latestDONE ✓compileSdk 35→36 · Compose BOM 2025.06.01→2026.05.01 (ui 1.11.1 / material3 1.4.0, M3 Expressive) · Media3 1.8.1→1.10.1 · core-lib desugaring added · VALIDATED on-device (home renders, content-detail scrolls + clicks, episode playback works)
P2.1Compose BOM upgradeDONE ✓2023.04.01 → 2025.06.01 · QA passed
P2.1bMedia3 + ui-composeDONE ✓1.4.1 → 1.8.1 · playback validated
P2.6aPlayer primitive + clips pilotDONE ✓NoicePlayerSurface + clips video
P2.6bPlayer fan-outIN PROGRESSportrait expanded-player video surface → Compose PlayerSurface DONE (commit c590787fa); landscape+fullscreen / IMA ad surface / mini-player (~10 sites) remain
P2.3ErrorView → ComposeDONE ✓render states (progress/no-internet/error) → Compose behind the exact public API; 71 callers + 13 hosts unchanged (commit 836cb1d63)
P2.4Compose paged-listIN PROGRESSpaging-compose proven on catalog; full RecyclerView→LazyColumn conversions done — Download (778f627e9), History (f8d6fecfb) + Liked (bd39a5672), state-list + load-more pattern; roll out to remaining LoadMoreAdapter sites (~63)
P2.2Interop benchmarkGATEscroll jank measurement (deferred — list rewrites validating clean so far)
P2.5Shared Views → composablesIN PROGRESSDONE: EpisodeRow (46b4f61c4, reused on catalog/Download/AudioBook/AudioSeries detail), SimilarRow (409cb1d81), RecentPlaysRow (bd39a5672), NotificationRow (498464169), ScheduleRow (194a5cf5a); home feed (ForYou + Podcast/Film/Radio tabs) → native LazyColumn + faithful impression tracking (1699bb6a2); all 3 detail screens de-monolithed (1c6de802b). Remaining: ~63 views/*.kt + 12 homesegmentviews/*.kt custom Views (home segment cells etc.) + detail header sub-views (Kreator/ReadMore/genre).

Phase 3 — keep-as-XML conversions (branch dev/no-pbi/compose-phase3)

ItemStatusDetail
Keep-as-XML re-verificationDONE ✓All 4 "keep-as-XML" views are migratable; only 3 tiny 3rd-party sub-surfaces stay AndroidView (IMA FrameLayout, Media3 TrackSelectionView/SubtitleView) — KEEP_XML_REVERIFY.md (ef66925cd)
Catalog / channel-detail screenDONE ✓Collapsing-scroll regression fixed → flat single-scroll LazyColumn (no CoordinatorLayout/AppBarLayout/ViewPager2/nested-scroll-interop); tab switch + Konten Serupa empty state; on-device validated (fc259aa13c5bc003c5)
Catalog episode rows → native Compose (= P2.5)DONE ✓Per-row AndroidView(EpisodeView) replaced by native EpisodeRow; behaviour delegated 1:1 (play/queue/download/price/analytics); pixel-validated old-vs-new incl. VIP/price (46b4f61c4)
Catalog Konten Serupa rows → native Compose (= P2.5)DONE ✓Per-row AndroidView(SimilarCatalogView) replaced by native SimilarRow; row-tap routing delegated 1:1 (EventBus OpenIndexEvent); regression-QA'd (populated visual pending similar data) (409cb1d81)
Home "Buat Kamu" (ForYou) feed → native LazyColumn (= P2.4+P2.5)DONE ✓Outer home RecyclerView (HomeSegmentPagingDataAdapter) → native LazyColumn over the same paging flow + PullToRefreshBox; content_horizontal → native carousel (heading + LazyRow of ContentHorizontalSegment cells), other segment types/banner/ads/top-highlight via AndroidView; click/see-all routing 1:1. Revenue impression tracking faithfully reproduced (new HomeImpressionTracking.kt: 30% segment / 15% entity gates, native-LazyRow + inner-RecyclerView scanning, eligibility = legacy) — adversarial parity review 9/9 PASS + segment_viewed/entity_viewed verified live on-device via Firebase debug
Home sibling tabs (Podcast / Film-AudioSeries / Radio) → native LazyColumn (= P2.4+P2.5)DONE ✓All 3 sibling tabs were structurally identical to old ForYou → now host the SAME reusable ForYouScreen + HomeImpressionTracking (outer RecyclerView + adapter + ImpressionTrackerHelper removed from each). Per-tab feed key/source/lifecycle/public-API preserved; added refreshSignal for programmatic refresh; corrected the AudioSeries "ForYou_Page" adapter-tag latent bug. On-device validated incl. live source=Podcast / source=Audioseries impression events
ErrorView (= P2.3)DONE ✓render states → Compose, exact API preserved (836cb1d63)
Player portrait surface (= P2.6b)DONE ✓expanded-player portrait video → Compose PlayerSurface (c590787fa)
Minor-dependency refreshDONE ✓~20 minor-behind deps → latest stable (3 capped by AGP9/compileSdk37/KAPT); QA-validated (8aa79e507)
Player landscape + fullscreen / IMA ad surfaceNEXTthe harder player surfaces (fullscreen rewire + IMA ad slot) — needs device QA for video/fullscreen
TrackSelectionDialogQUEUEDlow value (Media3 TrackSelectionView stays AndroidView); recommend deprioritize
activity_home shellQUEUEDScaffold/NavigationBar — gated on tab-fragment migration + Compose Navigation; sequence last (highest blast radius)
De-monolith detail bodies (Episode/AudioBook/AudioSeries)DONE ✓All 3 DONE — AudioBook, AudioSeries + Episode detail are flat Compose LazyColumns (no CoordinatorLayout/AppBar/NestedScroll/nested-scroll-interop anywhere in the detail family): pinned toolbar + extracted header AndroidView + native EpisodeRow list (AudioBook/AudioSeries) / hosted body incl. paged comments (Episode); AudioSeries keeps the filterLayout chip as a View so PopupFilterMenu anchors. Row actions reproduce EpisodeAdapter AUDIO_BOOK/AUDIO_SERIES 1:1. ultracode 4-agent adversarial review → GO-conditional; fixed the shared high-sev doubled-header-gap (zeroed headerLayout margin) + AudioBook Chapter font + Episode 56dp inset. All build-green + on-device validated (AudioSeries & Episode end-to-end incl. filter popup + paged comments). Follow-up: nativise header sub-views (Kreator/desc/genre) + checkData entitySubType back-fill. (task #36)

🐛 Bugs caught by validation (all fixed)

BugStatusRoot cause / fix
Content-detail pages frozen (no scroll/click)FIXED ✓Missing nested-scroll interop on CoordinatorLayout content (5 screens)
Home tabs stuck on skeletonFIXED ✓Paging itemCount race → onPagesUpdatedFlow (4 fragments)
Dev flavor: empty content everywhereFIXED ✓Dead api.dev host → api.uat
Cold-start crash (state restore)FIXED ✓detachedForCompose()
painterResource crash on shape drawablesFIXED ✓rememberDrawablePainter()

Remaining interop (road to ~0 AndroidView)

154 AndroidView( call-sites across 82 files (grounded re-count, 18 Jun). The raw number rose because the home + 3 detail monoliths were each split from one giant hosted View into several small leaf hosts (toolbar / header / body / errorView, plus per-row hosts) — but the heavy CoordinatorLayout/AppBar/NestedScroll machinery + nested-scroll bridges are gone and every remaining host is now a discrete, individually-replaceable widget. The count falls in earnest as the leaf hosts (segment cells, detail header sub-views, LoadMoreAdapter lists) are ported.

LoadMoreAdapter paged lists (→ P2.4) — biggest chunk~59
Shared custom Views (→ P2.5) — ~63 views/ + 12 homesegmentviews/ remain~75
ErrorView hosts (→ P2.3)0
Player surfaces (→ P2.6b) — portrait done~10

ErrorView ✓ render states now Compose (hosts unchanged via the kept public API). Carve-outs (stay AndroidView, NOT debt): YouTube player (2) · Google Mobile Ads / IMA (7) · WebView (3) · Media3 TrackSelectionView/SubtitleView. Navigation Component kept by design (Compose Navigation = Phase 3 capstone).

Build / infra

Latest commitd298c120c — De-monolith Episode detail + ultracode-review header-gap fixes (detail family 3/3)
Build✓ assembleDevelopmentDebug green (all 3 detail screens de-monolithed to flat LazyColumn)
On-device✓ all 4 home tabs + catalog/Download/History/Liked/Notification/Schedule; all 3 detail screens validated — AudioSeries (filter popup+re-sort) + Episode (Eps 1: header, ad, follow card, paged comments w/ likes/replies, Lihat Episode Lain) end-to-end on fresh AVD, no crash; header-gap fix verified
Push✓ all pushed to dev/no-pbi/compose-phase3

📦 Library freshness

✅ All minor-behind deps now bumped to latest stable + shipped (commit 8aa79e507). Only MAJOR/EOL upgrades remain — these need your review (breaking changes / toolchain). 3 deps are capped below latest by out-of-scope blockers (noted).

Audited 2026-06-14 (current vs latest stable, live from Maven metadata). compileSdk 36 · Compose BOM 2026.05.01 · Media3 1.10.1 · all ~20 minor deps are now current ✓. Remaining gaps, worst-first:

LibraryCurrentLatest stableStatusNotes
AGP tools.build:gradle8.10.19.2.1MAJOR 8→9Toolchain major; needs Gradle/JDK alignment
billing7.0.09.0.0MAJOR 7→9Play requires v8+ for new submissions; v7 deprecated
retrofit2.11.03.0.0MAJOR 2→3Mostly source-compatible; pair with OkHttp 5
okhttp logging-interceptor4.10.05.4.0MAJOR 4→5Network-critical, security/perf line
firebase BOM31.0.134.14.1MAJOR 31→343 majors behind across all Firebase modules
ktor (server)2.2.33.5.0MAJOR 2→3Embedded local server; coordinate w/ Kotlin
moengage13.02.0014.09.02MAJOR 13→14Bump companion inapp/cards/rich-notification too
appsflyer6.12.27.0.0MAJOR 6→7Major SDK bump
lottie5.2.06.7.1MAJOR 5→6Major bump
legacy ExoPlayer exoplayer:*2.19.1EOLEnd-of-life; superseded by Media3 (already 1.10.1). Migrate off.
kotlin2.1.212.4.0CAPPEDHeld at 2.1.21 — hilt-work KAPT bundles kotlin-metadata-jvm maxing at metadata 2.3.0 (=Kotlin 2.3.x). Unblocks after AGP 9 / KSP migration.
hilt2.582.59.2CAPPEDBumped to 2.58 (highest for AGP 8.10.1); 2.59.2 hard-requires AGP 9.0. Unblocks with AGP major.
core-ktx1.17.01.19.0CAPPEDBumped to 1.17.0 (highest for compileSdk 36); 1.19.0 needs compileSdk 37 + AGP 9.1.
glide-compose1.0.0-alpha.3(no stable; beta09)PRE-RELNo stable exists; on an old alpha — left as-is
✅ Now current (bumped this session): lifecycle 2.10.0, lifecycle-compose 2.10.0, navigation+safeargs 2.9.8, material 1.14.0, accompanist 0.36.0, activity-compose 1.13.0, fragment-ktx 1.8.9, paging 3.5.0, work 2.11.2, coil 2.7.0, appcompat 1.7.1, camerax 1.6.1, room 2.8.4, constraintlayout 2.2.1, recyclerview 1.4.0, glide 5.0.7, window 1.5.1.

Full detail + recommended upgrade order in compose-migration/LIBRARY_AUDIT.md. Minors applied + verified (commit 8aa79e507); majors/EOL await your go-ahead.

🧭 Compose-first alignment

Google now positions Jetpack Compose as the primary Android UI toolkit; the View-based Jetpack UI libraries we still host (RecyclerView, CoordinatorLayout, Fragment/Navigation, ViewPager2, SwipeRefreshLayout, CardView, Material Components) are in maintenance mode (critical fixes only) — so every remaining usage is a migration target, validating the zero-XML goal.

Google-recommended areaStatusNoice mapping
New UI built in Compose (no new XML)DONE ✓Policy: every new screen/component is Compose
Material 3 themingDONE ✓material3 1.4.0 (M3 Expressive available)
RecyclerView → Lazy listsTODOP2.4 / P2.5 (paging-compose + row Views)
CoordinatorLayout → ScaffoldTODOTrue rewrite; today an AndroidView + nestedScroll bridge
Custom Views → composablesIN PROGRESSP2.5 — EpisodeView ✓ + SimilarCatalogView ✓; ~89 views/ classes remain
Player → PlayerSurfaceIN PROGRESSP2.6a done (clips); P2.6b fan-out pending
Navigation ComposeTODOPhase 3 — in-scope, highest-risk, sequenced last
Baseline ProfilesTODOPhase 4 polish
Stability / strong-skippingTODOPhase 4 polish
Adaptive layouts (window size classes)TODOPhone + Android Auto landscape

Source: Google's official Compose-first guidance — https://developer.android.com/develop/ui/compose/first.

Timestamps are commit times (WIB). % method — Phase 1: in-scope screens migrated (live-room/keep-as-XML excluded). Phase 2: weighted across P2.1/1b/2/3/4/5/6. Dream: AndroidView sites eliminated vs total minus 3rd-party carve-outs. File counts are greps, approximate. This dashboard is regenerated by the agent after each change.