gurleensethi / sailor

Easy page navigation and management in Flutter apps.
https://pub.dev/packages/sailor
MIT License
145 stars 24 forks source link

Wrong context in SailorRoute builder to get BlocProvider #18

Open zash80 opened 4 years ago

zash80 commented 4 years ago

I'm trying to use Sailor with flutter_bloc. When adding a SailorRoute like this:

    SailorRoute(
        name: '/details',
        builder: (context, args, params) {
          print('context... $context');
          return BlocProvider.value(
            value: BlocProvider.of<WeatherBloc>(context),
            child: WeatherDetailPage(
              masterWeather: params.param('masterWeather'),
            ),
          );
        },
        params: [
          SailorParam<Weather>(
            name: 'masterWeather',
          ),
        ],
      ),

Seeing following exception:

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
        BlocProvider.of() called with a context that does not contain a Bloc of type WeatherBloc.

        No ancestor could be found starting from the context that was passed to BlocProvider.of<WeatherBloc>().

        This can happen if:
        1. The context you used comes from a widget above the BlocProvider.
        2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.

        Good: BlocProvider<WeatherBloc>(builder: (context) => WeatherBloc())
        Bad: BlocProvider(builder: (context) => WeatherBloc()).

        The context used was: Builder(dirty)

The relevant error-causing widget was: 
  MaterialApp file:///home/mc/development/projects/flutter_tmp/flutter-bloc-library-v1-tutorial/lib/main.dart:19:12
When the exception was thrown, this was the stack: 
#0      BlocProvider.of (package:flutter_bloc/src/bloc_provider.dart:80:7)
#1      Routes.createRoutes.<anonymous closure> (package:flutter_bloc_v1_tutorial/main.dart:49:33)
#2      Sailor.generator.<anonymous closure>.<anonymous closure> (package:sailor/src/sailor.dart:521:37)
#3      new BaseTransitionPageRoute.<anonymous closure> (package:sailor/src/transitions/base_transition_page_route.dart:23:60)
#4      PageRouteBuilder.buildPage (package:flutter/src/widgets/pages.dart:109:12)
...
════════════════════════════════════════════════════════════════════════════════════════════════════

For details see https://github.com/zash80/flutter-bloc-library-v1-tutorial forked from https://github.com/ResoCoder/flutter-bloc-library-v1-tutorial replacing flutter navigation with Sailor navigation.

zash80 commented 4 years ago

Re-open, closed by accident...

gurleensethi commented 4 years ago

@zash80 Can you let me know where are you providing the WeatherBloc in you application?

zash80 commented 4 years ago

@gurleensethi , see here: https://github.com/zash80/flutter-bloc-library-v1-tutorial/blob/master/lib/main.dart - WeatherBloc is set up through BlocProvider as 'home' in MaterialApp.

felangel commented 4 years ago

Hi @zash80 I took a quick look and the problem is the SailorRoute gives you access to the new route's BuildContext instead of the existing BuildContext.

      SailorRoute(
        name: '/details',
        builder: (context, args, params) {
          print('context... $context'); // this context is already the new route context
          return BlocProvider.value(
            value: BlocProvider.of<WeatherBloc>(context),
            child: WeatherDetailPage(
              masterWeather: params.param('masterWeather'),
            ),
          );
        },
        params: [
          SailorParam<Weather>(
            name: 'masterWeather',
          ),
        ],
      ),

I believe in order to fix this you would need to modify Sailor to provide the BuildContext of the current widget before the route is pushed.

Hope that helps 👍

talamaska commented 4 years ago

To avoid such weird things, I do not Use provider inside router definition, rather I'm using args to pass the data to the page so in the end the definition look slike this

SailorRoute(
        name: '/rss-items/filtered',
        builder: (context, args, params) {
          return RssItemsFilteredPage(args: args);
        },
      ),

where args are

class RssItemsFilteredPageArgs extends BaseArguments {
  final String keywords;
  final Function onInit;
  RssItemsFilteredPageArgs({
    this.keywords,
    @required this.onInit,
  });
}

and the navigation call looks like this

onTap: () {
        Routes.sailor(
          '/rss-items/filtered',
          args: RssItemsFilteredPageArgs(
            keywords: item.keywords,
            onInit: (keywords) {
              StoreProvider.of<AppState>(context)
                  .dispatch(GetFilteredItemsAction(item));
              StoreProvider.of<AppState>(context)
                  .dispatch(SetSelectedFilterAction(item));
            },
          ),
        );
}
sterrenb commented 4 years ago

I am experiencing a similar issue when trying to instantiate a new Bloc in a SailorRoute.builder.

Here is a snippet of the relevant SailorRoute.

...
SailorRoute(
    name: myPage,
    builder: (BuildContext context, BaseArguments args, _) {
      print('DEBUG_1: creating BlocProvider for MyBloc');

      return BlocProvider<MyBloc>(
        create: (_) {
          print('DEBUG_2: creating MyBloc');
          return MyBloc();
        },
        child: MyPage(),
      );
    }),
...

When the route is pushed, the DEBUG_1 message is printed and the DEBUG_2 message is not, indicating that the BlocProvider.create method is not run.

With this snippet, in MyPage.build, BlocProvider.of(context) throws.

Ideally, the BlocProvider.create method should be run when the route is pushed, and BlocProvider.of(context) should return the instance of MyBloc.

Is there any other idiomatic approach to inject a Bloc using SailorRoute?

felangel commented 4 years ago

@sterrenburg have you tried setting lazy to false on the BlocProvider?

...
SailorRoute(
    name: myPage,
    builder: (BuildContext context, BaseArguments args, _) {
      print('DEBUG_1: creating BlocProvider for MyBloc');

      return BlocProvider<MyBloc>(
        lazy: false,
        create: (_) {
          print('DEBUG_2: creating MyBloc');
          return MyBloc();
        },
        child: MyPage(),
      );
    }),
...
sterrenb commented 4 years ago

@sterrenburg have you tried setting lazy to false on the BlocProvider?

...
SailorRoute(
    name: myPage,
    builder: (BuildContext context, BaseArguments args, _) {
      print('DEBUG_1: creating BlocProvider for MyBloc');

      return BlocProvider<MyBloc>(
        lazy: false,
        create: (_) {
          print('DEBUG_2: creating MyBloc');
          return MyBloc();
        },
        child: MyPage(),
      );
    }),
...

Thank you! Adding lazy: false seems to work, and correctly handles BlocProvider.of<MyBloc>(context) in MyPage.