genui: media plugins in `genui.dart`'s import graph cost 10 pub.dev points and the Windows/Linux tags
What it costs today
https://pub.dev/api/packages/genui/score, checked 2026-09-04:
- 130 / 160, Platform support 10 / 20
- tags:
platform:android,platform:ios,platform:macos,platform:web— noplatform:windows, noplatform:linux, nois:wasm-ready - 192 likes, 28,870 downloads in 30 days
I ran pana against copies of genui 0.10.2. The unmodified copy reproduces the pub.dev result exactly (130/160, same four tags), so the harness matches what pub.dev runs.
| variant of genui 0.10.2 | platform tags | is:wasm-ready |
Platform support | total |
|---|---|---|---|---|
| unmodified | android, ios, macos, web | no | 10/20 | 130/160 |
video.dart out of genui.dart's import closure |
+ windows, linux | no | 10/20 | 130/160 |
audio_player.dart out |
android, ios, macos, web | yes | 10/20 | 130/160 |
| both out | + windows, linux | yes | 20/20 | 140/160 |
pana 0.23.19 (the version pub.dev runs); independently reproduced on 0.23.18. Every other report section is byte-identical across all four variants — conventions, docs, analysis 50/50, dependencies 20/40.
Reproduce: copy the package twice, make the edits in one copy, and run dart pub global run pana --no-warning --json <dir> on each.
Downstream, genui_gen is at 150/160 with the same four tags and no is:wasm-ready; pana names the chain through package:genui/genui.dart. Measured against a fixed genui resolved by path, its Platform section goes 10/20 → 20/20 with all six tags plus is:wasm-ready. (That run's total reads 140/160 only because pana flags a path dependency in a publishable package, −20 in Analysis; against a hosted fixed genui that section stays 50/50, so 160/160 — inferred, not measured.) Disclosure: genui_gen is my package.
The two chains
They are separate defects with separate costs.
1. video_player costs the Windows and Linux tags. pana's verbatim explanation, identical for both denials:
package:genui/genui.dartthat imports:package:genui/src/catalog.dartthat imports:package:genui/src/catalog/basic_catalog.dartthat imports:package:genui/src/catalog/basic_catalog_widgets/video.dartthat imports:package:video_player/video_player.dartthat declares support for platforms:Android,iOS,macOS,Web.
2. audioplayers costs is:wasm-ready, via audioplayers/src/audioplayer.dart → path_provider → path_provider_platform_interface → package:platform → dart:io.
Two things about this that are easy to get backwards, and both are checkable in one command:
audioplayersis not the platform-tag culprit. It declares all six platforms and shipsaudioplayers_linux/audioplayers_windows; pub.dev tags itandroid, ios, windows, linux, macos, web.video_playeris the only dependency of genui missing Linux and Windows.- The 10 points are not the platform count. pana's
lib/src/report/multi_platform.dartscores the Platform section 0 with no tags, 10 ifhasWebPlatform && !isWasmReady, 20 otherwise. The number of supported platforms only feeds the description string.stream_chat_flutteris 6-of-6 and still scores 10/20 for this reason.
That is why the middle two rows of the table both read 130/160. Fixing only what pana's message names moves the score by zero. Both files have to leave genui.dart's closure.
Two mechanical details that shape any fix:
- pana computes platform and wasm tags from the transitive closure of one library —
lib/<packageName>.dart, when it exists (pana/lib/src/tag/tagger.dart). Other public libraries inlib/are never inspected. genui already relies on this withlib/parsing.dartandlib/test.dart. So nothing has to be deleted and no dependency has to be dropped — I measured a variant that keeps both widget files and all three plugin deps inpubspec.yaml, moving only the exports into a second public library, and it scores the same 140/160 with all six tags andis:wasm-ready. - pana follows imports, syntactically, with no tree-shaking.
video.dartis the proof: it is already absent fromlib/src/catalog.dart's export list and still costs both tags, purely throughbasic_catalog.dart's import. So "stop exporting them" alone is a no-op, and a@Deprecatedforwarder left onBasicCatalogItemswould re-pull the plugin and undo the fix.
Not covered by the in-flight work
- #1877 — its design text assigns the BasicCatalog widgets to
packages/genui, and its core split already landed in flutter/genui#974 without touchingvideo_player. - #996 — edits
video.dartforVideo.posterUrlbut does not touchpackages/genui/pubspec.yaml. It is open, ~100 files, and touches the same file, so anything here should land after it. - #1863 ("ready to use" vs "requires configuration") is the closest fit.
BasicCatalogItems.asNoAssetCatalog()is already its runtime half; this is the compile-time half of the same line. I'd rather this be folded into #1863 than tracked separately if you see it that way too.
Options
A. Add a top-level platforms: block to packages/genui/pubspec.yaml. Non-breaking, three lines. Restores the Windows and Linux tags, recovers 0 points — the declaration prunes the platform graph but not the wasm graph. Not measured on genui, but stream_chat_flutter, flutter_quill_extensions and cached_video_player_plus all depend on video_player from their primary library and hold six tags this way. Reasonable stopgap for downstream discoverability; not the fix.
B. Move both widget files behind a second public library (package:genui/media.dart), removing the items from asCatalog(). Measured: 140/160, six tags, is:wasm-ready, dependencies unchanged. Breaking: BasicCatalogItems.audioPlayer, BasicCatalogItems.video and the top-level audioPlayer move; asCatalog() returns 16 items rather than 18. That last part is a runtime change, not a compile error — an agent that emits Video hits CatalogItemNotFoundException and renders a FallbackWidget. It also leaves asCatalog() advertising basicCatalogId while omitting two components of that catalog.
C. (recommended) Same move, but keep Video and AudioPlayer in asCatalog() with their schemas and exampleData, backed by a plugin-free default builder — the "video playback is not supported on this platform" card already in video.dart, retargeted to "no media backend registered; import package:genui/media.dart". package:genui/media.dart exports the plugin-backed implementations, and hosts opt in with asCatalog().copyWith(newItems: MediaCatalogItems.all) — Catalog.copyWith already merges by item.name, so registration overwrites the placeholders. Advertised component set stays 18/18, core_catalog_validation_test.dart keeps covering both items, and the degraded state is an explicit card instead of a fallback error. The score outcome here is inferred, not measured — it follows from the mechanism (no restricted package remains in genui.dart's closure), but I have only measured the delete-outright and the plain-move shapes. Measuring it is step one of the PR.
D. Satellite genui_media package. Same measured score as B. Its one distinct benefit is real: native plugin registration follows the resolved pubspec, not the Dart import graph, so only D drops audioplayers*, path_provider, video_player* from every non-media app's build (dev_tools/composer registers audioplayers_linux today and has no audio UI). I have not measured that binary-size delta. Against it: release coupling on a package that broke API in 0.8, 0.9 and 0.10 — genui_firebase_ai is still pinned to genui: ^0.7.0 from 2026-05-04.
E. Re-back both widgets on media_kit. Every widget class in both files is private (_AudioPlayerWidget, _VideoPlayerWidget, _CenterPlayButton, _BottomControlBar), so this is a zero-public-API-change route to the same tags and points, and the only option where Video actually plays on Linux. media_kit and media_kit_video both carry six tags plus is:wasm-ready. Cost: real engineering and libmpv binary weight. Not measured.
Recommendation: C if the goal is the score with the smallest behavioural change, E if the goal is platform support that is true rather than tagged. A is fine as an interim for the tags as long as it isn't mistaken for the fix. Your call — I have no stake in which.
Caveats I'd want on the record
- The Linux tag is not Linux playback.
video_playerhas no Linux implementation, andvideo.dart's_isVideoSupportedalready returns false there. Under B/C/D the tag becomes honest because the default library contains no media plugin, not because Video starts working. is:wasm-readyis a pana rule about the import graph, not a build failure.dart:ioresolves under dart2wasm to a throwing shim (support_conditional_import: falsein the SDK'slibraries.json), and audioplayers guards itspath_providercall with!kIsWeb, soflutter build web --wasmworks on a genui app today. The 10 points are still 10 points; I don't want to oversell what they represent.- The gain is easy to give back. It holds only while
lib/genui.dartexists as the primary library and nothing re-imports a media widget into its closure. I measured the revert: deletelib/genui.dartand everylib/*.dartbecomes a top library and the tags disappear. Worth a pana assertion in CI. - Conditional imports can't help. pana's only declared variables are
dart.library.*, and Android, iOS, Linux, macOS and Windows all shareRuntime.flutterNative— there is no variable that separates Windows from Android.
PR offer
Happy to write it, for whichever option you pick. For C that would be:
- The pana A/B for this exact shape, reported in the PR description with the command to re-run it.
- New
lib/media.dart+lib/src/media_catalog.dartexposingMediaCatalogItems.audioPlayer,.video,.all. basic_catalog.dart: media items keep name/schema/exampleData, default builders become the plugin-free placeholder; plugin imports move out.lib/src/catalog.dart: drop theaudio_player.dartexport (it moves tomedia.dart).format_duration.dartstays and is shared.pubspec.yamlunchanged —audioplayers,video_player,video_player_winall stay, so Windows playback does not regress.examples/simple_chatanddev_tools/composermigrated to the opt-in import.- Tests:
test/catalog/core_widgets/audio_player_test.dartupdated (it is the only test naming either symbol), plus a registration test assertingasCatalog().copyWith(newItems: MediaCatalogItems.all)yields the full 18 with the samecatalogId. Notevideo.darthas no test of its own today; its only coverage is the auto-generated schema check, which option C preserves. - A pana step in CI asserting
platform:windows,platform:linux,is:wasm-ready. - CHANGELOG entry naming both moved symbols.
Per docs/contributing/triage.md I'll wait for a priority label or an assignment before opening anything.
Two unrelated notes, mentioned so they don't get bundled in here
- genui's Dependencies section is 20/40 because "compatible with dependency constraint lower bounds" scores 0/20 — downgrade analysis fails with
UNDEFINED_CLASS 'SchemaRegistry'atlib/src/engine/surface_controller.dart:65and 9 similar. That is adart pub upgrade --tightenaway and is worth twice what this issue is worth. Happy to file it separately. - genui 0.10.2's published pubspec still sets
issue_tracker: https://github.com/flutter/genui/issues, which is disabled, so reporters get bounced before they reach this tracker.
Source: a2ui-project/a2ui