Skip to content

🐛 [Auth, Firestore, Storage, ...] Event Listeners are Not Properly Disposed on Web Hot Restart across all plugins. #7064

@rayliverified

Description

@rayliverified

Bug report

Describe the bug
Listeners to AuthStateChanges are not released on web hot restart, resulting in multiple listeners. These listeners cannot be cleared by the developer.

Steps to reproduce

  1. Run the following code in Flutter Web (replacing the test username and password)
import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Firebase Auth Test', home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { late FirebaseAuth firebaseAuth; late Stream<String?> firebaseAuthUserStream; late StreamSubscription firebaseAuthListener; @override void initState() { super.initState(); firebaseAuth = FirebaseAuth.instance; firebaseAuthUserStream = FirebaseAuth.instance .authStateChanges() .map((event) => event?.uid) .asBroadcastStream(); firebaseAuthListener = firebaseAuthUserStream.listen((uid) { print('AuthStateChanges: $uid'); }); } @override void dispose() { firebaseAuthListener.cancel(); super.dispose(); } void initFirebase() async { firebaseAuth.signInWithEmailAndPassword( email: 'test@testuser.com', password: '12345678'); } @override Widget build(BuildContext parentContext) { print('Build Home'); return Scaffold( body: SafeArea( child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ StreamBuilder<String?>( initialData: null, stream: firebaseAuthUserStream, builder: (context, snapshot) { print('Build Stream Builder'); if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } if (snapshot.hasData) { print(snapshot.data); print('Rebuild Stream'); return Text('User: ${snapshot.data!}'); } return Text('No data'); }), TextButton( onPressed: initFirebase, child: Text('Login'), ), ], ), ), ), ); } } 
  1. Press hot restart.
  2. Press hot restart a few more times.
  3. Press the Login button.
  4. Observe that the listener is duplicated the number of times that you hot restarted.

Expected behavior

Listeners are released and not duplicated. Web behavior should be the same as on mobile and desktop.

Sample project

This issue is likely specific to FlutterFire as replacing the auth listener with a regular listener does not result in duplicated listeners.
** Regular Listener Example **

import 'dart:async'; import 'package:flutter/material.dart'; void main() async { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Listener Web Reload Test', home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { StreamController<String?> streamController = StreamController.broadcast(); late StreamSubscription streamSubscription; @override void initState() { super.initState(); streamSubscription = streamController.stream.listen((value) { print('Update Change: $value'); }); } @override void dispose() { streamSubscription.cancel(); super.dispose(); } void updateValue() async { streamController.add(UniqueKey().toString()); } @override Widget build(BuildContext parentContext) { print('Build Home'); return Scaffold( body: SafeArea( child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ StreamBuilder<String?>( initialData: null, stream: streamController.stream, builder: (context, snapshot) { print('Build Stream Builder'); if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } if (snapshot.hasData) { print(snapshot.data); print('Rebuild Stream'); return Text('Value: ${snapshot.data!}'); } return Text('No data'); }), TextButton( onPressed: updateValue, child: Text('Update Value'), ), ], ), ), ), ); } } 

Additional context

Add any other context about the problem here.


Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand
[√] Flutter (Channel stable, 2.5.1, on Microsoft Windows [Version 6.1.7601], locale en-US) [!] Android toolchain - develop for Android devices (Android SDK version 30.0.2) X cmdline-tools component is missing Run `path/to/sdkmanager --install "cmdline-tools;latest"` See https://developer.android.com/studio/command-line for more details. X Android license status unknown. Run `flutter doctor --android-licenses` to accept the SDK licenses. See https://flutter.dev/docs/get-started/install/windows#android-setup for more details. [√] Chrome - develop for the web [√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.9.0) [√] Android Studio (version 2020.3) [√] VS Code, 64-bit edition (version 1.60.1) [√] Connected device (3 available) 

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand
 firebase_core: ^1.6.0 firebase_auth: ^3.1.1 google_sign_in: ^5.1.0 cloud_firestore: ^2.5.3 firebase: ^9.0.1 firebase_storage: ^10.0.3 cloud_functions: ^3.0.3 

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions