Skip to content

Modus-Logo-Long-BlackCreated with Sketch.

  • Services
  • Work
  • Blog
  • Resources

    OUR RESOURCES

    Innovation Podcast

    Explore transformative innovation with industry leaders.

    Guides & Playbooks

    Implement leading digital innovation with our strategic guides.

    Practical guide to building an effective AI strategy
  • Who we are

    Our story

    Learn about our values, vision, and commitment to client success.

    Open Source

    Discover how we contribute to and benefit from the global open source ecosystem.

    Careers

    Join our dynamic team and shape the future of digital transformation.

    How we built our unique culture
  • Let's talk
  • EN
  • FR

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.

Get Report


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. 

Stateful Widget Flutter Image

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.

Posted in Application Development
Share this

Wesley Fuchter

Wesley Fuchter is a Senior Principal Engineer at Modus Create, with over 13 years of experience building cloud-native applications for web and mobile. Working as a tech leader he's spending most of his time working closely with engineering, product, and design to solve customers' business problems. His experience sits at the intersection of hands-on coding, innovation, and people management with a mix of experiences going from AWS, Java and TypeScript to startups, agile and lean practices.
Follow

Related Posts

  • Introduction to Flutter Widget Testing

    Writing tests for applications is not the most fun task. We get so excited about…

  • Laravel Serverless Apps
    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…

Want more insights to fuel your innovation efforts?

Sign up to receive our monthly newsletter and exclusive content about digital transformation and product development.

What we do

Our services
AI and data
Product development
Design and UX
Modernization
Platform and MLOps
Developer experience
Security

Our partners
Atlassian
AWS
GitHub
Other partners

Who we are

Our story
Careers
Open source

Our work

Our case studies

Our resources

Blog
Innovation podcast
Guides & playbooks

Connect with us

Get monthly insights on AI adoption

© 2025 Modus Create, LLC

Privacy PolicySitemap
Scroll To Top
  • Services
  • Work
  • Blog
  • Resources
    • Innovation Podcast
    • Guides & Playbooks
  • Who we are
    • Our story
    • Careers
  • Let’s talk
  • EN
  • FR