felangel / bloc

A predictable state management library that helps implement the BLoC design pattern

Home Page:https://bloclibrary.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

UI doesn't react to state changes after using Navigator.push

Bruchess opened this issue · comments

Hello, I'm new in Flutter and I'm still learning state management with BLoC. I have a problem when I use Navigator.push. It's like the "context" of the BLoC is getting lost.

Let's say that I have declared these states:

@immutable 
abstract class MyStates {
  const MyStates();
}

@immutable class OtherState extends MyStates{
  const OtherState() ;
}

@immutable class WelcomePage extends MyStates{
  const WelcomePage() ;
}

And I have declared these events:

abstract class MyEvents {
  const MyEvents();
}

class UserThatHasToLogInDetected extends MyEvents {
  const UserThatHasToLogInDetected();
}

What I want is the users to be able to see what the app is about without logging in. However, if they try to use a special function of the app, then they will be requested to log in.

With this in mind, I wrote this BLoC:

class MyBloc extends Bloc<MyEvents, MyStates> {
  MyBloc() : super (const OtherState()) {
    
    on<UserThatHasToLogInDetected>((event, emit) {
      emit(const WelcomePage());
    });
}

Then in the main.dart file I have something like this:

void main()  {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider<MyBloc>(
      create: (context) => MyBloc(),
      child: MaterialApp(
        initialRoute: RouteGenerator.home,    //**Points to home.dart**
        onGenerateRoute: RouteGenerator.generateRoute... etc.
}

In the home.dart I have something like this:

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<BHappyBloc, BHappyState>(
      builder: (context, state) {
        if (state is WelcomePage) {
          return Welcome();  **//This makes the app to show the users the Welcome screen where they're required to log in**
        }  else {
          return const FirstPage();
        }
      },
    );
  }
}

At the beginning, the state is "OtherState" so, it will return FirstPage which have this:

class FirstPage extends StatefulWidget {
  const FirstPage({super.key});

  @override
  State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {

  @override
  Widget build(BuildContext context) {
    
    return Scaffold(
    body: Center(
            child: ElevatedButton(
                onPressed: () {
                  context.read<MyBloc>().add(const UserThatHasToLogInDetected());
                },
                child: const Text("Do something special in the app")));
   }
}

So, when I press this button in FirstPage the state changes to WelcomePage and the app returns Welcome() which is the screen where the user is required to log in.

Until here everything works fine. However, let's say that in the first page I add another button that when pressed, it executes:
Navigator.of(context).pushNamed(RouteGenerator.secondPage);

In that "secondPage" I have exactly the same code I have in FirstPage:

class SecondPage extends StatefulWidget {
  const SecondPage({super.key});

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {

  @override
  Widget build(BuildContext context) {
    
    return Scaffold(
    body: Center(
            child: ElevatedButton(
                onPressed: () {
                  context.read<MyBloc>().add(const UserThatHasToLogInDetected());
                },
                child: const Text("Do something special in the app")));
   }
}

And finally we arrive to my problem. When I press the button in the second page, through some prints I can see that the state is effectively changing to "WelcomePage", but the app doesn't return "Welcome" it stays showing SecondPage.

  1. What's happening?

I have tested that if I add a Navigator.pop() after context.read<MyBloc>().add(const UserThatHasToLogInDetected()); in the SecondPage then it returns to the FirstPage and then it responds to the state change returning "Welcome".

  1. How can I manage my navigation in a way that it doesn't lose the BLoC?

Of course in my real code I have much more states, events and lines of code. But I wanted to explain my problem briefly. Thanks in advance.

I have the same problem and I couldn't find the solution. 🤔🤔🤔

I've had the same problem for about 2 weeks and I still haven't resolved it...

This is not an issue with Bloc but with your design.

You have your bloc above MaterialApp, which is great -- all pages can access it.

Imagine routes/pages as "papers" and widgets as "drawings" on that paper. Based on what you described, your pages are Home Page and Second Page. On page Home there might be "drawings" welcome or first. That means when you are on Home Page, you can actually see bloc builder to build something.

But when you navigate to Second Page, again imagine a paper, you cannot see changes on Home Page, but only your current page, no matter if Home Page actually still exists or not. So if you want to create navigation based on states, you need to choose different approach, since your does not do that.

I would recommend i.e. creating states based on auth state like AuthInProgress, LoggedIn and UserNeedsToLogin etc. and then you have two options.

Use BlocListener under provider (you may need Builder above BlocListener to provide current context) and listen to changes there and there you can redirect to different pages (display different "papers"), or you can use something like GoRouter package which I recommend anyway and GoRouter object has redirect callback which you can implement like this maybe, you get the point...

MyStates? lastState;

FutureOr<String?> handleRedirect(
    BuildContext context,
    GoRouterState state,
  ) {
    final appState = context.read<MyBloc>().state;

    // Same route.
    final isSameState = appState == lastState;
    if (isSameState) {
      return null;
    }
    lastState = appState;

    // Auth in progress.
    if (appState is AuthInProgress) return null;

    if (appState is UserNeedsToLogin) {
      return '/login';
    }
    
    return null;

I hope you understand the concepts why your solution couldnt do that thing you wanted. Feel free to ask more questions for sure. :)

Thank you very much for your early answer. So, I think that in conclusion is not a good idea to think about screens as if they were states. I'm currently redesigning my app.