When it comes to learning Flutter, my observation is that a lot of people get stuck when they attempt to do anything outside of the tutorials and guides.
This is true for all things in software development, but I think Flutter has it worse because it happens regardless of how much experience the developer has under their belt. My guess is that it happens for the following reasons:
- Flutter is a framework that provides a lot of freedom to developers.
- There are too many choices and opinions when it comes to how things are done.
- The Flutter team is not opinionated as to how things should be done.
These things can be good or bad depending on the developer. I used Ruby on Rails for the majority of my career and I was able to witness its influence to software development so I am convinced that the things I listed above are leaning more towards the downside.
I think Flutter should be opinionated and there should be an official way of doing things. Sure, you’d be giving up the freedom of being able to do things your way, but in exchange, you’ll be a lot more productive because it will be easier to learn and be on the same page as everyone else. Being opinionated also makes it easier to transition between projects because they’ll all be structured the same way.
Enough with my opinion though. You didn’t read this article to hear about what I think Flutter should be. Let’s just get to the good stuff.
In this post, I am going to share the things I’ve learned when it comes to architecting your Flutter app.
At some point in your journey to learning Flutter, I’m sure you’ve always wondered how you’re going to manage the state in your app. If you’re a beginner to frontend development, you might not really know why you should even bother with proper state management in the first place. However, everyone talks about state management so, of course, we are going to find the best way to do it.
If you’re experienced with frontend development, it’s such a hassle to explore all of the ways to manage state, having to pick one, and stick with it. Why couldn’t the Flutter team just tell us what to use?
Fortunately, it finally happened. In the recent Google I/O 2019, the Flutter team finally endorsed an “official” way of handling state.
I added the quotes because at the end of the video, they mentioned that there is no silver bullet when it comes to state managment. You can still use whatever method you want to manage state.
Back in Google I/O 2018, they endorsed using Bloc to manage state, but I think that method is convoluted especially for beginners or small-scale apps. The method they mentioned in the 2019 video is way better and that’s what we’re going to use in this post. However, I’m going to add my own twist to it.
Let’s build an app that lets us sign up for an account! We’re going to start with creating a class for storing the state.
Similar to the endorsed way of managing state, we are going to create a class that extends
ChangeNotifier. However, instead of using it directly inside a model, we are using
ChangeNotifier on a class that’s created specifically for storing state.
Now that we’re tracking the status of the user’s authentication, let’s make some changes to
If you’re not familiar with Provider, it’s a package that provides (heh) a convenient way of using InheritedWidget, which is a class you can use for propagating information down the widget tree. Provider, by itself, doesn’t let you do state management directly. It simply gives you a better way of interfacing your widgets with your chosen method of state management. This means you can use BLoC, redux, MobX or whatever, but we’re choosing to use ChangeNotifier because it’s easy to learn and powerful enough to scale.
There are 3 things that might have caught your attention in the above example. The first thing is this:
We can definitely use
ChangeNotifierProvider() directly, but we’ll be adding another provider later, so let’s just use
MultiProvider already to make things easier for us later on.
Next, we have this comment:
I honestly don’t know what type to put here because we won’t be using just
ChangeNotifier later on. We added this comment so
flutter analyze won’t return an error on this line. That said, please let me know if you know the type to use here.
Next is this part:
We’re using a class called
MainView as our entry point to the application. If prefer having something like this to keep
main.dart as short as possible. It also allows us to work on a completely separate file for doing app-level stuff. You really should only modify
main.dart for the very important stuff like initializing state or other classes that are to be passed through Provider.
Next, let’s check out what
MainView is all about:
The basic gist of
MainView is that if the user is authenticated, the app should redirect the user to a screen dedicated for authenticated users (
DashboardScreen). If not, the app should take the user to a login screen instead (
We’ll get to those later. In the mean time, I’ll explain the stuff from Provider.
Consumer is one of two ways to retrieve an object you passed through Provider. You should use
Consumer when you’re trying to build a widget that’s based on a value that changes a lot. In our case, that is
Consumer has an optional argument called
child that lets you build a widget exactly once and inject that directly into the
builder method. It’s a good idea to utilize this option when you have a widget that doesn’t depend on any values from Provider because the widget won’t get rebuilt whenever something inside Provider changes. It’s more efficient in terms of performance.
Handling Business Logic
In this section, we’re going to cover how to handle business logic. Before we get to that though, let’s create the login screen I mentioned in the previous section:
This screen was taken straight out of Flutter’s guide on how to build forms. I added a few things and the most important part which is the function that the Login button will execute once it’s pressed.
We’re creating an instance of a class called
AuthenticationService and calling its
signIn method. The rationale here is that widgets should only be able to have read-access when it comes to data by accessing the state or any data using Provider. If it wishes to do any mutation, it should call a service object and let it handle things from there.
Let’s look at
Let’s go over this class and its contents:
AuthenticationService accepts a
BuildContext and an optional
GlobalKey<FormState>. The context is required so we can call whatever data we need from Provider as well as have access to the navigator. The form key is required to do the validation inside of the service class instead of inside the button’s
AuthenticationService will call Provider so it can have access to
This is the good stuff.
signIn() takes 2 text controllers (for the email and password fields) which we will be using later on in this post. At the moment, it will only see if the form fields are valid. In case it’s not clear, these are the validations that will execute once
formKey.currentState.validate() is called:
formKey.currentState.validate() returns true when the fields inside the form associated with the given form key have valid inputs. Otherwise, it returns false and changes the affected form fields into an error state (the fields turn red and displays the error message).
Once the form has passed the basic validation,
AuthenticationService will call a state mutation called
authenticationSuccess(). As you remember from earlier,
authenticationSuccess() changes the value of
authenticated from false to true. Here it is in case you forgot and are too lazy to scroll up:
authenticated is now true at this point,
MainView will render
DashboardScreen now instead of
SignInScreen. Here is the code again in case you forgot what it looked like:
In conclusion, let’s summarize the things we talked about in this post:
- State is stored inside the state classes (
lib/state/authentication_state.dart). The state class also provides methods for mutating the state.
- Business logic is handled by service classes (
- The state can be accessed by using the Provider package (
- Service classes have access changing the state and other business logic.
- Widgets should only have read access to the state. Mutation should only be done by service classes.
- If a widget wishes to do any mutation (state or business logic), they should call a service class method (
I’ll be creating another post in the future to further expand on the examples that I gave. Thanks for reading!