felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.84k stars 3.4k forks source link

BlocConsumer not listening to any state change #2020

Closed ctrlji closed 3 years ago

ctrlji commented 3 years ago

Describe the bug Except the email verification screen of my flutter project, everything works fine. The BLOC for email verification screen & body does not emit changes in the cubit file when running code. I use this logic component methodology for other authentication screens like login and register screens and they work perfectly. But for some reasons I'm not sure of the email verification screen does not pick up on any state changes.

Steps to reproduce Here is the link to the full flutter project's public repository: https://github.com/abolajimustapha/apartmentplus/tree/main/

  1. Examine the lib/presentation/screens/verify folder
  2. Also look at the lib/logic/verify for the BLOC of the verify screen
  3. You can also run the project in a local setup to see how verify_cubit.dart file won't emit state changes to the verify screen

Scripts verify_mail_screen.dart : screen that gets navigated to from login or register screen on user click event

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_wordpress/schemas/user.dart';
import 'body.dart';

class VerifyMail extends StatelessWidget {
  final User user;

  VerifyMail({this.user});
  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
        value: SystemUiOverlayStyle(
          statusBarColor: Theme.of(context).primaryColor,
        ),
        child: Scaffold(
          body: Body(
            user: user,
          ),
        ));
  }
}

body.dart: a separate file that holds the main UI components the VerifyMail screen, where the magic of BlocConsumer listening is supposed to happen(but sadly does not).

import 'package:flutter/material.dart';
import 'package:ApartmentPlus/presentation/widgets/forms/verify_mail_form.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_wordpress/schemas/user.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ApartmentPlus/logic/verify/verify_cubit.dart';
import 'package:ApartmentPlus/data/repositories/wp_authentication/email_verification.dart';
import 'package:ApartmentPlus/presentation/widgets/alerts/loading_dialog.dart';

class Body extends StatefulWidget {
  final User user;

  Body({this.user});

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  final GlobalKey<FormBuilderState> _fbKey = new GlobalKey<FormBuilderState>();
  VerificationRepository verificationRepo = VerificationRepository.instance;

  @override
  Widget build(context) {
    ThemeData _theme = Theme.of(context);
    GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
    return BlocProvider(
      create: (ctx) => VerifyCubit(verificationRepo),
      child: Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(
          backgroundColor: _theme.scaffoldBackgroundColor,
          automaticallyImplyLeading: false,
          elevation: 0,
          leading: IconButton(
            icon: Icon(Icons.arrow_back_ios),
            onPressed: () {
              if (Navigator.of(context).canPop()) {
                Navigator.of(context).pop();
              }
            },
          ),
        ),
        body: BlocConsumer<VerifyCubit, VerifyState>(
          listener: (context, state) {
            if (state is Error) {
              WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
                Navigator.pop(LoadingDialog.instance.dialogContext);
              });
              Scaffold.of(context).showSnackBar(SnackBar(
                behavior: SnackBarBehavior.floating,
                backgroundColor: _theme.errorColor,
                duration: Duration(seconds: 5),
                elevation: 12,
                content:
                    Row(mainAxisAlignment: MainAxisAlignment.start, children: [
                  Icon(
                    Icons.warning,
                    color: _theme.accentColor,
                  ),
                  SizedBox(
                    width: 10,
                  ),
                  Flexible(
                    child: Text(state.message,
                        style: TextStyle(
                          fontSize: 14,
                        )),
                  )
                ]),
              ));
            }
            if (state is Loading) {
              LoadingDialog.instance
                  .showAlertDialog(context, "${state.message}");
            }
            if (state is VerifyEmailSent) {
              WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
                Navigator.pop(LoadingDialog.instance.dialogContext);
              });
              VerifyCubit(verificationRepo).startCountdown(state.time);
              Scaffold.of(context).showSnackBar(SnackBar(
                behavior: SnackBarBehavior.floating,
                backgroundColor: _theme.hintColor,
                duration: Duration(seconds: 5),
                elevation: 12,
                content:
                    Row(mainAxisAlignment: MainAxisAlignment.start, children: [
                  Icon(
                    Icons.warning,
                    color: _theme.accentColor,
                  ),
                  SizedBox(
                    width: 10,
                  ),
                  Flexible(
                    child: Text(state.scaffoldMessage,
                        style: TextStyle(
                          fontSize: 14,
                        )),
                  )
                ]),
              ));
            }
          },
          builder: (context, state) {
            if (state is VerifyInitial) {
              VerifyCubit(verificationRepo).sendEmail(this.widget.user);
              return Center(child: CircularProgressIndicator());
            } else {
              return Container(
                padding: EdgeInsets.all(15.0),
                child: Column(
                  children: <Widget>[
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: <Widget>[
                          Container(
                            padding: EdgeInsets.symmetric(vertical: 30.0),
                            child: Text(
                              "Verify Your Email",
                              style: TextStyle(fontSize: 30.0),
                            ),
                          ),
                          Text(
                            //${user.email}
                            "Check your Email, We've sent an OTP to ${this.widget.user.email}",
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 14.0),
                          ),
                          SizedBox(
                            height: 30.0,
                          ),
                          //the form
                          VerifyForm(fbkey: _fbKey, context: context),
                          SizedBox(
                            height: 30.0,
                          ),
                          Wrap(
                            children: <Widget>[
                              Text(
                                "Didn't receive the Email?",
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              SizedBox(
                                width: 10.0,
                              ),
                              (state is CountdownStarted)
                                  ? Container(
                                      child: Text("${state.current}"),
                                    )
                                  : GestureDetector(
                                      onTap: () {
                                        VerifyCubit(verificationRepo)
                                            .sendEmail(this.widget.user);
                                      },
                                      child: Text(
                                        "Resend Code",
                                        style: TextStyle(
                                          fontWeight: FontWeight.bold,
                                          color: _theme.primaryColor,
                                        ),
                                      ),
                                    ),
                            ],
                          )
                        ],
                      ),
                    ),
                    Container(
                      width: MediaQuery.of(context).size.width,
                      height: 45.0,
                      child: FlatButton(
                        color: _theme.primaryColor,
                        child: Text(
                          "VERIFY",
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.white,
                          ),
                        ),
                        onPressed: () {
                          VerifyCubit(verificationRepo).triggerLoading();
                        },
                      ),
                    )
                  ],
                ),
              );
            }
          },
        ),
      ),
    );
  }
}

