Manejo de sesión del usuario
En las aplicaciones, es fundamental gestionar la sesión del usuario para determinar si está autenticado o no. Esta gestión permite, entre otras cosas, mostrar u ocultar secciones de la app, restringir la navegación y adaptar la experiencia del usuario según su estado de autenticación.
En el contexto de Flutter, el manejo de la sesión del usuario puede realizarse de manera eficiente usando un UserBloc. Este Bloc se encarga de gestionar el estado de autenticación del usuario, realizar la gestión de sesión y manejar eventos relacionados con el inicio, cierre de sesión y actualización de la información del usuario. A continuación, te explico cómo funciona cada parte del código:
Consideraciones iniciales
Antes de comenzar la implementación del UserBloc
, asegúrate de que los siguientes componentes estén listos:
APIs
Las APIs necesarias para autenticar al usuario y obtener los datos del usuario autenticado deben estar implementadas y funcionando correctamente (idealmente plantear dos: AuthApi
y UserApi
).
El AuthApi
puede incluir:
- Autenticación (Login/Logout/Registro)
- Verificación de estado de sesión (si el usuario sigue autenticado o no).
El UserApi
puede incluir:
- Obtención de datos del usuario.
Repositorios
Debes tener los repositorios que interactúen con las APIs mencionadas. El UserRepository
debería exponer un Stream<User>
para poder suscribirse a los cambios del usuario.
Casos de Uso
UserUseCase
: Obtiene y maneja la información del usuario.AuthUseCase
: Gestiona el inicio y cierre de sesión.
Implementación
Comienza creando un UserBloc
en la carpeta core de la aplicación y define los eventos y estados para manejar adecuadamente los flujos de usuario.
UserEvent
Contiene los eventos que se pueden emitir en el UserBloc, tales como cuando la sesión comienza, cuando el usuario cambia, y cuando se solicita el cierre de sesión.
part of 'user_bloc.dart';
abstract class UserEvent extends Equatable {
const UserEvent();
List<Object> get props => [];
}
class UserStarted extends UserEvent {
const UserStarted();
}
class UserChanged extends UserEvent {
const UserChanged({required this.user});
final User user;
List<Object> get props => [user];
}
class UserLogoutRequested extends UserEvent {
const UserLogoutRequested();
}
UserState
El UserState
contiene el estado actual de la sesión del usuario, como la información del usuario y el estado de autenticación.
Tiene dos propiedades principales:
user
: Representa al usuario actual, que puede ser un objeto User o User.empty si el usuario no está autenticado.status
: El estado actual de la sesión, que puede ser uno de los valores del enum UserStatus.
UserStatus
maneja los siguientes estados:
unknown
: Estado inicial o desconocido (aún no se sabe si el usuario está autenticado).loading
: indica que los datos del usuario están siendo cargados.authenticated
: el usuario está autenticado.unauthenticated
: El usuario no está autenticado.failure
: El estado cuando ocurre un error en la autenticación o en la obtención de los datos.
part of 'user_bloc.dart';
enum UserStatus {
unknown,
loading,
authenticated,
unauthenticated,
failure;
bool get isUnknown => this == unknown;
bool get isLoading => this == loading;
bool get isAuthenticated => this == authenticated;
bool get isUnauthenticated => this == unauthenticated;
bool get isFailure => this == failure;
}
class UserState extends Equatable {
const UserState({
this.user = User.empty,
this.status = UserStatus.unknown,
});
final User user;
final UserStatus status;
bool get isAuthenticated => user != User.empty;
UserState copyWith({
User? user,
UserStatus? status,
}) {
return UserState(
user: user ?? this.user,
status: status ?? this.status,
);
}
List<Object?> get props => [user, status];
}
UserBloc
Gestiona los eventos y actualiza el estado del Bloc de acuerdo a los cambios en la autenticación del usuario.
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:project/src/domain/entities/entities.dart';
import 'package:project/src/domain/use_cases/use_cases.dart';
part 'user_event.dart';
part 'user_state.dart';
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc({
required UserUseCase userUseCase,
required AuthUseCase authUseCase,
}) : _userUseCase = userUseCase,
_authUseCase = authUseCase,
super(const UserState()) {
on<UserStarted>(_onStarted);
on<UserChanged>(_onChanged);
on<UserLogoutRequested>(_onLogoutRequested);
add(const UserStarted());
_userSubscription = _userUseCase.user.listen((user) {
add(UserChanged(user: user));
});
_signOutSubscription = _authUseCase.onNotAuthorized.listen((value) {
add(const UserLogoutRequested());
});
}
final UserUseCase _userUseCase;
final AuthUseCase _authUseCase;
StreamSubscription<User>? _userSubscription;
FutureOr<void> _onStarted(
UserStarted event,
Emitter<UserState> emit,
) async {
try {
emit(state.copyWith(status: UserStatus.loading));
final currentUser = await _userUseCase.currentUser;
emit(
state.copyWith(
user: currentUser,
status: currentUser == User.empty
? UserStatus.unauthenticated
: UserStatus.authenticated,
),
);
} catch (e) {
log(e.toString());
}
}
FutureOr<void> _onChanged(
UserChanged event,
Emitter<UserState> emit,
) async {
emit(state.copyWith(user: user));
}
FutureOr<void> _onLogoutRequested(
UserLogoutRequested event,
Emitter<UserState> emit,
) async {
emit(state.copyWith(status: UserStatus.loading));
await _authUseCase.signOut();
emit(
state.copyWith(
user: User.empty,
status: UserStatus.unauthenticated,
),
);
}
Future<void> close() async {
await _userSubscription?.cancel();
await _signOutSubscription?.cancel();
return super.close();
}
}
_onStarted
: Este método maneja el evento UserStarted y verifica el estado de autenticación del usuario cuando el Bloc se inicializa. Si el usuario está autenticado, cambia el estado a authenticated; si no, a unauthenticated._onChanged
: Este método maneja el evento UserChanged y actualiza el estado con los nuevos datos del usuario, siempre que haya un cambio en la información del usuario._onLogoutRequested
: Este método maneja el evento UserLogoutRequested y realiza el cierre de sesión. Actualiza el estado a unauthenticated y establece el user a User.empty.
BlocProvider
Instancia el BlocProvider en el archivo app.dart
, de forma que se pueda acceder a él en cualquier instancia de la aplicación.
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
BlocProvider(
lazy: false,
create: (context) => UserBloc(
userUseCase: UserUseCase(),
authUseCase: AuthUseCase(),
),
),
...
],
child: MaterialApp.router(
routerConfig: router,
builder: (context, child) {
return child;
},
theme: CustomTheme.theme(context),
localizationsDelegates: AppLocalizations.localizationsDelegates,
// Currently fixed to Spanish. In case of needing English,
// it should be changed to:
// supportedLocales: AppLocalizations.supportedLocales,
supportedLocales: const [
Locale('es'),
],
),
);
}
}
Usos del UserBloc
Existen dos escenarios comunes respecto a la autenticación y la navegación en una aplicación:
- Aplicaciones que requieren autenticación para acceder al contenido: En este caso, la pantalla de inicio es un formulario de inicio de sesión. Una vez que el usuario se autentica con éxito, se le redirige al índice principal o la vista principal de la aplicación.
- Aplicaciones que no requieren autenticación para acceder al contenido: En este tipo de aplicaciones, el usuario puede navegar por el contenido sin necesidad de iniciar sesión. La aplicación permite el acceso a ciertas funcionalidades o vistas sin restricciones, aunque algunas secciones adicionales podrían requerir autenticación.
Para el primer caso, puedes instanciar un BlocListener
en el router.dart
, de forma que cuando cambie la autenticación realices un cambio de ruta entre el LoginPage
y el IndexPage
.
Para el segundo caso, por medio del contexto puedes acceder al estado de autenticación que guarda el Bloc y en base al valor, mostrar o no ciertas secciones de la app, permitir o no navegar a ciertas vistas, etc.
context.read<UserBloc>().state.isAuthenticated