Courses

[NEW] Flutter 3 Mobile App with Laravel 12 API

Adding Registration Functionality with API Usage

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:

  1. Change from a Stateless to a Stateful widget
  2. Add Global FormKey and TextEditingController for each field
  3. Add property to store error messages
  4. Add a function to handle registration
  5. Wrap our Widgets with Form
  6. Add all the fields and cancel button (to return to the login screen)
  7. 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:

  1. To store the user's authentication state - whether the user is logged in or not
  2. 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.

Previous: Creating Categories
avatar
Luis Antonio Parrado

Modifications in lib/main.dart doesn't match with commit. It's missing the part when we apply AuthProvider and redirect to new route home.

avatar

Thank you for letting me know! I've updated the code to be correct

avatar
You can use Markdown
avatar
You can use Markdown