In this lesson, we will focus on adding a registration process. This includes a few things:
- Creating a Home Screen
- Creating a Registration API function
- Adding Registration Fields to the UI
- Creating an Auth Provider to handle authentication management
- Modifying our
main.dart
file to react to the authentication state
Here's how the registration screen will look like at the end:
Creating Home Screen
Let's start by creating a Home screen, where our user will be redirected after successful registration:
lib/screens/home.dart
import 'package:flutter/material.dart'; class Home extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Logged in!'), ), body: Center( child: Text('Welcome', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), ), ), ); }}
For now, we will keep it simple. We will add more features to this screen later.
Creating Registration API Function
Next, we will need our API service to call the registration endpoint. Let's create a new function in our ApiService
class:
lib/services/api.dart
// ... Future<String> register(String name, String email, String password, String password_confirm, String device_name) async { String url = '$baseUrl/api/auth/register'; final http.Response response = await http.post( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', 'Accept': 'application/json', }, body: jsonEncode(<String, String>{ 'name': name, 'email': email, 'password': password, 'password_confirmation': password_confirm, 'device_name': device_name, }), ); if (response.statusCode == 422) { final Map<String, dynamic> data = json.decode(response.body); final Map<String, dynamic> errors = data['errors']; String message = ''; errors.forEach((key, value) { value.forEach((error) { message += '$error\n'; }); }); throw Exception(message); } return response.body; }
Expanding Registration Form
Then, it's time to expand our registration form. Here, we have a few things to do:
- Change from a Stateless to a Stateful widget
- Add Global FormKey and TextEditingController for each field
- Add property to store error messages
- Add a function to handle registration
- Wrap our Widgets with Form
- Add all the fields and cancel button (to return to the login screen)
- Add error messages to the UI
So let's do that one by one in our lib/screens/register.dart
file:
1. Change from Stateless to Stateful widget
// ... class Register extends StatelessWidget {class Register extends StatefulWidget { const Register({super.key}); @override RegisterState createState() => RegisterState(); } class RegisterState extends State<Register> {// ...
2. Add Global FormKey and TextEditingController for each field
// ... class RegisterState extends State<Register> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final confirmPasswordController = TextEditingController(); // ...
3. Add property to store error messages
class RegisterState extends State<Register> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final confirmPasswordController = TextEditingController(); String errorMessage = '';
4. Add a function to handle registration
import 'package:laravel_api_flutter_app/providers/auth_provider.dart';import 'package:provider/provider.dart'; // ... class RegisterState extends State<Register> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final confirmPasswordController = TextEditingController(); String errorMessage = ''; Future<void> submit() async { final form = _formKey.currentState; if (!form!.validate()) { return; } final AuthProvider provider = Provider.of<AuthProvider>(context, listen: false); try { String token = await provider.register( nameController.text, emailController.text, passwordController.text, confirmPasswordController.text, 'Some device name'); Navigator.pop(context); } catch (Exception) { setState(() { errorMessage = Exception.toString().replaceAll('Exception: ', ''); }); } } // ...
5. Wrap our Widgets with Form
// ... Card( elevation: 0, margin: EdgeInsets.only(left: 20, right: 20), child: Padding( padding: EdgeInsets.all(20.0), child: Form( key: _formKey, child: Column( children: <Widget>[ TextField( keyboardType: TextInputType.name, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Name', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.emailAddress, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Confirm Password', ), ), SizedBox(height: 20), // Acts as a spacer ElevatedButton( onPressed: () { Navigator.pushNamed(context, '/categories'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.purple, foregroundColor: Colors.white, minimumSize: Size(double.infinity, 40), ), child: Text('Register'), ), Padding( padding: EdgeInsets.only( top: 20), // Different way to add padding child: InkWell( child: Text('<- Back to Login'), onTap: () => Navigator.pop(context)), ) ], ), ), ))], // ...
6. Add all the fields and cancel button (to return to the login screen)
children: <Widget>[ TextField( keyboardType: TextInputType.name, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Name', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.emailAddress, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ), SizedBox(height: 20), // Acts as a spacer TextField( keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Confirm Password', ), ), SizedBox(height: 20), // Acts as a spacer ElevatedButton( onPressed: () { Navigator.pushNamed(context, '/categories'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.purple, foregroundColor: Colors.white, minimumSize: Size(double.infinity, 40), ), child: Text('Register'), ), Padding( padding: EdgeInsets.only( top: 20), // Different way to add padding child: InkWell( child: Text('<- Back to Login'), onTap: () => Navigator.pop(context)), ) ], children: <Widget>[ TextFormField( keyboardType: TextInputType.name, controller: nameController, validator: (String? value) { if (value!.isEmpty) { return 'Name is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Name', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.emailAddress, controller: emailController, validator: (String? value) { if (value!.isEmpty) { return 'Email is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.visiblePassword, controller: passwordController, obscureText: true, autocorrect: false, enableSuggestions: false, validator: (String? value) { if (value!.isEmpty) { return 'Password is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.visiblePassword, controller: confirmPasswordController, obscureText: true, autocorrect: false, enableSuggestions: false, validator: (String? value) { if (value!.isEmpty) { return 'Confirm Password is required'; } if (value != passwordController.text) { return 'Passwords do not match'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Confirm Password', ), ), SizedBox(height: 20), // Acts as a spacer ElevatedButton( onPressed: () => submit(), style: ElevatedButton.styleFrom( backgroundColor: Colors.purple, foregroundColor: Colors.white, minimumSize: Size(double.infinity, 40), ), child: Text('Register'), ), Text(errorMessage, style: TextStyle(color: Colors.red)), Padding( padding: EdgeInsets.only(top: 20), // Different way to add padding child: InkWell( child: Text('<- Back to Login'), onTap: () => Navigator.pop(context)), )],
That's it! Our page is complete. Here's a full code for the lib/screens/register.dart
file:
import 'package:flutter/material.dart';import 'package:laravel_api_flutter_app/providers/auth_provider.dart';import 'package:provider/provider.dart'; class Register extends StatefulWidget { const Register({super.key}); @override RegisterState createState() => RegisterState();} class RegisterState extends State<Register> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final confirmPasswordController = TextEditingController(); String errorMessage = ''; Future<void> submit() async { final form = _formKey.currentState; if (!form!.validate()) { return; } final AuthProvider provider = Provider.of<AuthProvider>(context, listen: false); try { String token = await provider.register( nameController.text, emailController.text, passwordController.text, confirmPasswordController.text, 'Some device name'); Navigator.pop(context); } catch (Exception) { setState(() { errorMessage = Exception.toString().replaceAll('Exception: ', ''); }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Register'), ), body: Container( color: Theme.of(context).primaryColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Card( elevation: 0, margin: EdgeInsets.only(left: 20, right: 20), child: Padding( padding: EdgeInsets.all(20.0), child: Form( key: _formKey, child: Column( children: <Widget>[ TextFormField( keyboardType: TextInputType.name, controller: nameController, validator: (String? value) { if (value!.isEmpty) { return 'Name is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Name', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.emailAddress, controller: emailController, validator: (String? value) { if (value!.isEmpty) { return 'Email is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.visiblePassword, controller: passwordController, obscureText: true, autocorrect: false, enableSuggestions: false, validator: (String? value) { if (value!.isEmpty) { return 'Password is required'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ), SizedBox(height: 20), // Acts as a spacer TextFormField( keyboardType: TextInputType.visiblePassword, controller: confirmPasswordController, obscureText: true, autocorrect: false, enableSuggestions: false, validator: (String? value) { if (value!.isEmpty) { return 'Confirm Password is required'; } if (value != passwordController.text) { return 'Passwords do not match'; } return null; }, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Confirm Password', ), ), SizedBox(height: 20), // Acts as a spacer ElevatedButton( onPressed: () => submit(), style: ElevatedButton.styleFrom( backgroundColor: Colors.purple, foregroundColor: Colors.white, minimumSize: Size(double.infinity, 40), ), child: Text('Register'), ), Text(errorMessage, style: TextStyle(color: Colors.red)), Padding( padding: EdgeInsets.only(top: 20), // Different way to add padding child: InkWell( child: Text('<- Back to Login'), onTap: () => Navigator.pop(context)), ) ], ), ), ), ) ], ))); }}
Creating Auth Provider
We need to create an Auth Provider for a few reasons:
- To store the user's authentication state - whether the user is logged in or not
- To store the API token that we will get after successful login/registration
And of course, this is not limited to these two reasons. We could store all the user's information for later use, such as name, email, etc. For now, we will keep it really simple:
lib/providers/auth_provider.dart
import 'package:flutter/material.dart';import 'package:laravel_api_flutter_app/services/api.dart'; class AuthProvider extends ChangeNotifier { bool isAuthenticated = false; ApiService apiService = ApiService(); AuthProvider(); Future<String> register(String name, String email, String password, String password_confirmation, String device_name) async { String token = await apiService.register( name, email, password, password_confirmation, device_name); isAuthenticated = true; notifyListeners(); return token; }}
Making Our Application Reactive to Authentication State
For our last task to make this work - we need to wrap our main.dart
in a Provider. This is required to gain access to AuthProvider
in our application:
lib/main.dart
import 'package:laravel_api_flutter_app/screens/home.dart';import 'package:laravel_api_flutter_app/providers/auth_provider.dart'; // ... return ChangeNotifierProvider( create: (context) => AuthProvider(), child: Consumer<AuthProvider>(builder: (context, authProvider, child) { return MultiProvider( providers: [ ChangeNotifierProvider<CategoryProvider>( create: (context) => CategoryProvider()), ], child: MaterialApp( title: 'Welcome to Flutter', home: Login(), routes: { '/login': (context) => Login(), '/register': (context) => Register(), '/categories': (context) => CategoriesList(), }, )); }));
Now, we can load our application and test the registration process. If everything is done correctly, you should be able to register a new user and be redirected to the Home screen:
The next lesson will add the Login functionality with basic Device information collection.
Check out the GitHub Commit for this lesson.
Modifications in
lib/main.dart
doesn't match with commit. It's missing the part when we applyAuthProvider
and redirect to new routehome
.Thank you for letting me know! I've updated the code to be correct