dev/no-pbi/compose-phase3 (Phase 3 kickoff; forked from phase2) · auto-maintained by the migration agententitySubType 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.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.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.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.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").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).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.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.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 RecyclerView→LazyColumn 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.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.)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).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.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?" → YA → DELETE /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.)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.ic_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.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.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.LazyColumn of EpisodeRow ✓. Converted the catalog all-episodes screen (EpisodeListFragment + SemuaEpisodesAdapter → EpisodeView 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).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).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).LazyColumn of EpisodeRow ✓ — first FULL RecyclerView→Lazy list conversion (real AndroidView host removed). The Download collection screen previously hosted a pre-built RecyclerView (EpisodeLoadMoreAdapter → EpisodeView 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).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).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).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.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.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).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).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.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.onNewIntent re-delivery, navigation, paging content load, image loading — all clean, zero crashes / ANR. Emulator booted on-demand then killed (RAM policy).Configuration.Provider getter→val; onNewIntent(Intent?)→non-null; WebChromeClient.createIntent() null-guard; @EntryPoint 6× var→val (Hilt 2.58). MAJOR/EOL deps (billing/retrofit/okhttp/firebase/ktor/moengage/appsflyer/lottie/AGP, legacy ExoPlayer) remain flagged.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.nestedScroll(rememberNestedScrollInteropConnection()) to 5 CoordinatorLayout detail screens (Episode, Radio, AudioSeries, Channel, AudioBook). Root cause: AppBar swallowed all touch.itemCount race; drive hideLoading() off onPagesUpdatedFlow (4 home fragments). Validated home renders live content.PlayerSurface + reusable NoicePlayerSurface primitive. Dev flavor repointed api.dev→api.uat (dead host). Validated Media3 1.8.1 playback (audio + video).1.4.1 → 1.8.1 + added media3-ui-compose (PlayerSurface). One API break fixed (DownloadHelper callback). Launch + playback smoke verified.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.detachedForCompose(), 112 sites) and painterResource on shape/layer drawables (rememberDrawablePainter()). OTP screen centering fixed.HomePagerTabScreen (for-you/radio/podcast/audiobook/audioseries + new variants), GenreDetail, HomeLihatSemua, 4 collection screens, SimilarCatalog, VideoPlayer YouTube host (trending-prep).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).
| NEXT | Detail 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.) |
| NEXT | P2.4 — LoadMoreAdapter rollout — ~59 files still use LoadMoreAdapter; replace with the proven catalog paging-compose LazyColumn pattern. The single biggest interop-reducing chunk. |
| QUEUED | P2.5 long-tail — remaining shared custom Views: ~63 views/*.kt + 12 homesegmentviews/*.kt (home segment cells, e.g. ContentHorizontalSegment). Convert highest-traffic first. |
| QUEUED | P2.6b player — landscape + fullscreen rewire, IMA ad surface, mini-player (~10 sites; needs on-device video/fullscreen QA). |
| GATE | P2.2 benchmark — scroll-jank measurement on the LazyColumn rewrites (deferred; validating clean so far). |
| CAPSTONE | activity_home shell → Scaffold/NavigationBar + Compose Navigation — highest blast radius, sequenced LAST. |
| FOREVER XML | Irreducible 3rd-party carve-outs (NOT debt): YouTube player, Google IMA ad FrameLayout, WebView, Media3 TrackSelectionView/SubtitleView. TrackSelectionDialog = low-value, deprioritized. |
| ID | Task | Status | Detail |
|---|---|---|---|
| P2.0 | compileSdk 36 + go latest | DONE ✓ | 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.1 | Compose BOM upgrade | DONE ✓ | 2023.04.01 → 2025.06.01 · QA passed |
| P2.1b | Media3 + ui-compose | DONE ✓ | 1.4.1 → 1.8.1 · playback validated |
| P2.6a | Player primitive + clips pilot | DONE ✓ | NoicePlayerSurface + clips video |
| P2.6b | Player fan-out | IN PROGRESS | portrait expanded-player video surface → Compose PlayerSurface DONE (commit c590787fa); landscape+fullscreen / IMA ad surface / mini-player (~10 sites) remain |
| P2.3 | ErrorView → Compose | DONE ✓ | render states (progress/no-internet/error) → Compose behind the exact public API; 71 callers + 13 hosts unchanged (commit 836cb1d63) |
| P2.4 | Compose paged-list | IN PROGRESS | paging-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.2 | Interop benchmark | GATE | scroll jank measurement (deferred — list rewrites validating clean so far) |
| P2.5 | Shared Views → composables | IN PROGRESS | DONE: 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). |
dev/no-pbi/compose-phase3)| Item | Status | Detail |
|---|---|---|
| Keep-as-XML re-verification | DONE ✓ | 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 screen | DONE ✓ | Collapsing-scroll regression fixed → flat single-scroll LazyColumn (no CoordinatorLayout/AppBarLayout/ViewPager2/nested-scroll-interop); tab switch + Konten Serupa empty state; on-device validated (fc259aa13→c5bc003c5) |
| 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 refresh | DONE ✓ | ~20 minor-behind deps → latest stable (3 capped by AGP9/compileSdk37/KAPT); QA-validated (8aa79e507) |
| Player landscape + fullscreen / IMA ad surface | NEXT | the harder player surfaces (fullscreen rewire + IMA ad slot) — needs device QA for video/fullscreen |
TrackSelectionDialog | QUEUED | low value (Media3 TrackSelectionView stays AndroidView); recommend deprioritize |
activity_home shell | QUEUED | Scaffold/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) |
| Bug | Status | Root cause / fix |
|---|---|---|
| Content-detail pages frozen (no scroll/click) | FIXED ✓ | Missing nested-scroll interop on CoordinatorLayout content (5 screens) |
| Home tabs stuck on skeleton | FIXED ✓ | Paging itemCount race → onPagesUpdatedFlow (4 fragments) |
| Dev flavor: empty content everywhere | FIXED ✓ | Dead api.dev host → api.uat |
| Cold-start crash (state restore) | FIXED ✓ | detachedForCompose() |
| painterResource crash on shape drawables | FIXED ✓ | rememberDrawablePainter() |
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.
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).
| Latest commit | d298c120c — 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 |
✅ 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:
| Library | Current | Latest stable | Status | Notes |
|---|---|---|---|---|
AGP tools.build:gradle | 8.10.1 | 9.2.1 | MAJOR 8→9 | Toolchain major; needs Gradle/JDK alignment |
| billing | 7.0.0 | 9.0.0 | MAJOR 7→9 | Play requires v8+ for new submissions; v7 deprecated |
| retrofit | 2.11.0 | 3.0.0 | MAJOR 2→3 | Mostly source-compatible; pair with OkHttp 5 |
okhttp logging-interceptor | 4.10.0 | 5.4.0 | MAJOR 4→5 | Network-critical, security/perf line |
| firebase BOM | 31.0.1 | 34.14.1 | MAJOR 31→34 | 3 majors behind across all Firebase modules |
| ktor (server) | 2.2.3 | 3.5.0 | MAJOR 2→3 | Embedded local server; coordinate w/ Kotlin |
| moengage | 13.02.00 | 14.09.02 | MAJOR 13→14 | Bump companion inapp/cards/rich-notification too |
| appsflyer | 6.12.2 | 7.0.0 | MAJOR 6→7 | Major SDK bump |
| lottie | 5.2.0 | 6.7.1 | MAJOR 5→6 | Major bump |
legacy ExoPlayer exoplayer:* | 2.19.1 | — | EOL | End-of-life; superseded by Media3 (already 1.10.1). Migrate off. |
| kotlin | 2.1.21 | 2.4.0 | CAPPED | Held 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. |
| hilt | 2.58 | 2.59.2 | CAPPED | Bumped to 2.58 (highest for AGP 8.10.1); 2.59.2 hard-requires AGP 9.0. Unblocks with AGP major. |
| core-ktx | 1.17.0 | 1.19.0 | CAPPED | Bumped to 1.17.0 (highest for compileSdk 36); 1.19.0 needs compileSdk 37 + AGP 9.1. |
| glide-compose | 1.0.0-alpha.3 | (no stable; beta09) | PRE-REL | No 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.
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 area | Status | Noice mapping |
|---|---|---|
| New UI built in Compose (no new XML) | DONE ✓ | Policy: every new screen/component is Compose |
| Material 3 theming | DONE ✓ | material3 1.4.0 (M3 Expressive available) |
| RecyclerView → Lazy lists | TODO | P2.4 / P2.5 (paging-compose + row Views) |
| CoordinatorLayout → Scaffold | TODO | True rewrite; today an AndroidView + nestedScroll bridge |
| Custom Views → composables | IN PROGRESS | P2.5 — EpisodeView ✓ + SimilarCatalogView ✓; ~89 views/ classes remain |
| Player → PlayerSurface | IN PROGRESS | P2.6a done (clips); P2.6b fan-out pending |
| Navigation Compose | TODO | Phase 3 — in-scope, highest-risk, sequenced last |
| Baseline Profiles | TODO | Phase 4 polish |
| Stability / strong-skipping | TODO | Phase 4 polish |
| Adaptive layouts (window size classes) | TODO | Phone + 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.