Skip to content

feat: setup sse connection to broadcast skin and project updates#5

Merged
koukibadr merged 1 commit into
mainfrom
feat/live-skin/setup-sse-connection
Jun 24, 2026
Merged

feat: setup sse connection to broadcast skin and project updates#5
koukibadr merged 1 commit into
mainfrom
feat/live-skin/setup-sse-connection

Conversation

@koukibadr

@koukibadr koukibadr commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added live theme/skin updates via a persistent backend stream, so changes can appear automatically.
    • FlutterSkin.init() now performs full one-call setup, including initial loading and lifecycle-aware pause/resume behavior.
  • Bug Fixes
    • Improved update reliability with automatic reconnect and backoff when the stream drops.
  • Documentation
    • Updated the changelog and README installation snippet for version 0.0.2.
  • Examples
    • Refreshed the example app to react to runtime skin changes.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@koukibadr, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f33f43ce-bd31-4b19-82cc-450ddef94d69

📥 Commits

Reviewing files that changed from the base of the PR and between 3b452f6 and 39e52cf.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • example/android/app/src/main/AndroidManifest.xml
  • example/lib/main.dart
  • example/lib/widgets/movie_card.dart
  • lib/constants/fskin_constants.dart
  • lib/flutter_skin.dart
  • lib/remote/fskin_remote_config.dart
  • lib/services/fskin_subscriber.dart
  • lib/services/skin_service.dart
  • pubspec.yaml
📝 Walkthrough

Walkthrough

Version 0.0.2 adds backend skin streaming, lifecycle-aware stream management, a public skin-change stream, example app rebuilds on updates, and release/version metadata updates.

Changes

SSE Skin-Change Broadcasting

Layer / File(s) Summary
Backend host constant and skin fetch wiring
lib/constants/fskin_constants.dart, lib/services/skin_service.dart
FskinConstants defines the backend host, and SkinService.fetchData() uses it for the skin request.
SSE subscriber with reconnect
lib/services/fskin_subscriber.dart
FskinSubscriber opens the /fskin/stream request, parses stream events, triggers updates for matching event names, retries with backoff, and disposes its HTTP resources.
Remote config skin-change stream
lib/remote/fskin_remote_config.dart
FskinRemoteConfig adds a broadcast StreamController<ThemeData> and exposes onSkinChanged; fetchConfig() emits updated theme data when a cached config is present.
FlutterSkin stream and lifecycle wiring
lib/flutter_skin.dart
FlutterSkin becomes a WidgetsBindingObserver, exposes onSkinChanged, initializes the singleton observer during init(), starts the subscriber stream, and handles app resume/pause lifecycle changes.
Example app rebuilds on skin changes
example/lib/main.dart, example/lib/widgets/movie_card.dart, example/android/app/src/main/AndroidManifest.xml
example/lib/main.dart subscribes to FlutterSkin.onSkinChanged and rebuilds MyApp; the example manifest adds network permissions, and the movie card text truncation is adjusted.
Version and release notes update
pubspec.yaml, README.md, CHANGELOG.md
pubspec.yaml bumps the package version, README.md updates the install snippet, and CHANGELOG.md adds the 0.0.2 release entry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • koukibadr/flutter_skin#2: Modifies the same FlutterSkin.init / FskinRemoteConfig / SkinService flow with API-key-based fetching that this PR extends with SSE-driven updates.

Poem

🐇 I hop by moonlit streaming light,
Skin changes flutter through the night.
One init() call, then themes awake,
And every update makes my whiskers shake.
No silence now, the rabbit sings:
Fresh colors bloom on tiny wings 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding SSE-based streaming to broadcast skin updates and react to project update events.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 1: The changelog version is out of sync with the package version, so
update the entry in CHANGELOG.md to match the version set in pubspec.yaml.
Locate the top release heading in the changelog and change the documented
version from 0.0.2-alpha.1 to 0.0.2 so the release notes align with the
published package.
- Line 1: The first heading in the changelog uses a level-2 heading, which
violates markdownlint MD041; update the top-of-file heading in CHANGELOG.md to
use an H1 instead. Keep the existing version/date text, but change the heading
marker at the start of the file from a second-level heading to the first-level
heading so the document begins with the correct markdown structure.

