Flutter Null property in viewmodel using provider and get_it

Viewed 594

Im developing a user authentication flow in flutter using firebase auth.

I have the basis of the app which allows user login, home, password reset however i'm stuck while developing a profile screen for the user. My intention is to allow the user to amend a pre-populated form to update their details.

To keep the architecture as clean as as possible so i have a view and a viewmodel where i can then connect to the services (authentication and firestore).

To facilitate the state management im using both Get_it and Provider. Get_it so i can easily access the classes/viewmodels and provider so i can update the view should the data change.

The problem im experiencing is that the user property of the ProfileViewModel is null when i initially open the profile view despite it being populated from the authentication service on login, which in turn causes an error when i attempt to set the initial values of the text boxes in the form. If i press back from the error and try again then everything loads as expected.

From my perspective the authentication model should contain a valid User object as the user is logged in. Calling model.loadData() on the profile initState view should return the viewmodel that contains the current user object.

The user login works just fine however this uses provider and not a service locator (Get_it) to get the user object. I've included most of the code below for completeness.

Thanks in advance

My service locator

GetIt serviceLocator = GetIt.instance;

void setupServiceLocator() {
  // services
  serviceLocator.registerLazySingleton<AuthenticationService>(
      () => AuthenticationService());

  serviceLocator
      .registerLazySingleton<FirestoreDatabase>(() => FirestoreDatabase());

  // view models
  serviceLocator.registerFactory<ProfileViewModel>(() => ProfileViewModel());
  serviceLocator
      .registerFactory<PasswordResetViewModel>(() => PasswordResetViewModel());
}

Profile View

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({Key key}) : super(key: key);

  @override
  _ProfileScreenState createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  final _formKey = GlobalKey<FormState>();
  bool checkBoxValue = false;
  double _height;
  double _width;
  double _pixelRatio;
  bool _large;
  bool _medium;

  String displayName;
  String email;
  String phone;
  String url;

  ProfileViewModel model = serviceLocator<ProfileViewModel>();

  @override
  void initState() {
    super.initState();
    model.loadData();
  }

  @override
  Widget build(BuildContext context) {
    _height = MediaQuery.of(context).size.height;
    _width = MediaQuery.of(context).size.width;
    _pixelRatio = MediaQuery.of(context).devicePixelRatio;
    _large = ResponsiveWidget.isScreenLarge(_width, _pixelRatio);
    _medium = ResponsiveWidget.isScreenMedium(_width, _pixelRatio);

    return Material(
      child: Scaffold(
        body: Container(
          height: _height,
          width: _width,
          margin: EdgeInsets.only(bottom: 5),
          child: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                form(),
                button(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget form() {
    return Container(
      margin: EdgeInsets.only(
          left: _width / 12.0, right: _width / 12.0, top: _height / 20.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            displayNameTextFormField()
          ],
        ),
      ),
    );
  }

  Widget displayNameTextFormField() {
    return CustomTextField(
      keyboardType: TextInputType.text,
      icon: Icons.person,
      hint: "Display Name",
      initalValue: model.user.displayName,
      onSave: (value) {
        setState(() {
          displayName = value;
        });
      },
    );
  }

  Widget button() {
    return ElevatedButton(
      onPressed: () {
        if (_formKey.currentState.validate()) {
          //submit the form
          _formKey.currentState.save();

          model.saveProfile(email, displayName, url, phone);

          Navigator.of(context).pop();
        }
      },
      child: Container(
        alignment: Alignment.center,
        //height: _height / 20,
        width: _large ? _width / 4 : (_medium ? _width / 3.75 : _width / 3.5),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.all(Radius.circular(20.0)),
          gradient: LinearGradient(
            colors: <Color>[Colors.orange[200], Colors.pinkAccent],
          ),
        ),
        padding: const EdgeInsets.all(12.0),
        child: Text(
          'SIGN UP',
          style: TextStyle(fontSize: _large ? 14 : (_medium ? 12 : 10)),
        ),
      ),
    );
  }
}

Profile View Model

class ProfileViewModel extends ChangeNotifier {
  // final firestoreService = serviceLocator<FirestoreDatabase>();
  final authenticationService = serviceLocator<AuthenticationService>();

  User _user;
  User get user => _user;

  void loadData() {
    _user = authenticationService.currentUser;
    notifyListeners();
  }

  Future<void> saveProfile(email, displayName, url, phoneNumber) async {
    _user = await authenticationService.updateUserDetails(
        email, displayName, url, phoneNumber);
    notifyListeners();
  }
}

Authentication Service

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

enum Status {
  Uninitialized,
  Authenticated,
  Authenticating,
  Unauthenticated,
  Registering
}

class AuthenticationService extends ChangeNotifier {
  //Firebase Auth object
  FirebaseAuth _auth;

  //Default status
  Status _status = Status.Uninitialized;

  Status get status => _status;

  User _currentUser;
  User get currentUser => _currentUser;

  Stream<User> get user => _auth.authStateChanges().map(_userFromFirebase);

  AuthenticationService() {
    //initialise object
    _auth = FirebaseAuth.instance;

    //listener for authentication changes such as user sign in and sign out
    _auth.authStateChanges().listen(onAuthStateChanged);
  }

  //Create user object based on the given FirebaseUser
  User _userFromFirebase(User user) {
    if (user == null) {
      return null;
    }
    _currentUser = user;
    return user;
  }

  //Method to detect live auth changes such as user sign in and sign out
  Future<void> onAuthStateChanged(User firebaseUser) async {
    if (firebaseUser == null) {
      _status = Status.Unauthenticated;
    } else {
      _userFromFirebase(firebaseUser);
      _status = Status.Authenticated;
    }
    notifyListeners();
  }

  //Method for new user registration using email and password
  Future<User> registerWithEmailAndPassword(
      String email, String password, String displayName) async {
    try {
      _status = Status.Registering;
      notifyListeners();
      final UserCredential result = await _auth.createUserWithEmailAndPassword(
          email: email, password: password);

      var userFromFirebase = _userFromFirebase(result.user);

      await userFromFirebase.updateProfile(
          displayName: displayName, photoURL: null);

      return userFromFirebase;
    } catch (e) {
      print("Error on the new user registration = " + e.toString());
      _status = Status.Unauthenticated;
      notifyListeners();
      return null;
    }
  }

  //Method to handle user sign in using email and password
  Future<bool> signInWithEmailAndPassword(String email, String password) async {
    try {
      _status = Status.Authenticating;
      notifyListeners();
      await _auth.signInWithEmailAndPassword(email: email, password: password);

      _currentUser = FirebaseAuth.instance.currentUser;
      return true;
    } catch (e) {
      print("Error on the sign in = " + e.toString());
      _status = Status.Unauthenticated;
      notifyListeners();
      return false;
    }
  }

  //Method to handle password reset email
  Future<void> sendPasswordResetEmail(String email) async {
    await _auth.sendPasswordResetEmail(email: email);
  }

  //update the user details
  Future<User> updateUserDetails(
      String email, String displayName, String url, String phoneNumber) async {
    await currentUser.updateProfile(displayName: displayName, photoURL: url);
    if (currentUser.email != email) {
      await currentUser.updateEmail(email);
    }
    if (currentUser.phoneNumber != phoneNumber) {}

    _currentUser = FirebaseAuth.instance.currentUser;

    return currentUser;
  }

  //Method to handle user signing out
  Future signOut() async {
    _auth.signOut();
    _status = Status.Unauthenticated;
    notifyListeners();
    return Future.delayed(Duration.zero);
  }
}

Main action

Future<void> main() async {
  setupServiceLocator();
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(MainWidget());
}

class MainWidget extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AuthenticationService>(
          create: (context) => AuthenticationService(),
        ),
      ],
      child: AuthenticationWrapper(
        databaseBuilder: (_, user) => FirestoreDatabase(user: user),
      ),
    );
  }
}