In the Cubit Folder

verify_state.dart : holds classes for various states on function trigger

part of 'verify_cubit.dart';

@immutable
abstract class VerifyState extends Equatable {
  const VerifyState();

  @override
  List<Object> get props => [];
}

class VerifyInitial extends VerifyState {
  const VerifyInitial();
}

class Loading extends VerifyState {
  final message;
  Loading({this.message});
}

class VerifyEmailSent extends VerifyState {
  final time;
  final scaffoldMessage;
  VerifyEmailSent({this.time, this.scaffoldMessage = "Check Email for token"});
}

class CountdownCompleted extends VerifyState {
  CountdownCompleted();
}

class CountdownStarted extends VerifyState {
  final current;
  CountdownStarted(this.current);
}

class Error extends VerifyState {
  final message;
  final code;
  Error(this.message, this.code);
}

class VerificationSuccess extends VerifyState {
  final route;
  VerificationSuccess({this.route});
}

verify_cubit.dart : cubit file for the verify screen UI triggers functions but doesn't emit states

import 'package:bloc/bloc.dart';
import 'package:flutter_wordpress/schemas/user.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:ApartmentPlus/data/repositories/wp_authentication/email_verification.dart';
import 'package:ApartmentPlus/presentation/routes/router.gr.dart' as router;
import 'package:flutter_wordpress/schemas/wordpress_error.dart';
import 'dart:convert';
import 'package:quiver/async.dart';

part 'verify_state.dart';

class VerifyCubit extends Cubit<VerifyState> {
  final VerificationRepository _verificationRepo;
  VerifyCubit(this._verificationRepo) : super(VerifyInitial());

  Future<void> sendEmail(User user) async {
    try {
      emit(new Loading(message: "Generating OTP..."));
      await _verificationRepo.generateToken(user);
      emit(VerifyEmailSent(time: 100));
    } catch (e) {
      print(e);
      if (e.runtimeType == WordPressError) {
        var jsonedError = json.decode(e.message);
        emit(Error("${jsonedError['message']}", jsonedError['code']));
      } else {
        emit(Error("$e", 22));
      }
    }
  }

  Future<void> verifyEmail(User user, String userToken) async {
    try {
      emit(Loading(message: ""));
      await _verificationRepo.verifyUser(user, userToken);
      emit(VerificationSuccess(route: router.Router.login));
    } catch (e) {
      print(e);
      if (e.runtimeType == WordPressError) {
        var jsonedError = json.decode(e.message);
        emit(Error("${jsonedError['message']}", jsonedError['code']));
      } else {
        emit(Error("$e", 22));
      }
    }
  }

