Riverpod - How to wrap a PreferredSizeWidget inside a Consumer

Viewed 1344

I have a DefaultTabController and I have a method that returns List<PreferredSizeWidget> which are the AppBar's of pages. I want them to watch a state in a ChangeNotifier and therefore I want to wrap them inside of a Consumer. When I try to do so I get an error like this:

"The argument type 'Widget' can't be assigned to the parameter type 'PreferredSizeWidget'."

How can I fix this?

Thanks and regards.

3 Answers

The error comes from the appBar's parameter of Scaffold requiring a PreferredSizeWidget. I can think of two solutions :

  • You can wrap your Consumer with a PreferredSize and use Size.fromHeight() as preferredSize. That is if the height is constant amongst your appbars.
  • You can avoid using the appBar parameter altogether by wrapping your Scaffold's body with an Expanded inside a Column, and making the Consumer its first child.

This is my implementation based on Mickael suggestion:

First I've createad a AppBarParams class to save the AppBar state

@freezed
class AppBarParams with _$AppBarParams {
  const factory AppBarParams({
    required String title,
    required List<Widget> actions,
  }) = _AppBarParams;
}

Then I've created a StateProvider in global providers file like this:

final appBarParamsProvider = StateProvider<AppBarParams>((ref) {
  return AppBarParams(title: "Default title", actions: []);
});

and attached it in the main app with a Consumer:

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: "App Title",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SafeArea(
        child: Scaffold(
          appBar: PreferredSize(
            preferredSize: Size.fromHeight(kToolbarHeight),
            child: Consumer(
              builder: (context, watch, child) {
                final appBarParams = watch(appBarParamsProvider).state;
                return AppBar(
                  title: Text(appBarParams.title),
                  actions: appBarParams.actions
                );
            })
          ),
          body: ... your body widget
        )
      )
    )
  }
}

Then you just need to edit the provider state to have the AppBar update accordingly, to update the AppBar when the user switches between pages, I've created this mixin:

mixin AppBarHandler {

  void updateAppBarParams(
    BuildContext context, {
    required String title,
    List<Widget> Function()? actions
  }) {
    WidgetsBinding.instance!.addPostFrameCallback((_) async {
      context.read(appBarParamsProvider).state = context
        .read(appBarParamsProvider).state
        .copyWith(
           title: title, 
           actions: actions != null ? actions() : []
        );
    });
  }
}

And in every main screen view which has to change the title or the actions I did this:

class Page1 extends HookWidget with AppBarHandler {

  const Page1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    updateAppBarParams(context, 
      title: "Page 1 title",
      actions: () => [
        IconButton(icon: const Icon(Icons.refresh), onPressed: () {
          //a custom action for Page1
          context.read(provider.notifier).updateEntries();
        })
      ]
    );
    ... your screen widget
  }
}

I would recommend again wrapping the whole AppBar in Consumer. You should wrap widgets inside appbar fields (like leading, title etc) with consumer. It is mentioned in Flutter's documentation to keep the Consumer at the lowest level possible. If you want them to appear conditionally, you can follow the below practice:

AppBar(actions:[
    Consumer<Provider>(builder: (context,value,child) => value.toShow() ? 
         MyWidget() : const SizedBox()
                       )
                ]
       );
Related