Firebase authentication
Learning objectives
- You know how to use Firebase Authentication.
Presently, anyone opening the application can see all the notes and can add and delete notes. Let's modify the application so that notes are personal -- that is, users can see, add, and delete only their own notes. To achieve this, we'll use Firebase Authentication.
Starting with Firebase authentication
To get started with Firebase authentication, follow the Get Started with Firebase Authentication on Flutter guide at https://firebase.google.com/docs/auth/flutter/start. To summarize, you need to:
- Add the
firebase_auth
package to the project by runningflutter pub add firebase_auth
. After this, the dependencies part of thepubspec.yaml
file should be as follows:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_riverpod: ^2.4.0
riverpod: ^2.4.0
firebase_core: ^2.9.0
cloud_firestore: ^4.5.1
firebase_auth: ^4.4.1
- Enable authentication in the [Firebase console]https://console.firebase.google.com/) by clicking your project, choosing (or searching) authentication, and selecting a Sign-in-method. Here, we'll use anonymous authentication, so we'll select Anonymous, and enable it. Once selected, the sign-in method in the Authentication part of the console for the project should look similar to Figure 1 below.

Creating a user provider
Next, let's create a user provider that is used to provide access to the user. As the changes to the user are received from Firebase, this is a great moment to try out a StreamProvider. The StreamProvider
is a provider that listens to a stream and updates its value when the stream emits a new value.
Create a file called user_provider.dart
in the folder providers
under lib
and place the following contents to the file.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final userProvider = StreamProvider<User?>((ref) {
return FirebaseAuth.instance.authStateChanges();
});
The FirebaseAuth
class provides access to the Firebase Authentication service, and the authStateChanges
method returns a stream that provides information on the changes to the current user. We do not need to create the User
model, as it is included in the firebase_auth
package.
Login screen
Let's next create a login screen that can be used for authentication. For now, we are using anonymous authentication, which is done by calling the asynchronous FirebaseAuth.instance.signInAnonymously()
method. The login screen will simply show a button that, when pressed, calls the method.
Create a file called login_screen.dart
in the folder screens
under lib
and place the following contents to the file.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class LoginScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.login),
label: const Text('Login anonymously'),
onPressed: () async {
await FirebaseAuth.instance.signInAnonymously();
},
),
],
),
),
);
}
}
Adjusting notes app
Let's next adjust our NotesApp
in main.dart
to account for whether the user has logged in. Watching for the user provider provides access to an instance of AsyncValue that contains the user and allows defining three methods: (1) functionality for when there is data, (2) functionality for the case of an error, and (3) functionality when the data is still loading. In our case, we wish to show the login screen when there is data but the user is null, while otherwise -- when the user is not null -- we'll show the notes screen.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'firebase_options.dart';
import 'providers/user_provider.dart';
import 'screens/login_screen.dart';
import 'screens/note_screen.dart';
void main() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(ProviderScope(child: NotesApp()));
}
class NotesApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final asyncUser = ref.watch(userProvider);
return MaterialApp(
title: 'My notes',
home: asyncUser.when(
data: (user) {
return user == null ? LoginScreen() : NoteScreen();
},
error: (error, stackTrace) {
return const Center(child: Text("Something went wrong.."));
},
loading: () {
return const Center(child: Text("Loading..."));
},
),
);
}
}
Now, when we open up the application, the application looks similar to Figure 2 below.

When we press the login button, there is a brief delay when the anonymous authentication is conducted, after which the application looks similar to Figure 3 below -- that is, now we see the notes screen with the existing notes.

Accounting for user in notes
Currently, the created notes are not related to the specific user, but are still generic and available for all. Let's next adjust the application so that users can see their own notes.
Adjusting user model
First, we'll add a field to the Note
that contains the user id. Adjust the note.dart
to match the following.
class Note {
final String id;
final String content;
final String userId;
Note({required this.id, required this.content, required this.userId});
factory Note.fromFirestore(Map<String, dynamic> data, String id) {
return Note(
id: id,
content: data['content'],
userId: data['userId'],
);
}
Map<String, dynamic> toFirestore() {
return {
'content': content,
'userId': userId,
};
}
}
Adjusting the note provider
Next, we'll adjust the note provider so that it retrieves only the notes that belong to the current user. For this, we need to add the user id to the note provider and adjust the queries that are used to retrieve data from the database and to add data to the database. In addition, we also need to adjust the creation of the note provider to account for the user provider.
Adjust the note_provider.dart
to match the following.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/note.dart';
import '../providers/user_provider.dart';
class NoteNotifier extends StateNotifier<List<Note>> {
final String userId;
NoteNotifier({required this.userId}) : super([]) {
_fetchNotes();
}
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
void _fetchNotes() async {
if (userId == '') {
return;
}
final snapshot = await _firestore
.collection('notes')
.where('userId', isEqualTo: userId)
.get();
final notes = snapshot.docs.map((doc) {
return Note.fromFirestore(doc.data(), doc.id);
}).toList();
state = notes;
}
void addNote(String content) async {
if (userId == '') {
return;
}
final noteData =
Note(id: '', content: content, userId: userId).toFirestore();
final noteRef = await _firestore.collection('notes').add(noteData);
final note = Note.fromFirestore(noteData, noteRef.id);
state = [...state, note];
}
void deleteNote(String id) async {
await _firestore.collection('notes').doc(id).delete();
state = state.where((note) => note.id != id).toList();
}
}
final noteProvider = StateNotifierProvider<NoteNotifier, List<Note>>((ref) {
final asyncUser = ref.watch(userProvider);
return asyncUser.when(data: (user) {
return NoteNotifier(userId: user!.uid);
}, loading: () {
return NoteNotifier(userId: '');
}, error: (error, stackTrace) {
return NoteNotifier(userId: '');
});
});
Now, when you try out the application, you'll see that the notes are now specific to the user. When testing the application out in a browser, you can verify this by opening the application in incognito mode. Figure 4 shows two instances of the application side by side, where one of the instances is running in a browser window with incognito mode while the other is not.
