Skip to content

refactor: drop runBlocking { getString } from common repos and parsers#1051

Open
anikettuli wants to merge 2 commits into
NuvioMedia:cmp-rewritefrom
anikettuli:fix/runblocking-getstring-bundle
Open

refactor: drop runBlocking { getString } from common repos and parsers#1051
anikettuli wants to merge 2 commits into
NuvioMedia:cmp-rewritefrom
anikettuli:fix/runblocking-getstring-bundle

Conversation

@anikettuli

@anikettuli anikettuli commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces a cross-cutting threading anti-pattern across the meta-details, collection, Trakt, profile, and TMDB code paths, and fixes one localization bug surfaced while auditing the same call sites.

  • Removes 23 of 41 runBlocking { getString(...) } call sites in commonMain. The remaining 18 are in androidMain platform-bridge callbacks (OkHttp / ExoPlayer / NotificationManager) and are tracked as a follow-up.
  • Fixes a mis-localized MetaTrailer.type fallback that broke trailer category grouping/sorting in non-English locales.

PR type

  • Refactor + bug fix

Why

runBlocking { getString } removal. getString from compose-resources is suspend for a reason — wrapping it in runBlocking blocks the calling thread on every property read or recomposition. Several offenders were inside data-class property getters (StreamItem.streamLabel, ManagedAddon.displayTitle) which are read by Compose on every recomposition, and inside repository helpers backing tab labels and validation errors. On Kotlin/JS it would deadlock outright.

The fix uses three patterns. Where the enclosing function was already suspend (or had a fully-suspend caller chain), runBlocking is dropped and the enclosing function marked suspend; composable onClick handlers were wrapped in coroutineScope.launch { ... }. Touched in this pass: MetaDetailsParser.parse, CollectionRepository.validateJson, FolderDetailRepository.initialize, TraktAuthRepository.onConnectRequested, ProfileRepository.verifyPinLocally, HomeCatalogSettingsRepository.{syncCatalogs, syncCollections, buildCollectionDefinitions}, TmdbMetadataService.{buildPeople, toMetaTrailer}.

Where the call sat in a non-Composable helper that was only ever called from composables (Int.label, MetaVideo.episodeBadge in DetailSeriesContent.kt), the helper was marked @Composable and switched to stringResource(...). Where the call sat in a data-class property getter that cannot be made suspend, the fallback was replaced with a literal English string; those fallbacks only fire in degenerate edge cases (addon manifest with no name AND no URL-derived slug). Dead title / description fields on MetaScreenSectionItem were removed in the same pass — the only consumer was reading key.titleRes directly already, so the fields were holding stale strings produced by the very runBlocking call this PR removes. MetaScreenSectionKey.titleRes was elevated from private to internal to share between settings page and the meta details screen.

MetaTrailer.type fallback. MetaTrailer.type is a category enum (Trailer / Teaser / Clip) used as a grouping/sort key in TmdbMetadataService and as a category label in DetailTrailersSection. It was defaulting to the localized Res.string.generic_trailer string when missing, which meant a trailer with no type from a French-locale device grouped under "Bande-annonce" while the same dataset on an English device grouped under "Trailer". The fix is to default to empty string; DetailTrailersSection.kt:60 already does trailer.type.ifBlank { localizedFallback } at render time, so the user-visible behavior is unchanged but the underlying data is now locale-independent.

Policy check

  • This PR is not cosmetic-only, unless it is a translation PR.
  • This PR does not add a new major feature without prior approval.
  • This PR is small in scope and focused on one problem.
  • If this is a larger or directional change, I linked the approved feature request issue below.

Testing

./gradlew :composeApp:compileFullDebugKotlinAndroid passes clean. ./gradlew :composeApp:testFullDebugUnitTest passes; existing MetaDetailsParserTest was wrapped in runBlocking since MetaDetailsParser.parse is now suspend.

Manual (please verify during review):

  • Trakt settings → tap Connect: browser still opens (the callback is now suspending via coroutineScope.launch, no behavior change).
  • Collection Management → Import JSON with a malformed payload: the localized validation error still displays in the dialog.
  • Series details with tab layout enabled in Meta Screen settings: tab section labels render with their localized strings (a transient regression here from the refactor was caught in review and fixed in the same PR).
  • Title with multiple TMDB ratings: audience-score icon's TalkBack announcement is localized.
  • Title with TMDB trailers, some with missing type: blank types fall into the same group as the localized "Trailer" category rather than producing a separate empty-name group.

Screenshots / Video (UI changes only)

No new UI surface.

Breaking changes

None. All changes are internal to commonMain; no public KMP API was removed. Several internal repository functions became suspend and their callers were updated in the same PR.

