In our last Flutter blog, we explained writing tests for stateless widgets. Stateless code is less prone to present problems due to its simplicity and lack of state changes. State, on the other hand, is usually the bad guy.
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
As engineers, we are very good at writing code and logic with a given state in mind, but when users manage to create a different state in the application, our code fails.
When writing a Flutter app, we often deal with states. This article will teach you how to build and manage a stateful widget.
Flutter Stateful Widgets
State is one of the most common words used in software engineering. Even though it is clear what it means, it is fuzzy to understand exactly what it is in your app and code. Is the state the data in the database? Is it the user’s favorite color? Is it the size of a window? Yes, yes, and yes.
Flutter’s definition of the state is pretty straightforward, so let’s stick with that. In the broadest possible sense, the state of an app is everything that exists in memory when the app is running. This includes the app’s assets, all the Flutter framework variables related to the UI, animation state, textures, fonts, etc. However, while this broadest possible definition is valid, it’s not very useful for architecting an app.
First, you don’t even manage certain states (like textures). The framework handles those for you. So a more useful definition of the state is “whatever data you need to rebuild your UI at any moment in time.”
Second, the state that you manage yourself can be separated into two conceptual types: ephemeral state and app state.
According to Flutter’s documentation, a widget is a direct result of the builder method applied to a state. When building the widget, the state is used to define its behavior, such as what information to show, how to show, buttons to hide, texts to highlight, etc.
Now that we understand the meaning of state let’s move forward and understand how to manage it inside our application. There are different ways of handling the state, depending on the requirements of your app. For this example, we will deal with the state of a single widget that, by using Provider and ChangeNotifier, integrates with a Firebase service to store data.
The Experiment
Following up on the experiment presented in our previous article, we have a simple to-do list to add pending tasks. This article will focus on a different part of the experiment — How the page that creates/updates tasks works.
The requirements are pretty simple:
- The user should be able to create a task.
- The user should be able to update an existing task.
The Code
Building a stateful widget with Flutter is simple. A Flutter widget is a result of a state applied to a builder function. In this case, what we should care about is the build method. But some structured code is needed first as this is object-oriented code after all. A class representing the stateful widget should implement the createState method from the StatefulWidget abstract class. It requires the construction of another class that extends the State abstract class, the one that has the builder method we care about.
class TaskDetailsView extends StatefulWidget { const TaskDetailsView({Key? key}) : super(key: key); @override _TaskDetailsViewState createState() => _TaskDetailsViewState(); } class _TaskDetailsViewState extends State { final _formKey = GlobalKey(); @override Widget build(BuildContext context) {
Let’s focus on the builder method. Flutter uses a declarative approach, which means that for every action that changes the app state, a rebuild is necessary by triggering the builder method with the updated state. A new state, applied to the builder, will lead to a new screen result, and so on. You can find the complete builder method for the task form widget here.
We’re building a stateful widget that will show up on the user’s interface, and we care about the result of the build method. So let’s dive into it, starting with finding out where the state is. For us, the state is a Dart class.
It is not just an ordinary class but a class that extends from ChangeNotifier. A ChangeNotifier implementation allows other aspects of the application to subscribe to its changes. We call this class as TaskDetailsViewModel.
class TaskDetailsViewModel extends ChangeNotifier {
So let’s say we want to rebuild our UI due to a change to the task title. We can notify the listeners on the submit action to trigger the UI refresh.
Future save() async { final savedTask = _task.exists ? await _taskService.update(_task) : await _taskService.save(_task); notifyListeners(); return savedTask; }
We are leveraging the ViewModel pattern here. Our state is represented by a ChangeNotifier implementation that holds the data flow needed to handle coming from the View side (widgets). Based on the user’s interface actions, it interacts with the business logic existing in the Model side (repository, service, data objects) to execute the users’ wish, such as adding or updating a task on the database.
Our TaskDetailsViewModel class comprises attributes representing each field of our form — title, description, and due date. That state will be kept intact during the widget lifecycle even when it gets rebuilt. Also, our ViewModel depends on the external classes that provide business logic, for example, the TaskService. This service connects our UI directly to Firestore’s endpoints where we are storing our data, resulting in the following flow:
But you are probably asking yourself: How do I get the model instance inside my widget? The answer is pretty straightforward: You provide the state and consume it inside the widget.
Provider is a third-party package that, together with the ChangeNotifier and ViewModel approaches, creates a powerful architecture to state-manage your app. It allows us to instantiate and register an instance of our ChangeNotifier implementation at the start of any tree of the components. Down in the tree, we can consume that same instance, and when we perform the consume call, it will register the consuming widget as a listener to the ChangeNotifier observable.
That’s the catch! Any change you yield from your state changes will directly impact a new UI refresh. Therefore, if your widget results from the state applied to the build method, any change will result in the new state being presented to the user.
Here’s how to set up a provider:
- We need to register our view model to the provider internals. We have implemented our register logic inside our routing system for this example. For small trees of apps, this is an acceptable approach. Since we’re registering every page to the routing system, we also provide anything that the page needs to consume to behave. For bigger tree widgets, this approach might get messy, So, be careful implementing it:
return MaterialPageRoute( builder: (_) => ChangeNotifierProvider( create: (context) => TaskDetailsViewModel.withTask(getIt(), task), child: const TaskDetailsView()));
- We need to make it available to our consuming widget:
return Consumer( builder: (context, model, _) { return Scaffold( appBar: AppBar( title: Text(model.exists ? model.title : 'Create Task'), ),
If you provide one or more instances at the top of a given widget tree, you can consume and be notified about state changes on any of the instances down below.
Final Thoughts
State management is a complex task and decisions influencing it should be made during the architecture design stage. Failing at this phase might compromise your entire application and require lots of refactoring work later.
We learned that there are different ways to approach state management. We saw that putting together provider, change notifiers, and patterns like ViewModel allow us to manage our state without a ton of boilerplate code or heavy frameworks.
Ideally, this solution should solve all the problems, but we know that silver bullets don’t exist. All designs and solutions also bring tradeoffs to the table. Therefore, we should choose the right solution carefully and be ready for its tradeoffs.
The code used in the example is available here.
Need efficient and scalable software solutions? Learn more about our software development expertise.
Wesley Fuchter
Related Posts
-
Introduction to Flutter Widget Testing
Writing tests for applications is not the most fun task. We get so excited about…
-
How to Build a Serverless Application Using Laravel and Bref
Since the release of AWS Lambda, serverless architecture has grown more popular within the software…