Skip to content

Add core-compose module (#1239)#1304

Open
gaeun5744 wants to merge 10 commits into
PierfrancescoSoffritti:devfrom
gaeun5744:feature/core-compose
Open

Add core-compose module (#1239)#1304
gaeun5744 wants to merge 10 commits into
PierfrancescoSoffritti:devfrom
gaeun5744:feature/core-compose

Conversation

@gaeun5744

Copy link
Copy Markdown

Fixes #1239.

What

Adds a new core-compose module that provides a Compose-native API for the YouTube
player:

  • rememberYouTubePlayerState(...) — owns a single YouTubePlayerView across
    recompositions, wires it to the current LifecycleOwner, and releases it on
    dispose.
  • YouTubePlayer(state, modifier, fullscreenListener) — thin AndroidView
    wrapper that renders the view owned by the state.

The module is published as core-compose and depends on :core, so existing
users are unaffected.

Why

Issue #1239 reports a sporadic android.net.ConnectivityManager$TooManyRequestsException
thrown from NetworkObserver.doObserveNetwork when the player is used from
Compose. The root cause is that NetworkObserver calls
ConnectivityManager.registerDefaultNetworkCallback, which is subject to a
per-UID limit (~100 concurrent callbacks). In Compose, two patterns blow that
limit:

  1. View recreated on recomposition. The natural Compose pattern is to
    construct YouTubePlayerView inside AndroidView's factory. Lists
    (LazyColumn), navigation, or configuration changes create/release views
    rapidly — each initialize() call registers another network callback, and
    they accumulate faster than the system releases them.

  2. Callback registered during view construction. Setting
    enableAutomaticInitialization = false after YouTubePlayerView(ctx)
    comes too late — the view's init block already read the styleable
    attribute as its default (true) and called
    initialize(..., handleNetworkEvents = true, ...), which registers a
    network callback. A subsequent manual initialize(listener, options)
    overload hardcodes handleNetworkEvents = true internally
    (YouTubePlayerView.kt:151), so it registers a second callback. To make
    matters worse, NetworkObserver.doObserveNetwork overwrites its own
    networkCallback field without unregistering the previous one, so the
    first callback leaks permanently.

This PR fixes both by:

  • Inflating the view from an XML layout that sets
    app:enableAutomaticInitialization="false" before the init block runs, so
    no auto-initialization happens at construction time and we control the
    single initialize(...) call ourselves.
  • Scoping the view to remember { ... } so it survives recompositions and is
    created exactly once per state instance. DisposableEffect attaches the
    lifecycle observer and calls release() on dispose, which unregisters the
    network callback deterministically.

Sample app

Adds a LazyColumnExampleActivity that scrolls 30 autoplaying videos in a
LazyColumn with handleNetworkEvents = true. This reproduces the worst case
from #1239 (rapid view create/release + real network traffic) and is the
regression test I used to verify the fix.

Tested on

  • Pixel 7a emulator, API 34 — scrolled the LazyColumn example for several
    minutes without hitting the crash. On dev the same scenario reproduces
    TooManyRequestsException within a few scroll passes.

Notes for review

  • The existing ComposeExampleActivity in core-sample-app is left in place
    as a low-level AndroidView example. Happy to migrate it to the new API if
    preferred.
  • No changes to :core. This is purely additive.

@PierfrancescoSoffritti PierfrancescoSoffritti left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

thanks for the PR! what is the drawback of adding compose support directly in the core module?

/**
* Compose-native entry point for the YouTube player.
*
* Renders the [YouTubePlayerView] owned by [state]. Because the view lives inside the

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

remove this bit from kdoc, unnecessary details

* Renders the [YouTubePlayerView] owned by [state]. Because the view lives inside the
 * [rememberYouTubePlayerState] scope rather than being recreated on every recomposition, the
 * player's internal network observer is registered only once per state instance — which is the
 * Compose-safe way to avoid `ConnectivityManager$TooManyRequestsException`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — removed the network-observer paragraph, kept only the param docs.

fun YouTubePlayer(
state: YouTubePlayerState,
modifier: Modifier = Modifier,
fullscreenListener: FullscreenListener? = null,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

how did you handle going full screen in compose? would be nice to see in the sample

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good idea!
Added ComposeFullscreenExampleActivity to the sample app — uses the new fullscreenListener parameter and attaches the fullscreen view to the activity's decorView, same pattern as the existing FullscreenExampleActivity.

* Owns a single [YouTubePlayerView] across Compose recompositions.
*
* Instances must be created with [rememberYouTubePlayerState] and passed to the [YouTubePlayer]
* composable. The underlying view is created once inside the enclosing `remember` scope and

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The underlying view is created once inside the enclosing `remember` scope and
 * released when the state leaves composition, so the internal network observer registered by the
 * player is registered at most once per state instance. This avoids the
 * `ConnectivityManager$TooManyRequestsException` that can occur when rapid recompositions recreate
 * the view and pile up `registerDefaultNetworkCallback` calls above the per-UID system limit.

remove

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this can be done programmatically

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! Added a YouTubePlayerView(context, enableAutomaticInitialization: Boolean) constructor in :core so rememberYouTubePlayerState can construct the view programmatically. XML layout and res/ directory removed.

gaeun5744 added 5 commits May 3, 2026 16:44
The existing constructor reads enableAutomaticInitialization from styleable
attributes and defaults to true, so the init block has already auto-initialized
(and registered a network callback) by the time a caller can flip the property
from Kotlin. Add an additive constructor that takes the flag explicitly and
applies it before the init block runs, enabling programmatic construction
without inflating from XML.
…State

Use the new YouTubePlayerView(context, enableAutomaticInitialization = false)
constructor instead of inflating from an XML layout. Removes the layout
resource and the now-unused res/ directory, and trims the verbose KDoc on
YouTubePlayerState.
Demonstrates the FullscreenListener parameter on the YouTubePlayer composable
by attaching the fullscreen view to the host activity's decorView. Wraps
content in a Scaffold so status/navigation bar insets are applied to the
inline player, while the fullscreen overlay stays outside the Scaffold to
cover the system bar area.
@gaeun5744

Copy link
Copy Markdown
Author

thanks for the PR! what is the drawback of adding compose support directly in the core module?

@PierfrancescoSoffritti
Thanks! A few reasons I kept it as a separate :core-compose module:

  1. Forced dependency on Compose — pulling Compose into :core means every consumer (including pure View-based apps) downloads androidx.compose.runtime / ui / foundation and the lifecycle-runtime-compose artifact. That's a meaningful APK size hit for users who don't want Compose.
  2. Toolchain coupling:core would have to apply org.jetbrains.kotlin.plugin.compose and enable buildFeatures.compose = true, which forces a Compose-compatible Kotlin / AGP / Compose-compiler combination on the library and its publish pipeline. Today :core has no such constraint.
  3. Java-friendliness of :core:core is still consumable from Java-only projects. A Compose dependency would effectively make Kotlin (and the Compose compiler plugin) mandatory.
  4. Convention — Jetpack itself ships Compose support as separate -compose artifacts (lifecycle-runtime-compose, navigation-compose, paging-compose, hilt-navigation-compose, …), so a core-compose sibling matches what users already expect.

Happy to fold it into :core if you'd prefer; just wanted to flag the trade-offs.

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.

ConnectivityManager$TooManyRequestsException when using compose

2 participants