BlocListener Only returning intial loading state - flutter

I am building an app with flutter bloc. The issue i have is my bloc listener is only firing the initial state and not subsequent state change. All other questions have not been helpful as my state extends equatable to compare state. Here is my code below;
my login bloc
import 'package:bloc/bloc.dart';
import 'package:mobile_app/classes/custom_exception.dart';
import 'package:mobile_app/repositories/auth_repository.dart';
import 'package:mobile_app/states/login_status.dart';
class LoginBloc extends Cubit<LoginState> {
LoginBloc(this.auth) : super(LoginState.initial());
final AuthRepository auth;
void login(String email, String password) async {
emit(state.copyWith(loginStatus: Status.LOADING, isAuthenticated: false));
final response = await auth.doLogin(email, password);
if (response is AppException) {
emit(state.copyWith(
loginStatus: Status.ERROR,
error: response.toString(),
isAuthenticated: false));
} else {
emit(
state.copyWith(loginStatus: Status.COMPLETED, isAuthenticated: true));
}
}
}
My state file;
enum Status { INITIAL, LOADING, COMPLETED, ERROR }
class LoginState extends Equatable {
final Status loginStatus;
final String? error;
final bool isAuthenticated;
LoginState(
{required this.loginStatus, this.error, required this.isAuthenticated});
factory LoginState.initial() {
return LoginState(loginStatus: Status.INITIAL, isAuthenticated: false);
}
LoginState copyWith(
{required Status loginStatus,
String? error,
required bool isAuthenticated}) {
return LoginState(
loginStatus: loginStatus,
error: error,
isAuthenticated: isAuthenticated);
}
#override
List<Object?> get props => [loginStatus, error, isAuthenticated];
}
Then my listener
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state.loginStatus == Status.COMPLETED) {
Navigator.of(context).pushReplacementNamed('/dashboard');
}
if (state.loginStatus == Status.ERROR) {
final snackBar = SnackBar(
backgroundColor: Colors.black,
content: Text(state.error!),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
print(state);
},
I understand the listener is only called once for every state change but it's as if the listener is not registering any state change. Help will be appreciated!

Okay so i think i know where the error is coming from. i have a blocbuilder that is showing different pages based on the current state and on of those pages include the login page that has the bloc listener. So i removed the bloc builder and just returned the login page with the bloc listener the snackbar is called as it should be. I used a blocconsumer to achieve what i want to achieve.
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<LoginBloc, LoginState>(
listener: (context, state) {
if (state is LoginError) {
final snackBar = SnackBar(
backgroundColor: Colors.black,
content: Text(state.error),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
},
builder: (context, state) {
if (state is LoginLoading) {
return ProgressIndication();
} else if (state is LoginSuccess) {
return DashboardScreen();
}
return Login();
},
);
}
}

Related

Flutter bloc does not fetch data from API

I have some data that I want to fetch when the page loads up.
Below is the code for fetching the data in the screen
class _HighSchoolScreenState extends State<HighSchoolScreen> {
late PagingController<int, HighSchool> _pagingController;
#override
void initState() {
_pagingController = context.read<HighSchoolsBloc>().pageController;
_pagingController.addPageRequestListener(
(pageKey) {
context.read<HighSchoolsBloc>().add(
FetchHighSchools(page: pageKey, category: widget.category!),
);
},
);
super.initState();
}
I am using the infinite_scroll_pagination package to lazy load the data in the UI
Widget build(BuildContext context) => PagedListView<int, HighSchool>(
addAutomaticKeepAlives: false,
shrinkWrap: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<HighSchool>(
animateTransitions: true,
newPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
firstPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
itemBuilder: (context, item, index) => SchoolsContent(
item: item,
theme: theme,
isIos: isIos,
),
),
),
Below is also my bloc for the data
class HighSchoolsBloc extends Bloc<HighSchoolsEvent, HighSchoolsState> {
final String token = Hive.box('user').get(kToken);
bool hasNextPage = true;
late HighSchoolRepo _highSchoolRepo;
final PagingController<int, HighSchool> pageController =
PagingController(firstPageKey: 0);
Future<void> _fetchPage(int pageKey, FetchHighSchools event) async {
try {
final results = await _highSchoolRepo.get(
page: event.page,
category: event.category,
token: token,
);
hasNextPage = results['hasNextPage'];
final List<HighSchool> newItems = results['schools'];
if (!hasNextPage) {
pageController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
pageController.appendPage(newItems, nextPageKey);
}
} catch (error) {
pageController.error = 'error';
}
}
HighSchoolsBloc(this._highSchoolRepo) : super(InitailState()) {
on<FetchHighSchools>((event, emit) {
print('new ${event.category}');
_fetchPage(event.page, event);
});
}
}
So the real issue is whenever visit the screen, the data fetches correctly and shows on the screen(UI) but when I leave the screen and press on another category, it should fetch data based on the different category now but it is not event fetching anything again. it just shows the same data that was fetched previously
Below is the states for my bloc
#immutable
abstract class HighSchoolsState extends Equatable {
#override
List<Object?> get props => [];
}
// ignore_for_file: public_member_api_docs, sort_constructors_first
class InitailState extends HighSchoolsState {}
class HighSchoolFetchError extends HighSchoolsState {
late final String error;
HighSchoolFetchError(this.error);
#override
List<Object?> get props => [error];
}
PLEASE NOT THAT LOADING AND ERROR ARE HANDLED BY THE PACKAGE SO THERE'S NO NEED TO MAKE IT'S RELATIVE STATES
ALSO, ONE MORE ERROR I AM FACING IS SOMETIMES, WHEN I SCROLL THROUGH THE DATA GIVEN IT GIVES ME AN ERROR OF
This widget has been unmounted, so the State no longer has a context (and should be considered defunct). // It appears on line 32 which is where the initstate it.

Cubit - listener does not catching the first state transition

I'm using a Cubit in my app and I'm struggling to understand one behavior.
I have a list of products and when I open the product detail screen I want to have a "blank" screen with a loading indicator until receiving the data to populate the layout, but the loading indicator is not being triggered in the listener (only in this first call, when making a refresh in the screen it shows the loader).
I'm using a BlocConsumer and i'm making the request in the builder when catching the ApplicationInitialState (first state), in cubit I'm emitting the ApplicationLoadingState(), but this state transition is not being caught in the listener, only when the SuccessState is emitted the listener triggers and tries to remove the loader.
I know the listener does not catch the first State emitted but I was expecting it to catch the first state transition.
UI
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
Widget build(BuildContext context) {
_l10n = AppLocalizations.of(context);
return _buildConsumer();
}
_buildConsumer() {
return BlocConsumer<ProductCubit, ApplicationState>(
bloc: _productCubit,
builder: (context, state) {
if (state is ApplicationInitialState) {
_getProductDetail();
}
return Scaffold(
appBar: _buildAppbar(state),
body: _buildBodyState(state),
);
},
listener: (previous, current) async {
if (current is ApplicationLoadingState) {
_loadingIndicator.show(context);
} else {
_loadingIndicator.close(context);
}
},
);
}
Cubit
class ProductCubit extends Cubit<ApplicationState> with ErrorHandler {
final ProductUseCase _useCase;
ProductCubit({
required ProductUseCase useCase,
}) : _useCase = useCase,
super(const ApplicationInitialState());
void getProductDetail(String id) async {
try {
emit(const ApplicationLoadingState());
final Product = await _useCase.getProductDetail(id);
emit(CSDetailSuccessState(
detail: ProductDetailMapper.getDetail(Product),
));
} catch (exception) {
emit(getErrorState(exception));
}
}
}
ApplicationLoadingState
abstract class ApplicationState extends Equatable {
const ApplicationState();
#override
List<Object> get props => [];
}
class ApplicationLoadingState extends ApplicationState {
const ApplicationLoadingState();
}

Why can't I access the User's uid from the bloc's state in this example?

After successfully signing in to Firestore using the flutterfire_ui pacakge, the user is taken to HomeScreen where initState adds a GetUser event, which eventually causes the UserBloc to yield a state object called UserLoaded with a property called activeUser, which should contain a User object with a uid property. However, when I try to access state.activeUser.uid from inside the Blockbuilder, it throws the following error:
The getter 'uid' isn't defined for the class 'Stream<User?>'.
lib/screens/home_page.dart:38
'Stream' is from 'dart:async'.
'User' is from 'package:firebase_practice/models/user.dart' ('lib/models/user.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'uid'.
'HomeScreen state is: ${state.activeUser?.uid}',
Is this because I'm using both flutterfire_ui and FirebaseAuth? Any help would be greatly appreciated.
User Model
class User {
final uid;
final userName;
final email;
User({required this.uid, this.userName, this.email});
}
AuthService:
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_practice/models/user.dart';
class AuthService {
final auth.FirebaseAuth _firebaseAuth;
AuthService({auth.FirebaseAuth? firebaseAuth})
: _firebaseAuth = firebaseAuth ?? auth.FirebaseAuth.instance;
//create a dart User from Firebase user
User? _userFromFirebaseAuth(auth.User? user) {
return User(uid: user!.uid, email: user!.email);
}
Stream<User?>? get user {
return _firebaseAuth.authStateChanges().map(_userFromFirebaseAuth);
}
UserBloc:
class UserBloc extends Bloc<UserEvent, UserState> {
final AuthService _authService;
UserBloc( this._authService) : super(UserInitial()) {
on<GetUser>(_getUser);
}
FutureOr<void> _getUser(GetUser event, Emitter<UserState> emit) async {
Stream<User?>? user = await _authService.user;
if(user != null){
emit(UserLoaded(activeUser: user));
}
}
}
UserState:
class UserLoaded extends UserState {
Stream<User?> activeUser;
UserLoaded({required this.activeUser});
#override
List<Object> get props => [activeUser];
}
HomeScreen:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoaded) {
return Scaffold(
body: Center(
child: Text(
'HomeScreen with state is: ${state.activeUser.uid}',
style: TextStyle(fontSize: 40),
),
),
);
}
return CircularProgressIndicator();
},
);
}
}
You can't access the 'uid' directly because 'activeUser' is a stream of 'user'. So you could wrap your Text-widget with a StreamBuilder and provide 'state.activeUser' as the stream:
StreamBuilder(
stream: state.activeUser,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data?.uid : "");
}
return Text("");
},
);
But I have a question there, why do you save the stream itself inside your UserState? Why not save only the User and emit a new state whenever authStateChanges fires? You could do something like this:
class UserBloc extends Bloc<UserEvent, UserState> {
final AuthService _authService;
StreamSubscription<User?> _userSubscription;
UserBloc(this._authService) : super(UserInitial()) {
on<GetUser>(_getUser);
}
void _getUser(GetUser event, Emitter<UserState> emit) {
_userSubscription ??= _authService.user.listen((user) {
emit(UserLoaded(activeUser: user));
});
}
}
So you can change the UserState to hold a User? instead of a stream and you can access it directly inside you widget how you did it in your sample.
Attention: The code samples are only from my memory and probably wont work out of the box.

