Search code examples
flutterflutter-providerriverpod

ProviderScope on AlertDialog is not working properly


I have a widget that displays a counter in both a page and a pop-up window. However, increasing the value only affects the page's value in the present.

When I reopen the popup after closing it, it displays the new value.

I've been following this documentation: Riverpod-v2.

However, I made a quick adjustment to the CounterDisplay() widget. Both can be compared.

My Code Snippet:

final counterProvider = StateProvider((ref) => 0);

class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            CounterDisplay(
              count: count,
            ),
            ElevatedButton(
              onPressed: () {
                showDialog<void>(
                  context: context,
                  builder: (c) {
                    return ProviderScope(
                      parent: ProviderScope.containerOf(context),
                      child: AlertDialog(
                        content: CounterDisplay(
                          count: count,
                        ),
                        actions: [
                          IconButton(
                              onPressed: () {
                                ref.read(counterProvider.notifier).state++;
                              },
                              icon: const Icon(Icons.add))
                        ],
                      ),
                    );
                  },
                );
              },
              child: const Text('Show Dialog'),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            ref.read(counterProvider.notifier).state++;
          },
        ));
  }
}

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({super.key, required this.count});
  final int count;
  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

Code snippet from documentation:

final counterProvider = StateProvider((ref) => 0);

class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // We want to show a dialog with the count on a button press
    return Scaffold(
        body: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                showDialog<void>(
                  context: context,
                  builder: (c) {
                    return ProviderScope(
                      parent: ProviderScope.containerOf(context),
                      child: const AlertDialog(
                        content: CounterDisplay(),
                      ),
                    );
                  },
                );
              },
              child: const Text('Show Dialog'),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            ref.read(counterProvider.notifier).state++;
          },
        ));
  }
}

class CounterDisplay extends ConsumerWidget {
  const CounterDisplay({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

My CounterDisplay() widget shouldn't keep an eye on a provider, I say. It only needs to show the value for the passed parameter.

Additionally, I'm keeping an eye on the counter provider in the parent Home() widget and sending CounterDisplay() its value.

What makes this behavior?


Solution

  • In your case, the dialog will not be rebuilt because it is not in the widget tree immediately below Home. Dialogues and such are a different scope. That's why in the example from the documentation everything works as expected.

    Passing a parameter will work if it is a real subtree:

    final counterProvider = StateProvider((ref) => 0);
    
    class Home extends ConsumerWidget {
      const Home({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Scaffold(
            body: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                CounterDisplay(
                  count: count, // it's gonna be okay here!
                ),
                ElevatedButton(
                  onPressed: ...,
                  child: const Text('Show Dialog'),
                ),
              ],
            ),
            floatingActionButton: ...;
      }
    }
    
    class CounterDisplay extends StatelessWidget {
      const CounterDisplay({super.key, required this.count});
      final int count;
      @override
      Widget build(BuildContext context) {
        return Text('$count');
      }
    }
    

    Updated

    I've added a quick example to help you get started with ProviderScope.containerOf(context). You can run it directly in dartpad.dev and try removing/adding this:

    ProviderScope(
      parent: ProviderScope.containerOf(context),
      child: ...,
    )
    

    to see what's happening:

    import 'package:flutter/material.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    final counterProvider = StateProvider((ref) => 0);
    
    void main() => runApp(const ProviderScope(child: App()));
    
    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) => const MaterialApp(
            home: HomePage(title: 'Home Page'),
          );
    }
    
    class HomePage extends ConsumerWidget {
      const HomePage({super.key, required this.title});
    
      final String title;
    
      @override
      Widget build(BuildContext context, WidgetRef ref) => Scaffold(
            appBar: AppBar(title: Text(title)),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  const CounterDisplay(),
                  ProviderScope(
                    overrides: [
                      counterProvider.overrideWith((_) => 10),
                    ],
                    child: Builder(
                      builder: (context) => ElevatedButton(
                        onPressed: () {
                          showDialog<void>(
                            context: context,
                            builder: (c) => ProviderScope(
                              parent: ProviderScope.containerOf(context),
                              child: const AlertDialog(
                                content: CounterDisplay(),
                              ),
                            ),
                          );
                        },
                        child: const Text('Show Dialog'),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              child: const Icon(Icons.add),
              onPressed: () => ref.read(counterProvider.notifier).state++,
            ),
          );
    }
    
    class CounterDisplay extends ConsumerWidget {
      const CounterDisplay({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Text('$count');
      }
    }