#2532·a2ui

genui: media plugins in `genui.dart`'s import graph cost 10 pub.dev points and the Windows/Linux tags

Author: diegolopezrmCreated Sep 4, 2026Updated Sep 15, 2026
LabelsP2status: first-line-handled

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 — no platform:windows, no platform:linux, no is: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.dart that imports: package:genui/src/catalog.dart that imports: package:genui/src/catalog/basic_catalog.dart that imports: package:genui/src/catalog/basic_catalog_widgets/video.dart that imports: package:video_player/video_player.dart that declares support for platforms: Android, iOS, macOS, Web.

2. audioplayers costs is:wasm-ready, via audioplayers/src/audioplayer.dartpath_providerpath_provider_platform_interfacepackage:platformdart:io.

Two things about this that are easy to get backwards, and both are checkable in one command:

  • audioplayers is not the platform-tag culprit. It declares all six platforms and ships audioplayers_linux / audioplayers_windows; pub.dev tags it android, ios, windows, linux, macos, web. video_player is the only dependency of genui missing Linux and Windows.
  • The 10 points are not the platform count. pana's lib/src/report/multi_platform.dart scores the Platform section 0 with no tags, 10 if hasWebPlatform && !isWasmReady, 20 otherwise. The number of supported platforms only feeds the description string. stream_chat_flutter is 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 in lib/ are never inspected. genui already relies on this with lib/parsing.dart and lib/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 in pubspec.yaml, moving only the exports into a second public library, and it scores the same 140/160 with all six tags and is:wasm-ready.
  • pana follows imports, syntactically, with no tree-shaking. video.dart is the proof: it is already absent from lib/src/catalog.dart's export list and still costs both tags, purely through basic_catalog.dart's import. So "stop exporting them" alone is a no-op, and a @Deprecated forwarder left on BasicCatalogItems would 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 touching video_player.
  • #996 — edits video.dart for Video.posterUrl but does not touch packages/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_player has no Linux implementation, and video.dart's _isVideoSupported already 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-ready is a pana rule about the import graph, not a build failure. dart:io resolves under dart2wasm to a throwing shim (support_conditional_import: false in the SDK's libraries.json), and audioplayers guards its path_provider call with !kIsWeb, so flutter build web --wasm works 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.dart exists as the primary library and nothing re-imports a media widget into its closure. I measured the revert: delete lib/genui.dart and every lib/*.dart becomes 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 share Runtime.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:

  1. The pana A/B for this exact shape, reported in the PR description with the command to re-run it.
  2. New lib/media.dart + lib/src/media_catalog.dart exposing MediaCatalogItems.audioPlayer, .video, .all.
  3. basic_catalog.dart: media items keep name/schema/exampleData, default builders become the plugin-free placeholder; plugin imports move out.
  4. lib/src/catalog.dart: drop the audio_player.dart export (it moves to media.dart). format_duration.dart stays and is shared.
  5. pubspec.yaml unchanged — audioplayers, video_player, video_player_win all stay, so Windows playback does not regress.
  6. examples/simple_chat and dev_tools/composer migrated to the opt-in import.
  7. Tests: test/catalog/core_widgets/audio_player_test.dart updated (it is the only test naming either symbol), plus a registration test asserting asCatalog().copyWith(newItems: MediaCatalogItems.all) yields the full 18 with the same catalogId. Note video.dart has no test of its own today; its only coverage is the auto-generated schema check, which option C preserves.
  8. A pana step in CI asserting platform:windows, platform:linux, is:wasm-ready.
  9. 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' at lib/src/engine/surface_controller.dart:65 and 9 similar. That is a dart pub upgrade --tighten away 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.