In `@example/lib/main.dart`:
- Around line 8-10: The example app is hardcoding a real-looking apiKey value in
main, which should not be committed to source. Update the initialization in main
so it reads the key at runtime from a dart-define or environment-based
configuration instead of embedding the secret directly, and replace the literal
with a clearly fake placeholder or fallback. Use the main entrypoint and the
apiKey parameter as the spots to update, and ensure no live credential remains
in the example source.
- Around line 21-28: The `MyAppState` listener is created in `initState()` but
never stored or cancelled, and its callback calls `setState` without checking
whether the widget is still mounted. Capture the
`FlutterSkin.onSkinChanged.listen(...)` result in a `StreamSubscription`, cancel
it in `dispose()`, and guard the callback with `mounted` before calling
`setState`; also add the needed `dart:async` import for the subscription type.

In `@example/lib/widgets/movie_card.dart`:
- Around line 100-102: The movie card description is hardcoded to a Lorem ipsum
placeholder, causing every card to show identical text. Update the Text in
movie_card.dart within the MovieCard widget to use the per-movie description
field from the Movie model (such as description or overview) instead of the
fixed string, and keep the existing layout/maxLines behavior intact; if no such
field exists, verify that the placeholder is intentionally meant for demo use.

In `@lib/remote/fskin_remote_config.dart`:
- Around line 53-55: The cached skin update path in FSkinRemoteConfig is
emitting ThemeData with a null color scheme when skin colors are missing, which
should be treated as no remote theme instead. Update the _cachedConfig branch in
the onSkinChanged flow to skip adding to _skinController unless
_cachedConfig!.skin?.colors is present, matching the behavior of toThemeData(),
so consumers can continue using their own fallback theme.

In `@lib/services/fskin_subscriber.dart`:
- Around line 9-16: The subscriber lifecycle in FskinSubscriber is not
idempotent because listen() only clears _disposed and can leave an existing
subscription, pending retry, or closed _client alive across re-initialization.
Update listen() to cancel any active stream subscription and pending retry Timer
(or guard reconnects with a generation/token check) before calling _connect(),
and make sure retries use the latest apiKey only. In dispose(), fully terminate
the subscriber state by canceling the subscription/retry and preventing any
later reconnect unless a new client is created; adjust the reconnect logic in
_connect() and the retry path around the affected block so stale callbacks
cannot fire after teardown.
- Around line 21-24: The SSE request in fskin_subscriber.dart is building the
stream URL by interpolating apiKey directly into the URI, which can break when
the key contains reserved characters. Update the request creation in the
subscriber logic to use Uri.https with queryParameters instead of Uri.parse
string concatenation, and reference the existing http.Request setup in the same
block so the encoded query is applied consistently. If the backend supports it,
consider moving the key out of the query string and into an auth header for
better handling of sensitive values.
- Around line 26-30: The retry counter is being reset too early in the request
flow inside the subscriber logic around the _client.send() response handling,
which causes failed HTTP responses to be treated as successful stream starts.
Move the _retryCount reset in the response callback so it only happens after the
response status has been validated as a valid SSE stream and _subscription is
created, and keep failed 401/404/500 responses on the retry path. Use the
existing _client.send(), _retryCount, and _subscription handling in
fskin_subscriber.dart to locate the fix.
- Around line 53-59: The retry logic in _retry currently computes seconds with
clamp(), which leaves it as a num and uses linear growth instead of exponential
backoff. Update the delay calculation in FskinSubscriber._retry to produce an
int, then change the retry schedule to exponential backoff (for example, based
on _retryCount) while keeping the existing 2–30 second bounds before passing it
to Duration(seconds: ...).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6724452d-23e4-48cd-8605-43979dd41ecc

📥 Commits

Reviewing files that changed from the base of the PR and between 058cf44 and 18d0c7b.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • example/android/app/src/main/AndroidManifest.xml
  • example/lib/main.dart
  • example/lib/widgets/movie_card.dart
  • lib/constants/fskin_constants.dart
  • lib/flutter_skin.dart
  • lib/remote/fskin_remote_config.dart
  • lib/services/fskin_subscriber.dart
  • lib/services/skin_service.dart
  • pubspec.yaml

