tafaust
tafaust

Reputation: 1518

Flutter secure routes with a flutter-fire authentication guard and avoid unnecessary rebuilds

I currently face an issue where I route to an authentication guard view as my default route. My authentication guard view:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/user.dart';
import '../services/services.module.dart';
import '../widgets/common/async_stream.dart';
import 'landing_screen/landing_screen.dart';
import 'tabs_screen/tab_screen.dart';

/// The [ViewAuthGuard] decides whether to display the [LandingScreenView] or the [TabsScreenView].
class ViewAuthGuard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('ViewAuthGuard build called: $context');
    FirebaseAuthService authService = Provider.of<AuthService>(context, listen: false);

    return AsyncStreamWidget<User>(
      stream: authService.onAuthStateChanged,
      child: (User user) => TabsScreenView(),
      emptyWidget: LandingScreenView(),
      loadingWidget: null,
      errorWidget: null,
    );
  }
}

and my AsyncStreamWidget:

import 'package:flutter/material.dart';

import '../../../models/base_model.dart';
import '../../error/future_error.dart';
import '../../loading.dart';

class AsyncStreamWidget<T extends BaseModel> extends StatelessWidget {
  final Stream<T> stream;
  final T initialData;

  Widget _loading;
  Widget _empty;
  Widget Function(Object) _error;
  Widget Function(T) child;

  AsyncStreamWidget({
    @required this.stream,
    @required this.child,
    this.initialData,
    Widget loadingWidget,
    Widget emptyWidget,
    Widget Function(Object) errorWidget,
  }) {
    if (loadingWidget == null) {
      _loading = Loading();
    } else {
      _loading = loadingWidget;
    }

    if (errorWidget == null) {
      _error = (Object error) => FutureErrorWidget(error: error);
    } else {
      _error = errorWidget;
    }

    if (emptyWidget == null) {
      _empty = Center(child: Text('No data available.'));
    } else {
      _empty = emptyWidget;
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<T>(
      initialData: initialData,
      stream: stream,
      builder: (_, AsyncSnapshot<T> snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return _loading;
            break;
          case ConnectionState.active: // check if different behavior is needed for active and done
          case ConnectionState.done:
            // error state
            if (snapshot.hasError) {
              // todo more throughout error checking and specialized error widget
              return _error(snapshot.error);
            }

            // data state
            if (snapshot.hasData) {
              T data = snapshot.data;
              return child(data);
            }

            // empty state
            return _empty;
          case ConnectionState.none:
          default:
            print('E: Received Future [$stream] was null or else.');
            return _error('Unknown error.');
        }
      },
    );
  }
}

The FirebaseAuthService wraps the auth.FirebaseAuth.instance. My stream is constructed as follows:

User _userFromFirebase(auth.User user) {
  if (user == null) {
    return null;
  }
  return User(
    uid: user.uid,
    email: user.email,
    displayName: user.displayName,
    photoUrl: user.photoURL,
  );
}

@override
Stream<User> get onAuthStateChanged => _firebaseAuth.authStateChanges().map(_userFromFirebase);

I currently provide all my services above the ViewAuthGuard.

I wrapped my Material app with a ThemeProvider ChangeNotifier (in case that could be an issue).

My issue is that all widgets below the ViewAuthGuard are rebuild and their state is reset. This occurs to me while developing. When a hot reload occurs, all the children are rebuild. The TabsScreenView contains the initial navigation for my flutter app and always reset to index zero during development.

Question: How do I avoid the unnecessary reloads at this point?

What I tested so far:

Please drop me a comment if you need more information, code, console prints or else to support me. Thank you!

Upvotes: 0

Views: 1067

Answers (1)

tafaust
tafaust

Reputation: 1518

I was able to fix it myself. For anyone interested, this was the process:

The stream is the pivot element which decides if the AsyncStreamWidget rebuilds its child or not. So I did check if the stream changed across hot reloads by printing its hashCode attribute (and yes, it did change).

Subsequently, I did change my ViewAuthGuard to a StatefulWidget and used the didChangeDependencies method to store the stream in the state.

(I also started instantiating the widgets in the initState method and store them in the state as well so that they are created only once.)


EDIT And I should mention that my ViewAuthGuard has no dependencies. So it is not rebuilt due to a change of dependencies.

Upvotes: 0

Related Questions