This is by far the simplest one.
It takes in an AuthenticationService
as the constructor parameter.
AuthenticationService
has multiple methods that one usually needs when creating accounts and signing users in.
For example, in our case, we need the signInWithEmailAndPassword()
method, which delegates straight to a method with the same name in FirebaseAuth.instance
.
It also takes an InAppNotificationDispatcher
as its second constructor parameter.
InAppNotificationDispatcher is a top-level service-like class that displays in-app notifications, kinda like Toasts, as Flutter widgets.
We need it for displaying error messages - that’s also the responsibility of LoginModel.
Super dumb widgets, remember?
Again, the reason for not using FirebaseAuth
in LoginModel
directly is that if we were ever to change from Firebase Authentication to something else, we would only need to make changes in AuthenticationService
.
Since AuthenticationService
is the only place where we actually make the magic happen, calling the Firebase Auth functions, no other class or file or part in our code needs to change.
This means that LoginModel
and all related tests continue to work even if we were to change to a different authentication implementation.
Our LoginModel
has two fields, which unsurprisingly, are email
and password
.
The corresponding TextField
widgets for email and password in LoginPage
forward all changes to the corresponding fields in the model.
For example, the email field does this:
hey.dart
final model = context.read<LoginModel>(); (1)
TextField(
onChanged: (value) => model.email = value,
)
The same thing happens for the password field:
final model = context.read<LoginModel>();
TextField(
onChanged: (value) => model.password = value,
)
(we don’t need to watch LoginModel, because nothing in there is a change that needs LoginPage to rebuild.)
Contrary to a lot of other fields in ChangeNotifiers, email and password are exposed as public fields.
They don’t have setters calling notifyListeners()
- simply because there’s no need for that.
We don’t need to rebuild anything in LoginPage
when email
or password
changes - as the LoginPage
is the source for those changes because the user typed everything in, everything is already in sync.
LoginModel also has a boolean called isLoggingIn
.
It’s initially set to false.
Our SubmitButton
class displays a progress indicator or normal state depending on if this boolean is true or false.
This field is the only field in LoginModel that needs an accompanying notifyListeners()
call, since we need to update the SubmitButton every time the value changes.
In it’s constructor, LoginModel also takes in a VoidCallback
called onSuccess
.
The callback is called when the user is successfully logged in.
In LoginPage, the callback body contains a call to Navigator.pushNamed(context, '/timeline');
, where /timeline is the home page of Reflectly, just like the feed is in Facebook / Instagram.
Again, we’re avoiding passing a Navigator or a BuildContext to our LoginModel as it would violate the rules.
Another thing is that the model shouldn’t have to care what exactly happens when everything goes according to the plan.
It should just tell "hey, everything is OK, user is signed in, do your thing and navigate to somewhere or whatever.I did my job and I’m off."
The cool thing is that we can write tests for the callback and whether or not it has been invoked appropriately.
Whenever the user pressed the "SIGN IN" button, we call model.signIn()
.
Then the following happens:
1. we set isLoggingIn
to true
and call notifyListeners()
to update the SubmitButton so that it’s disabled and displays a progress indicator
2. we call authenticationService.signInWithEmailAndPassword()
, passing in the current values of the _email
and _password
fields
3. if it succeeds, call onSuccess()
. don’t reset isLoggingIn
, because we don’t want the user to click on the button anymore as the login was successful and the user is being taken to another route.
4. if the sign in doesn’t succeed, dispatch an error notification saying why.
We actually have a lot of different error cases that we handle, but here’s a few for example.
also, set isLogginIn
back to false
and notifyListeners(), so that the user can click the SubmitButton in order to try again.