Auth Wrapper

class AuthenticationWrapper extends StatelessWidget {
  const AuthenticationWrapper({Key key, this.databaseBuilder})
      : super(key: key);

  // Expose builders for 3rd party services at the root of the widget tree
  // This is useful when mocking services while testing
  final FirestoreDatabase Function(BuildContext context, User user)
      databaseBuilder;

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return AuthWidgetBuilder(
      databaseBuilder: databaseBuilder,
      builder: (BuildContext context, AsyncSnapshot<User> userSnapshot) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'My Docs App',
          routes: {
            // When navigating to the "/second" route, build the SecondScreen widget.
            '/signup': (context) => SignUpScreen(),
            '/profile': (context) => ProfileScreen(),
            '/passwordreset': (context) => PasswordResetScreen(),
          },
          home: SafeArea(
            child: Consumer<AuthenticationService>(
              builder: (_, authProviderRef, __) {
                if (userSnapshot.connectionState == ConnectionState.active) {
                  if (userSnapshot.hasData) {
                    // if (!userSnapshot.data.emailVerified) {
                    //   return VerifyEmailPage();
                    // }
                    return HomeScreen();
                  } else {
                    return SignInScreen();
                  }
                }
                return Material(
                  child: CircularProgressIndicator(),
                );
              },
            ),
          ),
        );
      },
    );
  }
}

Auth builder

class AuthWidgetBuilder extends StatelessWidget {
  const AuthWidgetBuilder(
      {Key key, @required this.builder, @required this.databaseBuilder})
      : super(key: key);
  final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
  final FirestoreDatabase Function(BuildContext context, User user)
      databaseBuilder;

  @override
  Widget build(BuildContext context) {
    final authService =
        Provider.of<AuthenticationService>(context, listen: false);
    return StreamBuilder<User>(
      stream: authService.user,
      builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
        final User user = snapshot.data;
        if (user != null) {
          /*
          * For any other Provider services that rely on user data can be
          * added to the following MultiProvider list.
          * Once a user has been detected, a re-build will be initiated.
           */
          return MultiProvider(
            providers: [
              Provider<User>.value(value: user),
              Provider<FirestoreDatabase>(
                create: (context) => databaseBuilder(context, user),
              ),
            ],
            child: builder(context, snapshot),
          );
        }
        return builder(context, snapshot);
      },
    );
  }
}
0 Answers
Related