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

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.

Related

Flutter: Error "The getter 'mydata' isn't defined for the type 'providerdemo' " eventhough I've defined them in providerdemo classs

I've defined a variable myvalue for accessing the document field from cloud firestore in Provider class providerdemo .But when I am trying to access it shows error The getter 'mydata' isn't defined for the type 'providerdemo'. .What's wrong in my code ?
providerdemo.dart
class providerdemo with ChangeNotifier {
static String? mydata;
final userData = FirebaseFirestore.instance
.collection("Users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
mydata =(value.data()?['uname'] ?? "Default userName");
}
Below is the class where I'm trying to access the value into Text() widget;
class _testState extends State<test> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Text(Provider.of<providerdemo>(context).mydata ?? "default"),
);
}
}
You didn't declare the variable in the class scope, only in the function scope. So what you have to do, is declare the variable like this:
class providerdemo with ChangeNotifier {
final mydata;
void setMydata(final data) {
mydata = data;
notifyListeners();
}
}
And for the best logic separation, you should create another file with for example a Cloud class, where you will write all the firebase related functions. Then when you initialise your app, you should call the following function like this: final data = Cloud.getMydata();, and use the setMydata function from your provider file in your UI code to update the variable.
class Cloud {
static dynamic getMydata() async {
return await FirebaseFirestore.instance
.collection("Users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
}
}
(Didn't test the code)
Also, please use CamelCase for class names.
You are getting the error The getter 'mydata' isn't defined for the type 'providerdemo' because you have not defined the function mydata in the providerdemo class
Further there are two problems:
You are defining the mydata twice, because of which it is not updating ,
It is not value.data() ?? ["uname"] it is value.data()["uname"] ?? "Default userName"
class providerdemo with ChangeNotifier {
static String mydata;
final userData = FirebaseFirestore.instance
.collection("Users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
// var mydata = (value.data() ?? ["uname"]); // 👈 You are creating new variable mydata
mydata = (value.data()?['uname'] ?? "Default userName"); //👈 Replace the above line by this
print(mydata) // Print the data once so that you know if the correct data is fetched.
});
notifyListeners();
}
So now you can use it as Text(providerdemo.mydata)
Edit
Create Notifier
class UsersState extends ChangeNotifier{
String userName = '';
void getName(){
FirebaseFirestore.instance
.collection("Users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
userName = (value.data()?[' uname'] ?? "Default userName");
print(userName) // Print the data once so that you know if the correct data is fetched.
});
notifyListeners();
}
Change your main.dart file to contain Notifier
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserState>(
create: (context)=> UsersState(),
child:MaterialApp(
home: myHomePage(),
)
);
}
}
Screen where you want to display the name
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
#override
Widget build(BuildContext context) {
Provider.of<UserState>(context, listen: false).getName();
final String name = Provider.of<UsersState>(context).userName;
return Center(
child: Text(name),
);
}
}

Flutter bloc 8.x authentication not updating