  Future<void> startCountdown(int time) async {
    emit(CountdownStarted(time));
    int _start = time;
    int _current = time;

    CountdownTimer countDownTimer = new CountdownTimer(
      new Duration(seconds: _start),
      new Duration(seconds: 1),
    );

    var sub = countDownTimer.listen(null);
    sub.onData((duration) {
      _current = _start - duration.elapsed.inSeconds;
      emit(CountdownStarted(_current));
    });

    sub.onDone(() {
      print("Done");
      sub.cancel();
      emit(CountdownCompleted());
    });
  }

  triggerLoading() {
    print("hi");
    print(Loading == VerifyInitial);
    emit(new Loading(message: "why now?"));
  }
}

Logs No error logs of any sort even on flutter analyze

Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 1.22.4, on Microsoft Windows [Version 10.0.18362.175], locale en-US)

[√] Android toolchain - develop for Android devices (Android SDK version 30.0.2) [√] Android Studio (version 4.0) [√] VS Code (version 1.51.1) [√] Connected device (1 available)

felangel commented 3 years ago

Hi @abolajimustapha 👋 Thanks for opening an issue!

I took a look and it looks like the problem is at https://github.com/abolajimustapha/apartmentplus/blob/eeb3b93b1be976c9a2252219e909372a31170893/lib/presentation/screens/authentication/verify/body.dart#L108.

First, you should make sure that when you are interacting with the cubit you are interacting with the same instance that is created. In this case you are creating an entirely different cubit and calling sendEmail.

Second, you should never do this within the builder. The builder (and build method) should have no side-effects. Instead I would recommend adding this event to the cubit when it is created.

BlocProvider(
    create: (ctx) => VerifyCubit(verificationRepo)..sendEmail(this.widget.user),
    child: Scaffold(...),
)

This will call sendEmail as soon as the widget is mounted.

Hope that helps 👍

ctrlji commented 3 years ago

Hi Felix, thanks a lot. the issue has been resolved.

I also found out that to effect states emitted from blocbuilder the context.watch<Cubit>().function(), in my case, context.watch<VerifyCubit>.sendEmail(user) also works fine keeping the same instance of cubit across the widget tree. For the sake of developers with related issue, this video series on flutter bloc really helps a lot: https://www.youtube.com/watch?v=w6XWjpBK4W8&list=PLptHs0ZDJKt_T-oNj_6Q98v-tBnVf-S_o

Once again thanks!

2shrestha22 commented 3 years ago

Hi @abolajimustapha Thanks for opening an issue!

I took a look and it looks like the problem is at https://github.com/abolajimustapha/apartmentplus/blob/eeb3b93b1be976c9a2252219e909372a31170893/lib/presentation/screens/authentication/verify/body.dart#L108.

First, you should make sure that when you are interacting with the cubit you are interacting with the same instance that is created. In this case you are creating an entirely different cubit and calling sendEmail.

Second, you should never do this within the builder. The builder (and build method) should have no side-effects. Instead I would recommend adding this event to the cubit when it is created.

BlocProvider(
    create: (ctx) => VerifyCubit(verificationRepo)..sendEmail(this.widget.user),
    child: Scaffold(...),
)

This will call sendEmail as soon as the widget is mounted.

Hope that helps

Sorry for replaying in a closed issue and I am going to as a noob question. How can we make sure to use same instance of bloc while we need to listen for state change from another bloc? I faced problem while listening to another bloc. After reading your comment I immediately made bloc a singleton and it worked. Is this only way and we need to use DI if we need to listen the state of a bloc from other bloc? or there is another workaround?

seyezy commented 2 years ago

Am also having similar issue too. I have a Bloc that is yielding a state but the BlocConsumer failed to listen to the state change. Can anybody tell me what am doing wrong.

STATE CLASS:

part of 'people_bloc.dart';

@immutable abstract class PeopleState {}

class PeopleInitial extends PeopleState {}

class PeopleLoadingState extends PeopleState { @override List<Object?> get props => []; }

class SearchLoadingState extends PeopleState { @override List<Object?> get props => []; }

class NoReturnState extends PeopleState { final String? message;

NoReturnState({this.message});

@override List get props => []; }

class PeopleErrorState extends PeopleState { final int? status; final String? message;

PeopleErrorState({this.status, this.message});

@override List get props => []; }

class GetPeopleState extends PeopleState { final SearchPeopleResponse getPeopleResponse;

GetPeopleState({required this.getPeopleResponse});

@override List<Object?> get props => [getPeopleResponse]; }

class GetSearchResultState extends PeopleState { final SearchPeopleResponse getPeopleResponse;

GetSearchResultState({required this.getPeopleResponse});

@override List<Object?> get props => [getPeopleResponse]; }

EVENT CLASS:

