Skip to content
cccskills
FreePublished on GitHub

riverpod-stream-provider-auth-transition-oscillation

Fix Flutter home feed / main screen stuck on loading spinner after login when using Riverpod StreamProvider that watches GoRouter location changes. Use when: (1) Screen shows BrandedLoadingIndicator or CircularProgressIndicator permanently after successful auth redirect, (2) Widget watches a route-type-gating provider that returns AsyncValue.loading() intermittently, (3) Logs show route location oscillating between stale and current paths during post-login transition (e.g., /welcome/* after /home/0), (4) Provider chain has double-gate: widget gates on pageContext AND data provider also gates on pageContext. Distinct from riverpod-infinite-rebuild-loop (rapid rebuilds) — this causes permanent loading state, not infinite rebuilds.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md7.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Riverpod StreamProvider Auth Transition Oscillation

Problem

After successful login and redirect, the main screen (home feed, dashboard, etc.) is permanently stuck on a loading indicator. The auth redirect works correctly (URL shows /home/0), but the screen never renders data. This is NOT a rapid rebuild issue — the widget builds a few times then settles on a loading state.

Context / Trigger Conditions

Symptoms:

  • Screen stuck on loading spinner after successful login redirect
  • URL bar / GoRouter shows correct path (e.g., /home/0)
  • Data provider (e.g., homeFeedProvider) has data if checked directly
  • No error messages — just permanent loading
  • May show brief flash of content before reverting to loading

Architecture that triggers this:

  1. A StreamProvider that watches router.routerDelegate for location changes
  2. This stream parses routes into a RouteContext with a type field (home, explore, etc.)
  3. Downstream providers gate on routeContext.type == RouteType.home and return AsyncValue.loading() when the type doesn't match
  4. The widget watches the downstream provider and shows loading indicator

The oscillation pattern:

Auth state changes → Router redirects to /home/0
  → routerDelegate emits /home/0 ✓
  → routerDelegate emits /welcome/login (stale!) ✗
  → routerDelegate emits /home/0 ✓
  → routerDelegate emits /welcome/* (stale!) ✗
  ...oscillates for several frames

Why it happens: GoRouter's routerDelegate listener fires for EVERY location change during transitions, including intermediate/stale states. During post-login, the router processes multiple pending navigations (pop welcome screen, push home screen) and the delegate emits each intermediate state. A sync StreamController propagates these instantly.

Log signature:

CTX derive: type=RouteType.home npub=null index=0
CTX derive: type=RouteType.welcome npub=null index=null   ← stale!
CTX derive: type=RouteType.home npub=null index=0

Root Cause Analysis

The issue is a double gate on an oscillating stream:

routerDelegate listener
  ↓ (emits every location change)
StreamProvider<RouteContext>  ← oscillates between /home and /welcome
  ↓
videosForHomeRouteProvider   ← returns loading() when type != home  [GATE 1]
  ↓
HomeScreenRouter.build()     ← watches pageContext for type check   [GATE 2]

When the stream oscillates, both gates open and close rapidly. The widget ends up rendering the loading state from whichever emission came last in the settling period.

Solution

Pattern: "I Know Who I Am" — Bypass Route-Type Gating

When a widget knows its own context (it's only mounted at a specific route), it doesn't need to gate on a route-type stream. It can read route info synchronously.

Step 1: Read URL index synchronously from GoRouter

// BEFORE: Watching oscillating stream
final pageContext = ref.watch(pageContextProvider);
return pageContext.when(
  data: (ctx) {
    if (ctx.type != RouteType.home) return loading();
    // ...
  },
  loading: () => loading(),
  error: (e, s) => error(),
);

// AFTER: Read synchronously — this widget IS the home screen
final router = ref.read(goRouterProvider);
final location = router.routeInformationProvider.value.uri.toString();
final segments = location.split('/').where((s) => s.isNotEmpty).toList();
int urlIndex = 0;
if (segments.length > 1 && segments[0] == 'home') {
  urlIndex = int.tryParse(segments[1]) ?? 0;
}

Step 2: Watch the data provider directly

// BEFORE: Watching intermediate provider that gates on route type
final videosAsync = ref.watch(videosForHomeRouteProvider);

// AFTER: Watch the data provider directly — no route-type gate needed
final videosAsync = ref.watch(homeFeedProvider);

Step 3: Remove unused intermediate provider imports

Clean up imports for any intermediate route-gating providers that are no longer used.

When NOT to Apply This Fix

  • When the widget genuinely needs to render different content based on route type (e.g., a shared shell that shows different feeds)
  • When the widget is mounted at multiple routes and needs to switch behavior
  • If the issue is actually rapid rebuilds (use riverpod-infinite-rebuild-loop instead)

Verification

After the fix:

  1. Login → redirect to home → home feed loads immediately (no permanent spinner)
  2. No RAPID REBUILD warnings (this fix doesn't cause those)
  3. Swipe through feed works normally
  4. Pull-to-refresh works
  5. Navigate away and back — feed still loads
  6. Test with ref.watch(homeFeedProvider) in initState to confirm data arrives

Example: Complete Fix (HomeScreenRouter)

Before (stuck on loading):

@override
Widget build(BuildContext context) {
  final pageContext = ref.watch(pageContextProvider);
  return buildAsyncUI(
    pageContext,
    onData: (ctx) {
      if (ctx.type != RouteType.home) {
        return const Center(child: BrandedLoadingIndicator(size: 80));
      }
      final videosAsync = ref.watch(videosForHomeRouteProvider);
      return buildAsyncUI(videosAsync, ...);
    },
  );
}

After (loads correctly):

@override
Widget build(BuildContext context) {
  // Read URL synchronously — HomeScreenRouter is only at /home/:index
  final router = ref.read(goRouterProvider);
  final location = router.routeInformationProvider.value.uri.toString();
  final segments = location.split('/').where((s) => s.isNotEmpty).toList();
  int urlIndex = 0;
  if (segments.length > 1 && segments[0] == 'home') {
    urlIndex = int.tryParse(segments[1]) ?? 0;
  }

  // Watch data directly — no route-type gate needed
  final videosAsync = ref.watch(homeFeedProvider);
  return buildAsyncUI(videosAsync, onData: (state) { ... });
}

Notes

  • Distinct from rebuild loops: riverpod-infinite-rebuild-loop covers rapid rebuilds (50+ per second). This issue causes 3-8 rebuilds that settle on a LOADING state.
  • Related to auth timing: Often co-occurs with synchronous router redirect needing data that isn't yet available. See companion fix: pre-fetch data before setting auth state so redirects have what they need.
  • StreamProvider vs read: The oscillation only affects StreamProvider watching reactive router state. ref.read() of GoRouter's current location is stable.
  • GoRouter's routerDelegate: This listener fires for every intermediate navigation state. It's reliable for final states but oscillates during multi-step transitions (login → pop welcome → push home).
  • Debug technique: Add print('CTX derive: type=${ctx.type}') to the StreamProvider to see the oscillation pattern.

Related Skills

  • riverpod-infinite-rebuild-loop — rapid rebuilds from watch/listener issues
  • flutter-pageview-url-routing-reorder-loop — infinite loop from item reorder tracking
  • flutter-startup-network-blocking — blocking network ops during startup

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Fix ArgoCD ExternalSecret deployment failing with "namespace X is not permitted in project Y". Use when: (1) ExternalSecret shows OutOfSync in ArgoCD but won't sync, (2) ArgoCD application status shows "namespace X is not permitted in project 'infrastructure'", (3) ExternalSecret targets a namespace managed by a different ArgoCD project, (4) Using apps-of-apps pattern with separate infrastructure and application projects.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

Art direction for any content — reads text, PDF, Word, HTML, PPT, then proposes 2-3 creative directions with photography style, mood, and visual language. After selection, generates AI image prompts and visual briefs section-by-section. Use when the user shares content and needs visual direction, image sourcing, or creative direction for any material.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

Fix "Null check operator used on a null value" errors when an object is set to null during an async await. Use when: (1) Object reference is nullified while awaiting, (2) Code accesses object with ! after await returns, (3) Cancel/dispose operations run concurrently with async operations on same object. Solution: capture local reference before await.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

Add custom metadata headers (x-amz-meta-*) to AWS v4 signed requests for GCS S3-compatible API. Use when: (1) Adding custom metadata to GCS uploads via S3 API, (2) Getting signature mismatch errors after adding new headers, (3) x-amz-meta-* headers being ignored or causing 403 errors. Custom headers MUST be included in canonical headers and signed headers list.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

Fix password/secret authentication failures caused by trailing newlines when creating Google Cloud secrets (or similar) with bash here-strings. Use when: (1) Password authentication fails with correct password, (2) Secret created with `<<< "value"` syntax, (3) Error like "password authentication failed" or "invalid token" despite correct value. Bash here-strings (`<<<`) add a trailing newline that corrupts secrets.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

Fix silent video/media processing failures caused by URL extraction code that filters on file extensions (.mp4, .webm, .webp). Use when: (1) Media moderation, transcoding, or analysis silently skips files from Blossom or content-addressed storage servers, (2) URL extraction from Nostr event tags (imeta, r tags) drops URLs without recognized extensions, (3) CDN fallback URLs append .mp4 but the actual server uses extensionless content-addressed paths like /{sha256}. Common in Nostr video events (kind 34236) where different clients use different URL formats.

The English summary is being prepared. Showing the original description.

divinevideo/divine-mobile265Updated Oct 23, 2026

See all skills by divinevideo

Report a problem with this skill