Architecture Case Study: Reflectly

As it’s more beneficial to go over what happens in real world apps than just talking about things in the abstract, let’s go over how we use ChangeNotifiers at Reflectly.

We’ll start with the simpler ones, gradually growing in difficulty towards the end.

LoginModel

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,
)
1 Library import

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.

diag c4e12021a1890a759092f57b22f20bf5

scraps

  • Quote / content page stuff - "don’t share a ChangeNotifier"