part of 'people_bloc.dart';

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

class GetPeopleEvent extends PeopleEvent { final String term; GetPeopleEvent({required this.term}); @override List get props => [term]; }

class SearchPeopleEvent extends PeopleEvent { final String term; SearchPeopleEvent({required this.term}); @override List get props => [term]; }

BlOC:

class PeopleBloc extends Bloc<PeopleEvent, PeopleState> { final client = RestClient(Dio(BaseOptions(contentType: "application/json"))); PeopleBloc() : super(PeopleInitial());

@override void onTransition(Transition<PeopleEvent, PeopleState> transition) { super.onTransition(transition); print(transition); }

List people = []; @override Stream mapEventToState( PeopleEvent event, ) async* { if (event is SearchPeopleEvent) { yield SearchLoadingState(); //yield PeopleLoadingState(); try { var token = await getToken(); //print(token); SearchPeopleResponse responseData = await client.getPeople(token!, event.term);

    if (responseData.status == 200) {
      yield GetSearchResultState(getPeopleResponse: responseData);
    } else {
      yield PeopleErrorState(message: responseData.msg);
      print("loadingE");
    }
  } catch (e) {
    //print("error msg here ${e.toString()}");
    PeopleErrorState(message: e.toString());
  }
}
if (event is GetPeopleEvent) {
  try {
    yield PeopleLoadingState();
    var token = await getToken();
    //print(token);
    SearchPeopleResponse responseData =
        await client.getPeople(token!, event.term);

    if (responseData.status == 200) {
      if (responseData.data.length == 0) {
        yield NoReturnState(message: 'No Result');
      } else {
        yield GetPeopleState(getPeopleResponse: responseData);
      }
    } else if (responseData.status == 401) {
      yield NoReturnState(message: responseData.msg); //no result
    } else {
      yield PeopleErrorState(message: responseData.msg);
    }
  } catch (e) {
    //print("error msg here ${e.toString()}");
    PeopleErrorState(message: e.toString());
  }
}

}

MY STATEFUL CLASS:

class SearchPeopleScreen extends StatefulWidget { const SearchPeopleScreen({Key? key}) : super(key: key); static String routeName = 'search_people';

@override _SearchPeopleScreenState createState() => _SearchPeopleScreenState(); }

class _SearchPeopleScreenState extends State { final PeopleBloc _peopleBloc = PeopleBloc();

List data = []; bool loading = false;

void initState() { Future.delayed(Duration.zero, () { // _peopleBloc.add( // GetPeopleEvent(term: ''), // ); BlocProvider.of(context).add( GetPeopleEvent(term: ''), ); }); super.initState(); }

@override Widget build(BuildContext context) { return BlocConsumer<PeopleBloc, PeopleState>( listener: (context, state) { print("Listener has been called"); if (state is GetSearchResultState) { loading = false; print("Result Found in view"); } else if (state is SearchLoadingState) { loading = true; print("Search loading"); } else if (state is PeopleLoadingState) { loading = true; } else if (state is GetPeopleState) { //print(state.getPeopleResponse.status); loading = false; data = state.getPeopleResponse.data; print("Am Here2"); } else if (state is NoReturnState) { loading = false; } else if (state is PeopleErrorState) { print("Am Here3"); loading = false; print(state.message); final snackBar = SnackBar(content: Text('state.message.toString()')); ScaffoldMessenger.of(context).showSnackBar(snackBar); } }, builder: (context, state) { return Container( child: Indexer( children: [ if (loading) Center( child: Container( width: 80.0, height: 80.0, child: SpinKitCircle( color: Colors.blue, size: 50.0, ), ), ), BlocProvider( create: (context) => PeopleBloc(), child: SingleChildScrollView( child: Container( height: 500.0, width: double.infinity, child: ListView.separated( separatorBuilder: (BuildContext context, int index) => Divider(height: 2), itemCount: data.length, itemBuilder: (context, index) { final people = data[index]; return GestureDetector( onTap: () async { Navigator.pushNamed( context, PeopleProfile.routeName, arguments: people.user_id); // Navigation.intentWithClearAllRoutes( // context, PeopleProfile.routeName); }, child: ListTile( title: Text( people.fullname, style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text('@${people.username}'), leading: CircleAvatar( radius: 25, backgroundColor: Colors.grey, child: ClipRRect( borderRadius: BorderRadius.circular(50), child: Image.network( people.photo_path, width: 50, height: 50, fit: BoxFit.cover, ), ), ), trailing: MyButton( index: index, userID: int.parse(people.user_id)), ), ); }), ), ), ), ], ), ); }, ); }