Learn how to implement user authentication in your Flutter Notes App using Firebase Authentication and Firestore, ensuring secure and private access to user-specific notes.
In this section, we will delve into implementing user authentication for your Notes App using Firebase. Authentication is crucial for ensuring that each user’s notes are secure and private. By the end of this chapter, you will have a fully functional authentication system that allows users to sign up, log in, and manage their notes securely.
User authentication is a fundamental aspect of modern applications, providing a secure way to manage user access and data. In this project, we will use Firebase Authentication to handle user sign-up and login processes. Additionally, we will integrate Firestore to store notes specific to authenticated users, ensuring that each user’s data is isolated and secure.
Before diving into the implementation, ensure that your Firebase project is set up and integrated with your Flutter app. If you haven’t done this yet, refer to the earlier section on setting up Firebase.
Add Firebase Authentication Package: First, include the Firebase Authentication package in your pubspec.yaml
file.
dependencies:
flutter:
sdk: flutter
firebase_auth: ^3.1.6
cloud_firestore: ^3.1.6
Initialize Firebase: Ensure Firebase is initialized in your main.dart
file.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
The first step in implementing authentication is to create the user interface for sign-up and login forms. These forms will allow users to enter their credentials and access their notes.
Create a simple sign-up form where users can enter their email and password to create an account.
class SignUpScreen extends StatefulWidget {
@override
_SignUpScreenState createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final AuthService _authService = AuthService();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sign Up')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
ElevatedButton(
onPressed: () async {
User? user = await _authService.signUp(
_emailController.text,
_passwordController.text,
);
if (user != null) {
// Navigate to the notes screen
}
},
child: Text('Sign Up'),
),
],
),
),
);
}
}
Similarly, create a login form for existing users to access their accounts.
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final AuthService _authService = AuthService();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
ElevatedButton(
onPressed: () async {
User? user = await _authService.signIn(
_emailController.text,
_passwordController.text,
);
if (user != null) {
// Navigate to the notes screen
}
},
child: Text('Login'),
),
],
),
),
);
}
}
With the UI in place, the next step is to implement the logic for handling user authentication using Firebase.
Create an AuthService
class to encapsulate the authentication logic. This class will handle sign-up, login, and sign-out operations.
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// Sign up with email and password
Future<User?> signUp(String email, String password) async {
try {
UserCredential result = await _auth.createUserWithEmailAndPassword(
email: email, password: password);
return result.user;
} catch (e) {
print(e);
return null;
}
}
// Sign in with email and password
Future<User?> signIn(String email, String password) async {
try {
UserCredential result = await _auth.signInWithEmailAndPassword(
email: email, password: password);
return result.user;
} catch (e) {
print(e);
return null;
}
}
// Sign out
Future<void> signOut() async {
await _auth.signOut();
}
// Auth state changes
Stream<User?> get user {
return _auth.authStateChanges();
}
}
Handling user sessions and authentication states is crucial for providing a seamless user experience. You can use the authStateChanges
stream to listen for changes in the authentication state and update the UI accordingly.
In your main widget, listen to the authentication state to determine whether to show the login screen or the notes screen.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: AuthService().user,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
User? user = snapshot.data;
if (user == null) {
return MaterialApp(home: LoginScreen());
}
return MaterialApp(home: NotesScreen());
} else {
return CircularProgressIndicator();
}
},
);
}
}
To ensure that notes are user-specific, store each user’s notes in a sub-collection within their user document in Firestore.
When a user adds a note, save it under their user ID in Firestore.
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> addUserNote(String userId, Note note) async {
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('notes')
.add(note.toMap());
}
Fetch notes for the authenticated user by querying their specific sub-collection.
Stream<List<Note>> getUserNotes(String userId) {
return FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('notes')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => Note.fromMap(doc.id, doc.data()))
.toList());
}
Let’s put everything together in a practical example. We’ll create a simple notes app where users can sign up, log in, and manage their notes securely.
Here’s a visual representation of the authentication flow in your app:
flowchart TD A[User] --> B[Sign Up/Login] B --> C[Firebase Authentication] C --> D[Authenticated User] D --> E[Access Personal Notes] E --> F[Firestore Sub-collection: User's Notes]
By implementing user authentication in your Notes App, you’ve taken a significant step towards creating a secure and user-friendly application. This foundation can be expanded with additional features such as password reset, social login, and more.