Im experimenting and learning on a small project and im trying to implement an authentication workflow. Ive implemented everything and by printing my steps everything should just work fine. Im wondering why my BlocBuilder isn't going to update. The project is really small so I can provide you everything easy. Since im learning bloc, i appreciate every hint, approach and I want to thank you in advance.
terminal output when the app is starting:
flutter: building main.dart
flutter: AppLoaded()
flutter: user is NOT signed in
flutter: false
flutter: Transition { currentState: AuthInitial(), event: AppLoaded(), nextState: UnauthenticatedState() }
flutter: block says user is NOT authenticated
This is completely fine since im checking at the beginning if there is any user data valid. Now when I press on the Login Textbutton in my home.dart my Blocbuilder should show that im logged in, but it doesnt. This is the terminal output:
flutter: AppLoaded()
flutter: signed id with credentials: User{id: 1, socketId: 123, userName: Logged in User}
flutter: user is signed in
flutter: true
flutter: currentuser is not empty: User{id: 1, socketId: 123, userName: Logged in User}
flutter: Transition { currentState: AuthInitial(), event: AppLoaded(), nextState: AuthenticatedState() }
flutter: block says user is authenticated
main.dart
import 'package:fl_auth/bloc/auth/auth_bloc.dart';
import 'package:fl_auth/repositories/user_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/bloc_observer.dart';
import 'home.dart';
import 'models/auth.dart';
void main() {
BlocOverrides.runZoned(
() {
runApp(const MyApp());
},
blocObserver: SimpleBlocObserver(),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Auth _auth = Auth.instance;
UserRepository _userRepository = UserRepository(auth: _auth);
print('building main.dart');
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (context) =>
AuthBloc(userRepository: _userRepository)..add(AppLoaded()),
child: Home(),
),
);
}
}
home.dart
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/auth/auth_bloc.dart';
import 'models/auth.dart';
import 'repositories/user_repository.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Auth _auth = Auth.instance;
UserRepository _userRepository = UserRepository(auth: _auth);
AuthBloc authBloc = AuthBloc(userRepository: _userRepository);
return Scaffold(
body: SizedBox(
height: 500,
child: Column(
children: [
Container(
height: 200,
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is UnauthenticatedState) {
return Center(child: Text('User is unauthenticated'));
} else if (state is AuthenticatedState) {
return Center(child: Text('YEAH logged in!'));
} else {
return Center(child: Text('something went wrong'));
}
}),
),
TextButton(
onPressed: () => {
authBloc.userRepository.signIn(),
authBloc.add(AppLoaded())
},
child: Text('Login')),
],
)));
}
}
auth_event.dart
part of 'auth_bloc.dart';
abstract class AuthEvent extends Equatable {
const AuthEvent();
#override
List<Object> get props => [];
}
class AppLoaded extends AuthEvent {}
auth_state.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
part of 'auth_bloc.dart';
abstract class AuthState extends Equatable {
const AuthState();
#override
List<Object> get props => [];
}
class AuthInitial extends AuthState {}
class AuthenticatedState extends AuthState {
User user;
AuthenticatedState({
required this.user,
});
}
class UnauthenticatedState extends AuthState {}
auth.bloc
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:fl_auth/repositories/user_repository.dart';
import '../../models/user.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
UserRepository userRepository;
AuthBloc({required this.userRepository}) : super(AuthInitial()) {
on<AppLoaded>((event, emit) async {
try {
var isSignedIn = await userRepository.isSignedIn();
print(isSignedIn);
if (isSignedIn) {
var user = await userRepository.getCurrentUser();
emit(AuthenticatedState(user: user));
print('block says user is authenticated');
} else {
emit(UnauthenticatedState());
print('block says user is NOT authenticated');
}
} catch (e) {
emit(UnauthenticatedState());
}
});
}
}
user_repository.dart
import 'package:fl_auth/models/user.dart';
import '../models/auth.dart';
class UserRepository {
Auth auth = Auth.instance;
UserRepository({required this.auth});
// sign in with username
//TODO: change hardcoded username to email and passwort later on
Future<User> signIn() async {
try{
await Future.delayed(Duration(seconds: 1));
var credentials = User(id: 1, socketId: '123', userName: 'Logged in User');
print('signed id with credentials: ${auth.currentUser}');
auth.currentUser = credentials;
return auth.currentUser;
}catch(e){
print(e.toString());
throw e;
}
}
// check signed in status
Future<bool> isSignedIn() async {
try{
await Future.delayed(const Duration(seconds: 1));
var currentUser = auth.currentUser;
if(currentUser.isNotEmpty){
print('user is signed in');
return true;
} else {
print('user is NOT signed in');
return false;
}
}catch(e){
print(e.toString());
throw e;
}
}
// get user
Future<User> getCurrentUser() async {
try{
await Future.delayed(const Duration(seconds: 1));
var currentUser = auth.currentUser;
if(currentUser.isNotEmpty){
print('currentuser is not empty: $currentUser');
return currentUser;
} else {
var message = 'User is empty';
print('currentuser IS empty: $currentUser');
throw message;
}
}catch(e){
print(e.toString());
throw e;
}
}
}
auth.dart
import 'user.dart';
class Auth {
/// private constructor
Auth._();
/// the one and only instance of this singleton
static final instance = Auth._();
//ChatBloc chatBloc = ChatBloc(DatabaseApi.db);
// Create a User instance. Actually it would be better if this is empty so I can notice if a user is valid or not and can react by checking if the user has values and
// if not log the user out later on
User currentUser = User.empty;
}
user.dart
import 'package:equatable/equatable.dart';
/// {#template user}
/// User model
///
/// [User.empty] represents an unauthenticated user.
/// {#endtemplate}
class User extends Equatable {
/// {#macro user}
const User({
required this.id,
this.socketId,
this.userName,
});
/// The current user's scoket id.
final String? socketId;
/// The current user's id.
final int id;
/// The current user's name (display name).
final String? userName;
/// Empty user which represents an unauthenticated user.
static const empty = User(id: 0);
/// Convenience getter to determine whether the current user is empty.
bool get isEmpty => this == User.empty;
/// Convenience getter to determine whether the current user is not empty.
bool get isNotEmpty => this != User.empty;
#override
List<Object?> get props => [id, socketId, userName];
// Convert a user into a Map. The keys must correspond to the names of the
// columns in the database.
Map<String, dynamic> toMap() {
return {
'id': id,
'socketId': socketId,
'userName': userName,
};
}
factory User.fromMap(Map<String, dynamic> map) {
return User(
id: map['id'] as int,
socketId: map['socketId'] as String,
userName: map['userName'] as String,
);
}
// Implement toString to make it easier to see information about
// each user when using the print statement.
#override
String toString() {
return 'User{id: $id, socketId: $socketId, userName: $userName}';
}
}
The issue is In your OnTap Function you can call Event like this. I have checked and It's working as expected.
TextButton(
onPressed: () => {
authBloc.userRepository.signIn(),
context.read<AuthBloc>().add(AppLoaded())
},
child: const Text('Login')),

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];
}

