Flutter Basics - Going From Setstate To Architecture
Flutter Basics - Going From Setstate To Architecture
So what’s the most basic way of handling this? We’ll store a list of values
in the state locally and call setState when we get the new values.
Let's add some UI to display the list so that we can see the data. Change
your build function to this.
At this point you'll have a list of results on the screen. I’m gonna add a
little bit of styling to make things easier to look at then we can continue.
Move the ListItemUi out of the builder method into it’s own function. Set
the scaffold background color to grey[900] and use the code below for
your _getListItemUi.
Now back to the actual tutorial. We’ll tackle state feedback while the
setup is simple. So what states do we have?
Busy: We have to tell the user when we’re busy with something. While
we’re fetching the data we’ll show a progress indicator.
DataRetrieved(already taken care of): Show the data to the user when it
arrives.
ErrorOccurred: Show a message when something went wrong
NoData: Indicate to the user that the request was successful but they have
no data to display yet.
Let’s tackle busy first. We’ll increase the time delay to 2 seconds so we
can see the UI in action. We can determine the state using a private
property that checks if the data is null. Based on this value, return a
loading indicator or the list view. We have to make sure the data is null
when we start, so remove the initialisation code and leave it null.
After these changes when you restart your app you should see a busy
indication for 2 seconds then the results pop in. Nice. Onto the next state.
Let’s do the Error state. We’re limited with this approach, but we’ll make it
work. When there’s an error we’ll populate with one item that has the
error message in it. We’ll catch the error after the .then call. We’ll also
have to update the future to return an error. We’ll do that first.
Then we can pass in true in the initFunction call and catch the error.
Awesome. When you restart the app now you should see a little bit of
loading and then the error popping up. Last state is when there’s no info
returned. We’ll update the Future again to take in another additional
boolean that will return an empty list for us.
If there’s no items in the data returned we’ll add one with a message in it.
Add this logic where we get the data back.
Let’s Pass in the future to the FutureBuilder and supply it with an empty
builder. Like so.
Now we’re in business! We can return the UI’s we want and determine
state locally in the future builder. We’ll start with 2 basic states, busy and
dataFetched. Cut the code from the _getDataUI function and return it from
the builder function.
When there’s no data return the busy state, when there’s data return the
ListView builder. Use the snapshot.data instead of the _pageData variable
so you also have to pass that into the _getListItemUi function.
Now we can handle errors and empty data. Add the error handling right at
the top and the empty check just before the list view is returned. Both of
these will have the same styling so create a method called
_getInformationMessage(). This function takes in some text and displays
that.
Then Change the FutureBuilder to look like this.
With that you should have all the same functionality without using
setState. Pretty cool right. You can test out the states by passing in
different values to the _getListData function. pass in hasError: true and
restart the app and you’ll see the error message, the same with the
hasData: false. There’s still some limitations to using it this way.
You can’t re-run the future builder and go through all the states again. Let
me show you what I mean. Say for instance we got the data but realized
that it’s old so we want to update it. Very common scenario. We’ll add a
FloatingActionButton to the scaffold and when tapped fire off the Future
to show the limitation.
Above the backgroundColor in the scaffold add a floatingActionButton.
That’s a pointless button there, but it’s just to prove a point. If we wanted
to re-run the future to do a refresh then we wouldn’t be able to. Not with
this approach. To get something like this we need the UI to respond to
state changes consistently based on state values we pass it. For that we
can use a Stream and a StreamBuilder.
StreamBuilder
This widget allows you to return a UI based on the values you send to it
using a stream. The way this will benefit us is that we can send the state
values whenever we want and the widget will display the UI we defined
for that state. This means we can set it back to busy, fetch new data then
tell the stream we’re done and show updated data.
To start off we’ll use an enum to represent the states. Create one at the
top of the home file, outside the class, called HomeViewState. Leave out
Error because the stream controller allows you to add an error to the
stream.
enum HomeViewState { Busy, DataRetrieved, NoData }
The way to add values onto a Stream is using StreamController. Create a
Stream Controller, change the FutureBuilder to a StreamBuilder and
change the future parameter to stream.
As you can see the builder can still be used with the same UI. The only
difference will be that instead of using the data to make decisions we’ll be
looking at the enum values. Leave error handling as is and just update the
rest.
1. We have to get the listItems for the ListView builder from a different
place, it’s not in the snapshot anymore.
2. We have to emit the stream values
3. We have to run the future when we land on the page. For this we’ll
need to go back to a stateful widget and use the init function.
Remember to remove the local listItems value from the builder function in
the StreamBuilder.
The part we want to focus on now is what happens when our Future is
called. At the beginning we broadcast the busy state. If there’s an error we
add that to the stream, if we have noData we broadcast that and at the
end we tell the stream the data is fetched. At this point, even though our
business logic code is still in the same file as the UI, it’s already decoupled.
If you run the code now everything should still be the same, with one
addition. If you tap the FloatingActionButton the view will refresh :) . Now
that it’s all decoupled and in a stream we can create an “architecture”, and
by that I mean split your files up.
Splitting your files, logically
At this point the code for the UI is completely separate from the business
logic. The next logical step, for those that do think about architecture and
code maintenance, is to split up our file.
We’ll split our file into a Model and a view. Create a new file called
home_model.dart and move the following code in there.
In the home file you can now remove all the member variables and replace
it with one model instance. Then replace the calls to _getListData with
model.getListData and the stream with model.homeState. And there you
have it, a reactive-“architecture” for a simple app. There’s no name for this
architecture. But the app is architected. That’s all that I wanted to show.
Please leave some claps if you gained any insight from this, I would really
appreciate it. Youtube video coming out next week, a subscription would
be appreciated. I’ll be doing more Flutter Foundation series posts so
follow me to get them in your feed. If you have something you can’t wrap
your head around or struggle with let me know and I’ll add that to the list
of Foundation articles I want to write.
This is the end of the tutorial but I’d like to mention some things you can
do (as a young architect) to get on your own journey to building well
written, easy to maintain mobile apps using Flutter. Without forcing
certain architectures onto your apps.
Put your files in folders: Keep things neat by grouping your files, no need
for a deep structure just basic. views, viewmodels, models, services, etc.
Decide on a naming convention and stick to it: Here we called our model
a model, but that might clash with the data models we’ll use to represent
our information. You’ll have to come up with a convention to easily identify
the following things in your code.
● View files (The file representing one page/view in your app, not the
separate components)
● View State Models (The file that performs the business logic and
provides state, view-model is a common name, BLoC is being
thrown around now too, Controller is popular as well or just Model)
● Data Models (The file that represents the structured data in your
project). You have to think about this because lets say you have an
app where you can file Reports. You might name your view
report.dart, and when you make your model you want the simplest
name so you call your model for report report.dart also. This
obviously won’t work. But you can name your view report_view.dart,
and leave the model as the simple one, or vice versa. But you have to
stick to it. The rest of the architecture rules you can establish as you
go along. Things like, only the model can add to stream through
actions (BLoC), no two-way binding. Or, have two-way binding, or
instead of having multiple models we’ll have one that represents our
App State and we’ll send messages to it through a stream using
actions.
Whatever you decide, make sure it solves your problem first. Then look at
long term enjoyment of using that code base. If your code is separated
well from the beginning you can quickly change architectures and move
your logic around so don’t worry too much about choosing the perfect one
at the beginning.