In the previous post, we covered OTP codes with Spring Security Webflux. We sent the OTP code to the user upon login. In this story, we’ll implement the login process with a mobile app using Flutter.
Prerequisites
This is the list of all the prerequisites for following the part 2:
- Flutter 3.0.0
- Dart 2.17.0
- Android Studio or Visual Studio Code
- Run the backend side of part 1
Overview
We will consume our backend API with the Cubit/Bloc and how to use it in a Flutter application.
What is BLoC?
Business logic components (BLoC) allow you to separate the business logic from the UI. Writing code in BLoC makes it easier to write and reuse tests.
BLoC Architecture

Using the bloc library allows us to separate our application into three layers:
- Presentation
- Business Logic
- Data (Repository, Data Provider)
Cubit
Cubit is a class that extends BlocBase and can be extended to manage any type of state.

As you can see in the diagram above, in Cubit we don’t have events anymore instead they would be replaced by functions. Those functions will then trigger a state change which will update the UI.
Getting Started
Now we start by creating a new flutter project. In the terminal, type the following command:
flutter create flutter_otp

Add Dependencies
First, you need to add the following dependency to the pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
pin_code_fields: ^7.4.0
http: ^0.13.4
flutter_bloc: ^8.0.1
equatable: ^2.0.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- http package to get data from the web service.
- flutter_bloc for making it easy to integrate blocs and cubits
- equatable Being able to compare objects in
Dart
Project Structure
Our project structure will look like this:

- core package: manages all utility classes and components shared at all project layers
- data package: It contains domain packages (responsible for creating data model classes, enums, and DTOs), repositories (responsible for creating and manipulating data. The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates), and providers (responsible for providing data. The data provider must be generic and general-purpose).
- logic package: Responsible for managing the business logic. This layer can depend on one or more repositories to retrieve data needed to build up the application state.
- ui package: Responsible for UI design. It should handle user input and application lifecycle events.
We will start with the data layer which is the lowest level layer and work our way up to the presentation layer.
Remember the authentication flow we described in part 1 of this story.

Data Provider
Let’s create our first provider class AuthProvider . This class interacts with HTTP requests for the consumption of our REST API.
class AuthProvider {
static ApiClient apiClient = ApiClient();
Future<dynamic> signIn(Login login) async {
return await apiClient.post(Uri.parse(baseApi + loginUrl), login.toJson(), {'Content-Type':'application/json', 'Accept': 'application/json'});
}
Future<dynamic> checkOtpCode(String code) async {
String? token = await getToken();
return await apiClient.get(Uri.parse("${baseApi + otpCheckUrl}/$code"), {'Content-Type':'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'});
}
Future<dynamic> resendOtpCode() async {
String? token = await getToken();
return await apiClient.get(Uri.parse(baseApi + otpResendUrl), {'Content-Type':'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token'});
}
}
class ApiClient {
Future<dynamic> get(Uri url, Map<String, String>? headers) async {
dynamic responseJson;
try {
final response = await http.get(url, headers: headers);
responseJson = _returnResponse(response);
} on SocketException {
throw NetworkStatusException();
}
developer.log('api get received!');
return responseJson;
}
Future<dynamic> post(Uri url, dynamic body, Map<String, String>? headers) async {
dynamic responseJson;
try {
final response = await http.post(url, headers: headers , body: jsonEncode(body));
responseJson = _returnResponse(response);
} on SocketException {
throw NetworkStatusException();
}
developer.log('api post data!');
return responseJson;
}
}
Future<String?> getToken() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(tokenKey);
}
dynamic _returnResponse(http.Response response) {
var responseJson = jsonDecode(utf8.decode(response.bodyBytes));
var errorResp = responseJson["message"] ?? response.body.toString();
switch (response.statusCode) {
case 200:
return responseJson;
case 201:
return responseJson;
case 400:
throw BadRequestException(response.body.toString());
case 401:
case 403:
throw UnauthorisedException(errorResp);
case 404:
throw NotFoundDataException(errorResp);
case 500:
throw InternalErrorException(errorResp);
default:
return FetchDataException(
'Error occurred while Communication with Server with StatusCode : ${response.statusCode}');
}
}
Repository
The transformation is done on the raw data returned by the Data Provider in this layer.
class AuthRepository {
final AuthProvider provider;
AuthRepository({required this.provider});
Future<SuccessResponse> userLogin(Login login) async {
final response = await provider.signIn(login);
return SuccessResponse.fromJson(response);
}
Future<SuccessResponse> checkOtpCode(String code) async {
final response = await provider.checkOtpCode(code);
return SuccessResponse.fromJson(response);
}
Future<SuccessResponse> resendOtpCode() async {
final response = await provider.resendOtpCode();
return SuccessResponse.fromJson(response);
}
}
Business Logic Layer
Now let’s use Bloc/Cubit to get the data to the UI. We need to create the files that’ll hold our AuthCubitand also AuthState classes.
- AuthState
The UI will update according to the State it receives from the Bloc.
AuthInitial — State will indicate that no action has yet been taken by the user and that we should display an initial UI
AuthLoading — Will Show Progress Indicator
AuthNetworkError — Will show a network error message
AuthError —Will show an error that something went wrong
AuthSuccess —Will show a successful data loading indicator
AuthSubmit — Will show a successful submission message
@immutable
abstract class AuthState extends Equatable {
@override
List<Object> get props => [];
}
class AuthInitial extends AuthState {
}
class AuthLoading extends AuthState {
}
class AuthNetworkError extends AuthState {
final String message;
AuthNetworkError({
required this.message,
});
@override
List<Object> get props => [message];
}
class AuthError extends AuthState {
final String message;
AuthError({
required this.message,
});
@override
List<Object> get props => [message];
}
class AuthLoaded extends AuthState {
final dynamic response;
AuthLoaded({
required this.response,
});
@override
List<Object> get props => [response];
}
class AuthSubmit extends AuthState {
final dynamic data;
AuthSubmit({
required this.data,
});
@override
List<Object> get props => [data];
}
- AuthCubit
Let’s implement the AuthCubit which will run the AuthRepository logic and emit states.
class AuthCubit extends Cubit<AuthState> {
final AuthRepository repository;
AuthCubit({required this.repository}) : super(AuthInitial());
userLogin(Login login) async {
try {
emit(AuthLoading());
final response = await repository.userLogin(login);
emit(AuthLoaded(response: response ));
} on NetworkStatusException catch(e) {
emit(AuthNetworkError(message: e.toString()));
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
otpCheck(String code) async {
try {
emit(AuthLoading());
final response = await repository.checkOtpCode(code);
emit(AuthLoaded(response: response ));
} on NetworkStatusException catch(e) {
emit(AuthNetworkError(message: e.toString()));
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
otpResend() async {
try {
emit(AuthLoading());
final response = await repository.resendOtpCode();
emit(AuthLoaded(response: response ));
} on NetworkStatusException catch(e) {
emit(AuthNetworkError(message: e.toString()));
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
}
Presentation
Now it is time to complete the presentation layer. We first need to provide the AuthCubit in the router class with a BlocProvider .
class AppRouter {
const AppRouter._();
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
AuthCubit authCubit = AuthCubit(repository: AuthRepository(provider: AuthProvider()));
final args = settings.arguments;
switch (settings.name) {
case SplashScreen.routeName:
return MaterialPageRoute(
builder: (_) => const SplashScreen(),
);
case LoginScreen.routeName:
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (BuildContext context) => authCubit,
child: const LoginScreen(),
),
);
case OtpScreen.routeName:
final params = args as LoginDTO;
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (BuildContext context) => authCubit,
child: OtpScreen(loginParam: params)
),
);
case DashboardScreen.routeName:
return MaterialPageRoute(
builder: (_) => const DashboardScreen()
);
default:
throw const RouteException('Route not found!');
}
}
}
login_screen.dart

We will be using BlocListener which is a Flutter widget that takes a BlocWidgetListener and an optional block and calls the listener when the state of the block changes.
Final Result

If you enjoyed this article, please give it a few claps for support.
The code for this project is uploaded to Github here.