flutter_login and flutter_bloc navigation after authentication: BlocListener not listening to state change

I am trying to combine this with bloc, using this design pattern from the docs.
After the state has been instantiated, BlocListener stops listening to the authentication bloc and I am kind of forced to use the login form's onSubmitAnimationCompleted method for routing, which makes the listener useless in the first place.
MaterialApp() is identical to the example provided in the docs (I am trying to navigate from the login screen, which is the initialRoute in this case, to the home screen)
the login form looks like this:
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState> (
listener: (context, state) {
// first time around state is read
if (state is AuthenticationAuthenticated) {
Navigator.of(context).pushNamed(Home.routeName);
}
},
child: BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, state) {
return FlutterLogin(
title: 'Login',
logo: const AssetImage('lib/assets/madrid.png'),
onLogin: _authUser,
onSignup: _signupUser,
onRecoverPassword: _recoverPassword,
loginProviders: <LoginProvider>[
... Providers here...
],
// if this method is omitted, I'll get a [ERROR:flutter/lib/ui/ui_dart_state.cc(209)]
onSubmitAnimationCompleted: () {
Navigator.of(context).pushNamed(Home.routeName);
},
);
},
),
);
}
I am splitting events an state between two blocs, 'AuthenticationBloc' (wraps entire app, if a token has been stored then the state will be 'AuthenticationAuthenticated') and 'LoginBloc' (used for login/logout events)
#1 when I click on the sign up button, the associated method will call _loginBloc?.add(SignUpButtonPressed(email: email, password: password))
#2 fast forward to the bloc:
LoginBloc({required this.authenticationBloc, required this.loginRepository})
: super(const SignInInitial()) {
on<SignUpButtonPressed>(_signUp);
}
...
FutureOr<void> _signUp<LoginEvent>(SignUpButtonPressed event, Emitter<LoginState> emit) async {
emit(const SignInLoading());
try {
final credentials = User(email: event.email, password: event.password);
final success = await loginRepository.signUp(credentials);
if (success) {
final token = await loginRepository.signIn(credentials);
authenticationBloc.add(LoggedIn(email: event.email, token: token));
} else {
emit(const SignInFailure(error: 'Something went wrong'));
}
} on Exception {
emit(const SignInFailure(error: 'A network Exception was thrown'));
} catch (error) {
emit(SignInFailure(error: error.toString()));
}
}
this is successful, and it triggers the authentication bloc:
AuthenticationBloc({required this.userRepository})
: super(const AuthenticationUninitialized()) {
on<LoggedIn>(_loggedIn);
}
...
FutureOr<void> _loggedIn<AuthenticationEvent>(LoggedIn event, Emitter<AuthenticationState> emit) async {
await userRepository?.persistEmailAndToken(
event.email, event.token);
await _initStartup(emit);
}
...
Future<void> _initStartup(Emitter<AuthenticationState> emit) async {
final hasToken = await userRepository?.hasToken();
if (hasToken != null && hasToken == true) {
emit(const AuthenticationAuthenticated());
return;
} else {
emit(const AuthenticationUnauthenticated());
}
}
... and at the end of this, the state is updated to AuthenticationAuthenticated, which is the expected behaviour, and the observer logs the transition as expected.
Now, this state change should trigger the navigation from within the BlocListener, but nope.
I would like to get rid of the Navigator inside the onSubmitAnimationCompleted, and rely on the state change.
I reckon this might be caused by Equatable, as my state extends that:
abstract class AuthenticationState extends Equatable {
const AuthenticationState();
#override
List<Object> get props => [];
}
class AuthenticationAuthenticated extends AuthenticationState {
const AuthenticationAuthenticated();
}
However, I've tried for hours, but I can't find anything in the docs, github, or SO that works.
So, I have not been able to get rid of the Navigator inside of onSubmitAnimationCompleted (I guess the BlocListener is disposed when the form is submitted, and before the animation is completed), but in the process I've managed to make my state management clean and robust, so I'll leave a little cheatsheet below, feel free to comment or give your opinion:
Assuming your widget's build method looks something like this:
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState> (
bloc: _authenticationBloc,
listener: (context, state) {
if (state.status == AuthenticationAppState.authenticated) {
Navigator.of(context).pushNamed(Home.routeName);
}
},
child: BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, state) {
return FlutterLogin(
...
and that your events extend Equatable
import 'package:equatable/equatable.dart';
abstract class AuthenticationEvent extends Equatable {
const AuthenticationEvent();
#override
List<Object> get props => [];
}
class LoggedIn extends AuthenticationEvent {
final String email;
final dynamic token;
const LoggedIn({ required this.email, this.token });
#override
List<Object> get props => [email, token];
}
your Bloc will look like:
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
final SecureStorage? userRepository;
AuthenticationBloc({required this.userRepository})
: super(const AuthenticationState.uninitialized()) {
on<LoggedIn>(_loggedIn);
on<LoggedOut>(_loggedOut);
on<UserDeleted>(_userDeleted);
}
...
FutureOr<void> _loggedOut<AuthenticationEvent>(LoggedOut event, Emitter<AuthenticationState> emit) async {
emit(const AuthenticationState.loggingOut());
await userRepository?.deleteToken();
// API calls here
// event has access the event's properties e.g. event.email etc
}
the state has been refactored to:
import 'package:equatable/equatable.dart';
enum AuthenticationAppState {
uninitialized,
unauthenticated,
authenticated,
loggingOut,
loading,
}
class AuthenticationState extends Equatable {
const AuthenticationState._({
required this.status,
});
const AuthenticationState.uninitialized() : this._(status: AuthenticationAppState.uninitialized);
const AuthenticationState.unauthenticated() : this._(status: AuthenticationAppState.unauthenticated);
const AuthenticationState.authenticated() : this._(status: AuthenticationAppState.authenticated);
const AuthenticationState.loggingOut() : this._(status: AuthenticationAppState.loggingOut);
const AuthenticationState.loading() : this._(status: AuthenticationAppState.loading);
final AuthenticationAppState status;
#override
List<Object> get props => [status];
}

flutter_bloc many Event to many BlocBuilder

Recently I am learning flutter_bloc, and I refer to the project flutter_weather.
What I am puzzled is that if a Bloc class has many Events, and most of the Events will have values returned by State, and there are many BlocBuilders in the project, what should I do if I want a BlocBuilder to only respond to a certain Event?
The method I can think of is to divide this Bloc into multiple Blocs, or treat each value to be returned as an attribute of Bloc, BlocBuilder uses the buildwhen method to determine whether to rebuild.
But both of these methods are not good for me. Is there any good method? It is best to have projects on github for reference.
For example:
This is Event:
abstract class WeatherEvent extends Equatable {
const WeatherEvent();
}
class WeatherRequested extends WeatherEvent {
final String city;
const WeatherRequested({#required this.city}) : assert(city != null);
#override
List<Object> get props => [city];
}
class WeatherRefreshRequested extends WeatherEvent {
final String city;
const WeatherRefreshRequested({#required this.city}) : assert(city != null);
#override
List<Object> get props => [city];
}
This is State:
abstract class WeatherState extends Equatable {
const WeatherState();
#override
List<Object> get props => [];
}
class WeatherInitial extends WeatherState {}
class WeatherLoadInProgress extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
final Weather weather;
const WeatherLoadSuccess({#required this.weather}) : assert(weather != null);
#override
List<Object> get props => [weather];
}
class WeatherLoadFailure extends WeatherState {}
This is Bloc:
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
final WeatherRepository weatherRepository;
WeatherBloc({#required this.weatherRepository})
: assert(weatherRepository != null),
super(WeatherInitial());
#override
Stream<WeatherState> mapEventToState(WeatherEvent event) async* {
if (event is WeatherRequested) {
yield* _mapWeatherRequestedToState(event);
} else if (event is WeatherRefreshRequested) {
yield* _mapWeatherRefreshRequestedToState(event);
}
}
Stream<WeatherState> _mapWeatherRequestedToState(
WeatherRequested event,
) async* {
yield WeatherLoadInProgress();
try {
final Weather weather = await weatherRepository.getWeather(event.city);
yield WeatherLoadSuccess(weather: weather);
} catch (_) {
yield WeatherLoadFailure();
}
}
Stream<WeatherState> _mapWeatherRefreshRequestedToState(
WeatherRefreshRequested event,
) async* {
try {
final Weather weather = await weatherRepository.getWeather(event.city);
yield WeatherLoadSuccess(weather: weather);
} catch (_) {}
}
}
This is BlocConsumer:
// BlocBuilder1
BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
if (state is WeatherLoadInProgress) {
return Center(child: CircularProgressIndicator());
}
if (state is WeatherLoadSuccess) {
final weather = state.weather;
return Center(child: Text("WeatherRequested "))
}
)
// BlocBuilder2
BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
if (state is WeatherLoadInProgress) {
return Center(child: CircularProgressIndicator());
}
if (state is WeatherLoadSuccess) {
final weather = state.weather;
return Center(child: Text("WeatherRefreshRequested"))
}
)
The problem is that I want BlocBuilder1 only to work when the type of Event is WeatherRequested and BlocBuilder2 only works when the type of Event is WeatherRefreshRequested. One of my ideas is that each Event has its own State, and then judge the type of State in buildwhen.
Is there any good method?
if you want to build you widget to respond for certain states you should use
BlocConsumer and tell that bloc in buildWhen to tell it what state it should build/rebuild you widget on.
BlocConsumer<QuizBloc, QuizState>(
buildWhen: (previous, current) {
if (current is QuizPoints)
return true;
else
return false;
},
listener: (context, state) {},
builder: (context, state) {
if (state is QuizPoints)
return Container(
child: Center(
child: Countup(
begin: 0,
end: state.points,
duration: Duration(seconds: 2),
separator: ',',
),
),
);
else
return Container();
},
);