Linked issues

None linked. Surfaced during a code audit of runBlocking usage.

(Supersedes #1050, which auto-closed when the branch was renamed — cross-fork PRs don't follow GitHub's branch-rename redirect.)

Copilot AI review requested due to automatic review settings May 13, 2026 18:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Refactors multiple commonMain call sites to remove runBlocking { getString(...) } around Compose resources, pushing localization lookups into suspend/composable contexts, and fixes locale-dependent grouping caused by a localized MetaTrailer.type fallback.

Changes:

  • Replaces many runBlocking { getString(...) } usages by making call chains suspend, moving lookups into composables via stringResource(...), or caching string fallbacks within suspend functions.
  • Fixes MetaTrailer.type defaulting to a localized string by making the fallback locale-independent ("") and applying the localized label at render/grouping time.
  • Removes stale localized fields from MetaScreenSectionItem and updates tab labeling to derive titles from StringResource at render time.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserTest.kt Updates tests to call the now-suspend MetaDetailsParser.parse.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktCommentsRepository.kt Removes blocking resource lookup by caching a fallback username string in a suspend context.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktAuthRepository.kt Makes connect flow suspend and replaces blocking localization helper with getString.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/tmdb/TmdbMetadataService.kt Removes runBlocking lookups, converts helpers to suspend, and fixes trailer type fallback/grouping.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt Replaces localized default stream label with a literal fallback.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/TraktSettingsPage.kt Wraps connect actions in rememberCoroutineScope().launch to call suspend repository code.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/MetaScreenSettingsPage.kt Exposes MetaScreenSectionKey.titleRes for reuse in details UI.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt Removes blocking localization helper by making a verification helper suspend and calling getString.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeCatalogSettingsRepository.kt Converts sync/build helpers to suspend to allow non-blocking getString usage.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaScreenSettingsRepository.kt Removes localized string fields from section items and drops blocking localizedString helper.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt Uses stringResource(key.titleRes) when rendering tab labels.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt Makes parsing suspend and removes blocking string lookups while parsing videos/trailers/streams.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt Converts helper label/badge builders to composables using stringResource.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt Fixes TalkBack/localization for the audience-score rating icon without blocking calls.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt Makes initialization suspend and removes blocking string lookups for tab/error labels.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionRepository.kt Makes JSON validation suspend and removes blocking localized error creation.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionManagementScreen.kt Calls suspend JSON validation from a UI coroutine scope.
composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonModels.kt Replaces localized fallback addon label with a literal fallback in a getter.

Comment thread composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt Outdated
@anikettuli anikettuli force-pushed the fix/runblocking-getstring-bundle branch from 71bea9a to e86a5fd Compare May 17, 2026 16:29
@tapframe tapframe force-pushed the cmp-rewrite branch 2 times, most recently from 4dd7521 to 614ab1f Compare May 30, 2026 06:35
anikettuli and others added 2 commits June 11, 2026 16:43
Replaces 23 of 41 runBlocking { getString } sites in commonMain across
the meta-details, collection, Trakt, profile, and TMDB code paths.
Wrapping suspend getString in runBlocking blocks the calling thread on
every property read or recomposition and deadlocks on Kotlin/JS.

Three patterns: propagate suspend up call chains that were already
fully suspend; mark non-Composable helpers @composable + use
stringResource; and where the call sat in a data-class property
getter, use a literal English fallback (fires only on degenerate
addon manifests with no name and no URL-derived slug).

Also fixes MetaTrailer.type defaulting to a localized "Trailer"
string when missing — UI already handles blank type with a localized
fallback at render time, so the eager localization mismatched
grouping/sort keys across locales.

The 18 remaining sites are in androidMain platform-bridge callbacks
(OkHttp / ExoPlayer / NotificationManager) and are tracked as a
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StreamItem.streamLabel now returns name.orEmpty() so the data class
stays locale-neutral. The four composable display sites
(StreamsScreen, PlayerSourcesPanel, PlayerEpisodesPanel,
PlayerControls) localize the blank case with
stringResource(Res.string.stream_default_name). DownloadsRepository's
fallbackTitle path is unchanged because buildFileName already does
.ifBlank { "download" }; DownloadItem.streamTitle is only re-read in
PlayerScreen which already has its own .ifBlank fallback.

Also drops the redundant outer runBlocking wrapper on the
assertFailsWith test in MetaDetailsParserTest — only the inner
runBlocking is needed since assertFailsWith's lambda is non-suspend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@anikettuli anikettuli force-pushed the fix/runblocking-getstring-bundle branch from e86a5fd to 29cb596 Compare June 11, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants