Building An Authentication Flow In Flutter Using The GetX Library
A little under a year ago I wrote an article where I showed how to create an authentication flow using BLoC. On New Year’s Day (2021) as I was catching up with everything Flutter after being off the platform for a little over 6 months, I came across an awesome library called GetX. After reading about it for a couple of hours I decided to learn it by creating something. In this post I will create an authentication flow using GetX
.
What Is GetX
According to the docs GetX
is an ultra-light library for Flutter that combines high performance state management, intelligent dependency injection and route management. It has 3 basic principles at its core – performance, productivity and organization. If you want to learn more about this library check out the docs link above.
In this post I will make use of GetX
’s state management and dependency injection to create an authentication flow similar to the one I did using Bloc. I am not going to show every line of code for brevity. All the source code is available on GitHub. Let’s get started.
The App
Create a new flutter app and add the following libraries:
get: ^3.24.0
equatable: ^1.0.2
Once the packages have been installed create a directory called features/
where we are going to add all our features for the project. We will put everything that relates to a feature (or module) inside its own feature folder. This helps with organization in my opinion. We will start off with the authentication feature.
Authentication Feature
Add a directory authentication/
inside the features
directory and add authentication_state.dart
therein. Inside this file add the following code:
// features/authentication/authentication_state.dart
class AuthenticationState extends Equatable {
const AuthenticationState();
@override
List<Object> get props => [];
}
class AuthenticationLoading extends AuthenticationState {}
class UnAuthenticated extends AuthenticationState {}
class Authenticated extends AuthenticationState {
final User user;
Authenticated({@required this.user});
@override
List<Object> get props => [user];
}
class AuthenticationFailure extends AuthenticationState {
final String message;
AuthenticationFailure({@required this.message});
@override
List<Object> get props => [message];
}
These are the authentication states for our app. We are going to make use of them later when we build the UI.
Now let’s create our authentication controller. Add authentication_controller.dart
to the authentication
folder and add the following code:
// features/authentication/authentication_controller.dart
class AuthenticationController extends GetxController {
final AuthenticationService _authenticationService;
final _authenticationStateStream = AuthenticationState().obs;
AuthenticationState get state => _authenticationStateStream.value;
AuthenticationController(this._authenticationService);
// Called immediately after the contoller is allocated in memory.
@override
void onInit() {
_getAuthenticatedUser();
super.onInit();
}
Future<void> signIn(String email, String password) async {
final user = await _authenticationService.signInWithEmailAndPassword(email, password);
_authenticationStateStream.value = Authenticated(user: user);
}
void signOut() async {
await _authenticationService.signOut();
_authenticationStateStream.value = UnAuthenticated();
}
void _getAuthenticatedUser() async {
_authenticationStateStream.value = AuthenticationLoading();
final user = await _authenticationService.getCurrentUser();
if (user == null) {
_authenticationStateStream.value = UnAuthenticated();
} else {
_authenticationStateStream.value = Authenticated(user: user);
}
}
}
The AuthenticationController
extends GetX
’s GetxController
class. This class is lifecycle aware and will get disposed once there is nothing depending on it. This class, as suggested in the name, is a controller that contains logic that will be used by our view in a classic MVC architecture.
As you may have noticed GetX has an extension function .obs
that turns any object T
into a stream – Rx<T>
. This is what we have done with our _authenticationStateStream
property. I am a huge fan of reactive programming so we are going to be using it in this app.
Login Feature
We are now going to implement our login feature. I’m not going to talk about login states, get the source code on GitHub if you want to look at it. Let’s add our login controller:
// features/authentication/login/login_controller.dart
class LoginController extends GetxController {
final AuthenticationController _authenticationController = Get.find();
final _loginStateStream = LoginState().obs;
LoginState get state => _loginStateStream.value;
void login(String email, String password) async {
_loginStateStream.value = LoginLoading();
try{
await _authenticationController.signIn(email, password);
_loginStateStream.value = LoginState();
} on AuthenticationException catch(e){
_loginStateStream.value = LoginFailure(error: e.message);
}
}
}
In this controller we are getting the AuthenticationController
using GetX
’s dependency injection framework:
final AuthenticationController _authenticationController = Get.find();
When we call Get.find<T>()
we will get the instance of the AuthenticationController
that’s in memory. Very cool.
Let’s turn our attention to the UI and create the login screen:
// features/authentication/login/login_page.dart
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login'),
),
body: SafeArea(
minimum: const EdgeInsets.all(16),
child: _SignInForm(),
));
}
}
class _SignInForm extends StatefulWidget {
@override
__SignInFormState createState() => __SignInFormState();
}
class __SignInFormState extends State<_SignInForm> {
final _controller = Get.put(LoginController()); // inject controller
final GlobalKey<FormState> _key = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _emailController = TextEditingController();
bool _autoValidate = false;
@override
Widget build(BuildContext context) {
return Obx((){
return Form(
key: _key,
autovalidateMode: _autoValidate ? AutovalidateMode.always : AutovalidateMode.disabled,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'Email address',
filled: true,
isDense: true,
),
controller: _emailController,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
validator: (value) {
if (value == null) {
return 'Email is required.';
}
return null;
},
),
SizedBox(
height: 12,
),
TextFormField(
decoration: InputDecoration(
labelText: 'Password',
filled: true,
isDense: true,
),
obscureText: true,
controller: _passwordController,
validator: (value) {
if (value == null) {
return 'Password is required.';
}
return null;
},
),
const SizedBox(
height: 16,
),
RaisedButton(
color: Theme.of(context).primaryColor,
textColor: Colors.white,
padding: const EdgeInsets.all(16),
shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(8.0)),
child: Text('LOG IN'),
onPressed: _controller.state is LoginLoading ? () {} : _onLoginButtonPressed,
),
const SizedBox(height: 20,),
if (_controller.state is LoginFailure)
Text((_controller.state as LoginFailure).error,
textAlign: TextAlign.center,
style: TextStyle(
color: Get.theme.errorColor // easy way to access theme
),
),
if (_controller.state is LoginLoading)
Center(child: CircularProgressIndicator(),)
],
),
),
);
});
}
_onLoginButtonPressed() {
if (_key.currentState.validate()) {
_controller.login(_emailController.text, _passwordController.text);
} else {
setState(() {
_autoValidate = true;
});
}
}
}
We are using Obx
, a simple reactive widget by GetX
to listen to the changes in the login state which is an Rx stream. This will enable us to render conditional widgets such as the progress indicator and the error text. I used the Obx
in this case because it is the simplest reactive widget there-is. For more GetX
widgets check out their documentation.
Putting Everything Together
Let’s turn our attention to the main.dart
file and add the following code:
void main() {
initialize();
runApp(MyApp());
}
void initialize() {
// inject authentication controller
Get.lazyPut(() => AuthenticationController(Get.put(FakeAuthenticationService())),);
}
class MyApp extends GetWidget<AuthenticationController> {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Fluter GetX Auth',
theme: ThemeData(
primarySwatch: Colors.purple,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
debugShowCheckedModeBanner: false,
home: Obx(() {
if (controller.state is UnAuthenticated) {
return LoginPage();
}
if (controller.state is Authenticated) {
return HomePage(
user: (controller.state as Authenticated).user,
);
}
return SplashScreen();
}),
);
}
}
As you can see MyApp
extends GetWidget<AuthenticationController>
. This widget gives us access to our AuthenticationController
without us having to call Get.find()
. The other thing to note is that instead of MaterialApp
we now have GetMaterialApp
widget which enables us to do cool things with GetX
.
Like in the login page, we are using Obx
for our home
property so we can listen to changes on the authentication state and display the respective widgets.
That’s it. A simple authentication flow using GetX
. The complete source code is available on GitHub if you want to check it out. Here is how it looks:
Thanks so much for taking time to read. I hope you have learned something .
Comments