Skip to content
cccskills
FreePublished on GitHub

riverpod-infinite-rebuild-loop

Debug and fix infinite widget rebuild loops in Flutter apps using Riverpod state management. Use when: (1) Logs show "RAPID REBUILD" warnings or 50+ rebuilds in seconds, (2) UI becomes unresponsive or shows same content repeatedly, (3) Bug only affects slower devices or poor network conditions, (4) Provider watching creates circular dependencies with router/URL state. Covers ref.watch() overuse, watch+listener redundancy, and transitive dependency chains.

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

See how to install

Files included (1)

  • SKILL.md7.1 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Riverpod Infinite Rebuild Loop

Problem

Flutter widgets using Riverpod enter an infinite rebuild loop, causing:

  • 50+ widget rebuilds in seconds
  • UI becomes unresponsive
  • Same content rendered repeatedly
  • Bug manifests primarily on slower devices or poor network connections

Context / Trigger Conditions

Symptoms in logs:

⚠️ RAPID REBUILD #54! Only 15ms since last build
⚠️ RAPID REBUILD DETECTED! Only 6ms since last build

User-reported symptoms:

  • "Endless scroll loop" - same video/content keeps appearing
  • "App freezes" or becomes unresponsive
  • "Works on my phone but not on older devices"

Trigger scenarios:

  1. Multiple async providers completing at staggered times
  2. Provider that watches route/URL state AND updates URL in build
  3. Using ref.watch() AND adding manual listeners to same provider
  4. Transitive watches: Widget watches A, A watches B, Widget also watches B

Root Cause Analysis

Pattern 1: URL Update Feedback Loop

// BAD: Creates infinite loop
Widget build(BuildContext context) {
  final pageContext = ref.watch(pageContextProvider);  // Watches URL
  final videos = ref.watch(videosProvider);

  // Detect video moved position and "silently" update URL
  if (currentVideoIndex != urlIndex) {
    context.go('/home/$currentVideoIndex');  // URL change triggers rebuild!
  }
}

Loop: Videos reorder → URL updated → pageContextProvider emits → rebuild → videos may reorder again → repeat

Pattern 2: Watch + Listener Redundancy

// BAD: Double-subscribing causes double rebuilds
final cache = ref.watch(cacheProvider);  // Watch triggers rebuild
cache.addListener(onCacheChanged);        // Listener ALSO triggers action

Pattern 3: Transitive Watch Dependencies

// BAD: Double-watching same source
Widget build() {
  ref.watch(pageContextProvider);           // Watch #1
  ref.watch(derivedProvider);               // derivedProvider ALSO watches pageContextProvider!
}

Pattern 4: Staggered Async Provider Loading

// PROBLEMATIC on slow devices: Each watch triggers rebuild when provider completes
final a = ref.watch(asyncProviderA);  // Completes at T=100ms → rebuild
final b = ref.watch(asyncProviderB);  // Completes at T=200ms → rebuild
final c = ref.watch(asyncProviderC);  // Completes at T=350ms → rebuild
final d = ref.watch(asyncProviderD);  // Completes at T=500ms → rebuild
// On fast devices: all complete ~simultaneously, 1-2 rebuilds
// On slow devices: staggered completion, 4+ rebuilds

Solution

Step 1: Audit ref.watch() Usage

For each ref.watch() in build methods, ask:

  • Does this provider change frequently?
  • Do I need to REBUILD when it changes, or just REACT?
  • Am I also manually listening to this provider?

Riverpod Methods:

MethodBehavior
ref.watch()Subscribe + REBUILD on change
ref.read()Read once, NO rebuild
ref.listen()Subscribe + callback, NO rebuild

Step 2: Convert Unnecessary Watches

// BEFORE: Rebuilds on every change
final videoService = ref.watch(videoServiceProvider);

// AFTER: Read once, use listener for reactions
final videoService = ref.read(videoServiceProvider);
ref.listen(videoServiceProvider, (prev, next) {
  // React to changes without rebuilding
  if (next.hasNewVideos) refreshUI();
});

Step 3: Remove Watch + Listener Redundancy

// BEFORE: Double-subscription
final cache = ref.watch(cacheProvider);
cache.addListener(onCacheChanged);

// AFTER: Choose one approach
// Option A: Just watch (if rebuild is needed)
final cache = ref.watch(cacheProvider);

// Option B: Read + listen (if rebuild not needed)
final cache = ref.read(cacheProvider);
ref.listen(cacheProvider, (_, __) => onCacheChanged());

Step 4: Break URL Update Loops

// BEFORE: URL update in build causes loop
if (currentVideoIndex != urlIndex) {
  context.go('/home/$currentVideoIndex');
}

// AFTER: Don't update URL on content reorder
// Just track position with PageController, not URL
// OR use content ID in URL instead of index: /home/video/abc123

Step 5: Batch Initial Load

// BEFORE: Watch each async provider (N rebuilds on slow devices)
final a = ref.watch(asyncA);
final b = ref.watch(asyncB);

// AFTER: Create combined provider that waits for all
@riverpod
Future<CombinedState> combinedState(Ref ref) async {
  final a = await ref.watch(asyncA.future);
  final b = await ref.watch(asyncB.future);
  return CombinedState(a, b);
}
// Widget watches only the combined provider (1 rebuild)

Verification

After fixes:

  1. Run app on slow device or use network throttling
  2. Check logs for rebuild warnings - should see ≤5 rebuilds on startup
  3. Scroll/navigate and verify UI remains responsive
  4. No "RAPID REBUILD" warnings in logs

Example: Full Fix

Before (problematic):

class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    final pageContext = ref.watch(pageContextProvider);
    final videos = ref.watch(videosProvider);  // Also watches pageContextProvider internally!

    // URL update loop
    if (videos.currentIndex != pageContext.index) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        context.go('/home/${videos.currentIndex}');
      });
    }

    return PageView(...);
  }
}

After (fixed):

class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    // Only watch videos, read page context
    final pageContext = ref.read(pageContextProvider).requireValue;
    final videos = ref.watch(videosProvider);

    // Don't update URL on reorder - track with PageController only
    // URL only changes on explicit user navigation

    return PageView(...);
  }
}

Notes

  • This bug is timing-dependent - may not reproduce on fast devices
  • Test on physical devices with network throttling to catch issues
  • Add rebuild detection logging during development:
    static int _buildCount = 0;
    static DateTime? _lastBuild;
    
    @override
    Widget build(BuildContext context) {
      final now = DateTime.now();
      if (_lastBuild != null && now.difference(_lastBuild!).inMilliseconds < 100) {
        Log.warning('RAPID REBUILD #${++_buildCount}!');
      }
      _lastBuild = now;
      // ...
    }
    

References

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 22, 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 22, 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 22, 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 22, 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 22, 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 22, 2026

See all skills by divinevideo

Report a problem with this skill