Future Provider Stuck In loading state

I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider
final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));
In my UI I have this....
class LoginScreen extends HookWidget {
final TextEditingController emailEditingController = TextEditingController();
final TextEditingController passwordEditingController =
TextEditingController();
#override
Widget build(BuildContext context) {
var userInput =
UserInput(emailEditingController.text, passwordEditingController.text);
final login = useProvider(loginProvider(userInput));
return login.when(
data: (user) => Login(emailEditingController, passwordEditingController),
loading: () => const ProgressIndication(),
error: (error, stack) {
if (error is DioError) {
return Login(emailEditingController, passwordEditingController);
} else {
return Login(emailEditingController, passwordEditingController);
}
},
);
}
}
here is my doLogin function.
#override
Future<dynamic> doLogin(String email, String password) async {
try {
final response = await _read(dioProvider)
.post('$baseUrl/login', data: {'email': email, 'password': password});
final data = Map<String, dynamic>.from(response.data);
return data;
} on DioError catch (e) {
return BadRequestException(e.error);
} on SocketException {
return 'No Internet Connection';
}
}
I would like to know why it's stuck in the loading state. Any help will be appreciated.
First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.
In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.
The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.
First, a StateNotifierProvider:
enum LoginState { loggedOut, loading, loggedIn, error }
class LoginStateNotifier extends StateNotifier<LoginState> {
LoginStateNotifier(this._read) : super(LoginState.loggedOut);
final Reader _read;
late final Map<String, dynamic> _user;
static final provider =
StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));
Future<void> login(String email, String password) async {
state = LoginState.loading;
try {
_user = await _read(authRepositoryProvider).doLogin(email, password);
state = LoginState.loggedIn;
} catch (e) {
state = LoginState.error;
}
}
Map<String, dynamic> get user => _user;
}
This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.
Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.
class LoginScreen extends HookWidget {
const LoginScreen({Key? key, this.error = false}) : super(key: key);
final bool error;
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return Column(
children: [
TextField(
controller: emailController,
),
TextField(
controller: passwordController,
),
ElevatedButton(
onPressed: () async {
await context.read(LoginStateNotifier.provider.notifier).login(
emailController.text,
passwordController.text,
);
},
child: Text('Login'),
),
if (error) Text('Error signing in'),
],
);
}
}
You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.
Last but not least, we need to do something with our fancy new state.
class AuthPage extends HookWidget {
const AuthPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final loginState = useProvider(LoginStateNotifier.provider);
switch (loginState) {
case LoginState.loggedOut:
return LoginScreen();
case LoginState.loading:
return LoadingPage();
case LoginState.loggedIn:
return HomePage();
case LoginState.error:
return LoginScreen(error: true);
}
}
}
In practice, you're going to want to wrap this in another widget with a Scaffold.
I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.

BlocListener Only returning intial loading state

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();
},
);
}
}