refactor: fix lazy list stable keys and programme lookup scalability#392
Conversation
PR 1: Add REFACTORING.md with feature status matrix (stable/experimental/deprecated)
and 7-phase refactoring roadmap.
PR 2: Add stable keys to all lazy list items missing them:
- PlaylistGallery: items(entries.size, key = { entries[it].key.url })
- ChannelGallery: items(channels.itemCount, key = channels.itemKey { it.id })
- FavoriteGallery: items(count = channels.itemCount, key = channels.itemKey { it.id })
- ExtensionScreen: items(apps, key = { it.packageName })
- PlayerPanel: items(channels.itemCount, key = channels.itemKey { it.id })
- PlaylistTabRow: items(categories, key = { it })
Paging items use the canonical itemKey() API from androidx.paging.compose
instead of peek(), as recommended by the Paging 3 + Compose documentation.
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/70412a8f-8c9c-4b47-9b19-8355b03d4062
Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
core/foundation/build.gradle.kts was missing the org.jetbrains.kotlin.plugin.serialization plugin despite Abi being annotated @serializable. Without the compiler plugin no serializer is generated for Abi, causing data:compileReleaseKotlin to fail with "Serializer has not been found for type 'Abi'" when TvInfo is compiled. Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/55bd1ca3-08c0-4aa2-8b69-0de40f02833a Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Per Compose API guidelines the modifier parameter should be the last optional parameter before any trailing composable slots. In Preference() it was the second parameter. Moved to just before the trailing composable so it follows the canonical position. All call sites already use named arguments so no callers need changes. Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/05c6576e-9d0f-4800-bf81-360f3b3ede3a Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
The previous commit incorrectly moved modifier away from the first optional parameter slot. Official Compose API Guidelines require modifier to be the first optional parameter (immediately after all required parameters). Restore the original correct order. Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/05c6576e-9d0f-4800-bf81-360f3b3ede3a Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/2026ec1a-92ac-47e2-987a-1eadf82fae43 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/f53834df-91ea-4c0d-a584-41e3623293d1 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
|
|
||
| val time = Clock.System.now().toEpochMilliseconds() | ||
| return relationIds | ||
| .chunked(RELATION_ID_QUERY_BATCH_SIZE) |
There was a problem hiding this comment.
这里用 chunked 是为了避免 relation_id in (:relationIds) 在频道很多时一次绑定过多参数,触发 SQLite host-parameter 上限;同时仍然把原来逐频道查询合并成少量批量查询。当前批大小 500 是保守值,给 epgUrls 和其他参数留余量。相关提交:21da6ec。
There was a problem hiding this comment.
其实你是牺牲极端情况的可用性换取了性能,阅读这段逻辑,看看有没有更好的写法,有必要的话你可以新增dao方法、修改UI层逻辑
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/fb423185-0ea2-4f71-a865-e55b77783d87 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/fb423185-0ea2-4f71-a865-e55b77783d87 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot firewall configuration updated |
已重试目标编译。防火墙更新后 AGP 解析已不再阻塞,但当前 sandbox 中 |
There was a problem hiding this comment.
Pull request overview
Refactors playlist/channel UI to use stable item keys in Compose lazy components and improves EPG “current programme” lookup by introducing a bulk query that fetches current programmes for all channels in a playlist.
Changes:
- Add stable keys to multiple
Lazy*and paging-backed lists to prevent item state jumps and improve scroll stability. - Add
ProgrammeRepository.getProgrammesCurrently(playlistUrl)+ DAO query to fetch “current” programmes in bulk for a playlist. - Update playlist UI/data flow to hydrate paged channels with refreshed programme metadata (
ChannelWithProgramme) instead of per-item lookups.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepositoryImpl.kt | Adds bulk “current programmes” fetch and maps results by relation/channel id. |
| data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt | Extends repository API with bulk current-programmes lookup. |
| data/src/main/java/com/m3u/data/repository/channel/ChannelRepositoryImpl.kt | Exposes relation-id observation backed by DAO. |
| data/src/main/java/com/m3u/data/repository/channel/ChannelRepository.kt | Adds observeRelationIdsByPlaylistUrl to support programme refresh triggers. |
| data/src/main/java/com/m3u/data/database/dao/ProgrammeDao.kt | Introduces playlist-wide “current programme” SQL query. |
| data/src/main/java/com/m3u/data/database/dao/ChannelDao.kt | Adds query to observe distinct relation IDs for a playlist. |
| core/foundation/build.gradle.kts | Applies Kotlin serialization plugin to the foundation module. |
| business/playlist/src/main/java/com/m3u/business/playlist/PlaylistViewModel.kt | Adds ChannelWithProgramme, maintains currentProgrammes, and maps paging data to include programmes. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/playlist/PlaylistScreen.kt | Refactors to state/actions params and adapts to ChannelWithProgramme-based paging flows. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/playlist/components/PlaylistTabRow.kt | Adds stable keys for category items. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/playlist/components/ChannelGallery.kt | Switches to ChannelWithProgramme and adds stable paging keys. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/foryou/components/PlaylistGallery.kt | Adds stable keys for playlist items. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/favourite/components/FavoriteGallery.kt | Adds stable paging keys for favorites list. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/extension/ExtensionScreen.kt | Adds stable keys for app list items. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/business/channel/components/PlayerPanel.kt | Adds stable paging keys for channel gallery items. |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/AppViewModel.kt | Adapts global search paging to emit ChannelWithProgramme (programme = null). |
| app/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt | Updates ChannelGallery usage to the new ChannelWithProgramme flow shape. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| SELECT * FROM programmes | ||
| WHERE epg_url in (:epgUrls) | ||
| AND relation_id in ( | ||
| SELECT relation_id FROM streams | ||
| WHERE playlist_url = :playlistUrl | ||
| AND relation_id IS NOT NULL | ||
| ) | ||
| AND start <= :time | ||
| AND `end` >= :time |
| SELECT * FROM programmes | ||
| WHERE epg_url in (:epgUrls) | ||
| AND relation_id in ( | ||
| SELECT relation_id FROM streams | ||
| WHERE playlist_url = :playlistUrl | ||
| AND relation_id IS NOT NULL | ||
| ) | ||
| AND start <= :time | ||
| AND `end` >= :time |
| return programmeDao.getCurrentByPlaylistUrlAndEpgUrls( | ||
| playlistUrl = playlistUrl, | ||
| epgUrls = epgUrls, | ||
| time = time | ||
| ).associateBy { it.channelId } |
| channelRepository.observeRelationIdsByPlaylistUrl(playlistUrl) | ||
| .map { it.toSet() } | ||
| .distinctUntilChanged() | ||
| .let { relationIds -> | ||
| merge( | ||
| relationIds.take(1), | ||
| relationIds.drop(1).debounce(1.seconds) | ||
| ) | ||
| } |
| @@ -282,6 +311,7 @@ class PlaylistViewModel @Inject constructor( | |||
| ) | |||
| } | |||
| .flow | |||
| .withProgrammes(currentProgrammes) | |||
| .cachedIn(viewModelScope) | |||
| ) | |||
| } else { | |||
| @@ -295,6 +325,7 @@ class PlaylistViewModel @Inject constructor( | |||
| ) | |||
| } | |||
| .flow | |||
| .withProgrammes(currentProgrammes) | |||
| .cachedIn(viewModelScope) | |||
| } | |||
|
@copilot 看一下评论的问题 |
Agent-Logs-Url: https://github.com/oxyroid/M3UAndroid/sessions/6ea0737f-ad59-4ee0-9366-2f91dbb66050 Co-authored-by: oxyroid <70512220+oxyroid@users.noreply.github.com>
已继续修改并提交 d060758:修复了批量 current programme 查询的确定性/索引问题,并调整了 playlist Flow/Paging 组合以避免重复订阅和 programme 刷新时反复重建 cached pager。无 UI 改动。
已处理这些 review 点,提交为 d060758:DAO 现在每个 relation_id 只返回一个确定排序后的 current programme,schema v21 增加匹配查询的复合索引,ViewModel 改为单流首帧后 debounce,并将 cached pager 与 programme metadata 映射拆开。无 UI 改动。 |
Refactors playlist programme metadata loading to avoid per-item current programme queries while preserving availability for large playlists.
Current programme lookups now use playlist-scoped relation IDs inside the DAO instead of passing all relation IDs through batched
INparameters, avoiding SQLite host-parameter pressure in extreme channel-count cases. The DAO query also deterministically returns at most one current programme per relation ID when multiple EPG sources overlap.Adds a Room v21 composite index on programme lookup fields to keep current-programme queries scalable across large EPG tables.
The playlist ViewModel now observes distinct relation IDs as refresh triggers instead of full channel rows, avoids duplicate Room subscriptions when debouncing after the first emission, and keeps cached paging flows stable while only remapping programme metadata on refresh.
The temporary
REFACTORING.mdroadmap was removed.