Comment thread CHANGELOG.md Outdated
Comment thread example/lib/main.dart
Comment thread example/lib/main.dart
Comment thread example/lib/widgets/movie_card.dart
Comment thread lib/remote/fskin_remote_config.dart
Comment thread lib/services/fskin_subscriber.dart Outdated
Comment thread lib/services/fskin_subscriber.dart
Comment thread lib/services/fskin_subscriber.dart
Comment thread lib/services/fskin_subscriber.dart
@koukibadr koukibadr marked this pull request as draft June 24, 2026 11:54
@koukibadr koukibadr force-pushed the feat/live-skin/setup-sse-connection branch 5 times, most recently from c7fb921 to 3b452f6 Compare June 24, 2026 14:58
@koukibadr koukibadr marked this pull request as ready for review June 24, 2026 15:01

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/flutter_skin.dart (1)

17-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Start the SSE stream during initial initialization.

init() fetches the config but never calls _startStream(), so the persistent SSE connection is not opened until a later resume lifecycle event. Start the stream after FskinRemoteConfig.init(...) succeeds.

Proposed fix
     await FskinRemoteConfig.init(apiKey: apiKey);
+    _startStream();
 
     return _instance!;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/flutter_skin.dart` around lines 17 - 29, The init flow in
FlutterSkin.init() fetches remote config but never opens the SSE connection, so
the stream should be started immediately after FskinRemoteConfig.init(apiKey:
apiKey) completes successfully. Update FlutterSkin.init to invoke _startStream()
on the initialized _instance before returning, keeping the existing apiKey setup
and lifecycle observer registration intact so the stream is available during
first initialization rather than waiting for a resume event.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 7: The changelog entry for FlutterSkin.onSkinChanged has the wrong
emitted payload type; update the wording so it matches the public API contract
exposed by FlutterSkin.onSkinChanged as Stream<ThemeData>, not SkinTokens. Keep
the entry aligned with the actual stream behavior by replacing the type
description in the affected changelog line only.

In `@lib/flutter_skin.dart`:
- Around line 52-54: The resume handler in FlutterSkin is dropping the Future
returned by fetchConfig(), which can lead to unhandled async errors when the app
resumes. Update the AppLifecycleState.resumed path in the lifecycle override to
await FskinRemoteConfig.singleton.fetchConfig() inside a guarded async flow, or
attach catchError, and make sure _startStream() still runs from a finally path
so the stream restarts even if refresh fails.
- Around line 54-57: The lifecycle handling around _startStream and the
AppLifecycleState.paused/resumed branch is reusing a disposed FskinSubscriber,
which leaves _sse backed by a closed http.Client after resume. Update the resume
path to recreate _sse before calling _startStream, and ensure the logic in
listen/send uses a fresh subscriber/client instance rather than the disposed
one.

---

Outside diff comments:
In `@lib/flutter_skin.dart`:
- Around line 17-29: The init flow in FlutterSkin.init() fetches remote config
but never opens the SSE connection, so the stream should be started immediately
after FskinRemoteConfig.init(apiKey: apiKey) completes successfully. Update
FlutterSkin.init to invoke _startStream() on the initialized _instance before
returning, keeping the existing apiKey setup and lifecycle observer registration
intact so the stream is available during first initialization rather than
waiting for a resume event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c3d5e4f7-9656-4457-93d1-26e8c998173c

📥 Commits

Reviewing files that changed from the base of the PR and between 18d0c7b and 3b452f6.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • example/android/app/src/main/AndroidManifest.xml
  • example/lib/main.dart
  • example/lib/widgets/movie_card.dart
  • lib/constants/fskin_constants.dart
  • lib/flutter_skin.dart
  • lib/remote/fskin_remote_config.dart
  • lib/services/fskin_subscriber.dart
  • lib/services/skin_service.dart
  • pubspec.yaml

Comment thread CHANGELOG.md Outdated
Comment thread lib/flutter_skin.dart Outdated
Comment thread lib/flutter_skin.dart Outdated
@koukibadr koukibadr force-pushed the feat/live-skin/setup-sse-connection branch from 3b452f6 to 39e52cf Compare June 24, 2026 15:18
@koukibadr koukibadr merged commit eb963c6 into main Jun 24, 2026
2 checks passed
@koukibadr koukibadr deleted the feat/live-skin/setup-sse-connection branch June 24, 2026 15:20
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.

1 participant