Stateless Flutter

Providing for Purists

Richard Shepherd
10 min readDec 3, 2019

What?

What are we here for?

In this article we’ll look at the classic Flutter starter project — but with Stateless Widgets only. The purpose is to demonstrate how to create an entire app without writing Stateful Widgets. In case anyone needs reminding, here’s a peek at the final state(!) of the app — oh so familiar, right?

If you’ve not yet tried out Flutter, head on over to the home page at https://flutter.dev/. If you’re familiar with Flutter already, but are asking questions like:

  • how to manage real app-wide state?
  • how to more cleanly separate state-code from UI-code (than is possible using the builtin Stateful Widget)?
  • how to avoid explicitly passing state from widget-to-wrapped-widget … as we populate our UI and the widget-tree is getting deeper? Even across multiple screens (routes)
  • how to avoid using a big old Singleton for data state management!
  • I like the <XXX> state management approach, but the boiler-plate code requirements can be a hassle, is there something simpler?

then these questions are addressed, to some extent, in this article.

The code for this alternate-starter-app is located at https://github.com/eggzotic/flutter_create_stateless. The App looks and behaves (to the end-user) just like the default project that initially results from running flutter create …. Anyone who has installed flutter, and followed a beginner tutorial, has seen and used this starter-app. That familiarity is intentionally exploited here to show how to:

  • achieve the same result while writing Stateless Widgets only
  • begin using the Provider package for app-state management

Note that Provider is not the only choice available to separate, manage and facilitate access to the app state. Other options exist and it’s a matter of personal taste and preference as to which to use. When I happened upon Provider, I found it to be preferable to my previous approaches and have used it ever since — and have not yet had reason to move on. Note also that this is a separate issue from data-persistence. I will cover that in another article.

Why?

Why would we care about writing Stateless Widgets only?

For me, Stateless Widgets appeal due to their simplicity and clarity. A Stateless Widget contains final instance properties only, optional helper-methods, and a mandatorybuild method that describes the UI appearance for any given app-state. It does not defer its buildmethod to another State object (as in the case of Stateful Widgets) and has a simple lifecycle (again, compared to the more complex Stateful Widget + State object). The Flutter engine is optimised to ensure that visual updates to your app are limited to real-change, regardless of how much activity (i.e. re-builds) may be going on due to data-change.

How?

But my UI is not a static screen?

Does “writing Stateless Widgets only” imply the appearance can never change? Not at all. Stateless Widgets can access (& modify) data passed in from outside, e.g. through dependency injection. When a Stateless Widget is instantiated, the build method is run to determine its initial appearance. And that build method can be re-run to reflect the updated state of our App.

But, there is no setState() to be used with Stateless Widgets, so how can we trigger a re-build when app-state has changed?

Well, there is the option of using StreamBuilder — but that may require us to jump through some hoops for our data-model to also produce Stream(s) to be consumed by the StreamBuilder. For data naturally suited to Streams (or already being produced as a Stream — e.g. using FlutterFire with Google Cloud FireStore’s snapshot().listen()), then by all means use StreamBuilder to manage the display as the Stream produces data-updates. But in other cases that may seem unnatural, and require more code.

Enter … the Provider package (https://pub.dev/packages/provider). Among others, Provider includes a variant ChangeNotifierProvider which can both contain our app state and trigger updates (i.e. trigger build to re-run) of any consumer — such as a Stateless Widget, by notifying said consumer without the widget having to provide an explicit callback. Provider, in fact, offers several variants — e.g. using Stream, Future, … — but this article will be limited to demonstrating the ChangeNotifierProvider, which also happens to be the one I’ve used most often in Apps.

But really, how?

So, down to the business of how this alternate starter app is written. The app source code remains a single-file of dart code — main.dart, for convenience — but this is not best practise for a real project. Feel free to go one better and separate the dart state (CounterState, below) to a dedicated source file under the projectlib/ folder. The original main.dart that flutter create … generates (Stateful Widget and all) is present in the repo, renamed to main.dart.orig, for reference. For this article the intent is to add/remove minimal code to complete the transformation (from Stateful + State -to- Stateless + Provider) while keeping the code (and the running-app) as recognisable as possible.

The instructions below will take you from flutter create … to an end result as described & discussed above. Although I personally use Visual Studio Code (VS Code) + iOS Simulator as my main IDE + testing platform — the below steps do most of the non-editing using the bash command-line from the terminal, which hopefully keeps it applicable to the broadest audience. It is assumed you have already installed Flutter, plus Xcode and/or Android Studio etc. on your development machine.

While you can just download the repo and fire it up (via flutter run) — please don’t! Instead, by following the conversion process below, we’ll get a better feel for the effort involved in using Stateless Widget + Provider and for what it replaces/provides-an-alternative-to. Learning to appreciate that is the most value here.

  1. run flutter create flutter_create_stateless to create a new vanilla Flutter starter project (or choose your own project name)
  2. open the project folder in your preferred editor/IDE

Now we’ll introduce the Provider package:

  1. Open the pubspec.yaml file and locate the dependencies: section
  2. Add provider: ^3.2.0, indented appropriately to match the other items. Version 3.2.0 was current at the time of writing this article.
  3. next, run flutter packages get, if your IDE does not do this automatically.
  4. Now open main.dart
  5. Add this import statement near the top, just below the existing one for material.dart: import ’package:provider/provider.dart’;

Now we’re all set to use Provider.

Next we’ll create a new class for containing the app-state. The original app essentially has a single int as state — the number that increments as we tap the floating button at the bottom of the screen. Super-simple — even too simple for a real app — that’s why it’s a starter app(!). A new class to manage a single int? Overkill, maybe, but that’s what reference projects often do — it serves to keep the focus on the mechanism (Provider) and avoid getting distracted with genuine data-model complexity.

So, create the CounterState class. Add the definition to main.dart (or go a step further and create a new file, and add the import to main.dart for it):

class CounterState with ChangeNotifier {
// the actual state is private (hence the leading "_")
int _value = 0;
// this allows us read-only access to the state, which
// ensures modification of state is via public methods
// we expose (in this case that means "increment" only)
int get value => _value;
// how we modify state and notify consumers (e.g. the App UI)
void increment() {
_value += 1;
// notify the consumer of this data that something has been
// updated - thus avoiding any need for setState()
notifyListeners();
}
}

Key points to note about this class:

  1. with ChangeNotifier is in the class declaration. This enables instances of CounterState to notify any object that has registered a listener, on this instance, that a change may have occurred. ChangeNotifier is a builtin Flutter class described at https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html.
  2. notifyListeners() called immediately after modifying the state. This is how we notify listeners that a change really has occurred. When that listener is a Stateless Widget, this amounts to a re-build.
  3. CounterState is not a plain-dart (i.e. flutter-less) class. ChangeNotifier is a Flutter class. But apart from the class declaration, and judicious use of notifyListeners(), our class code is near enough to pure dart. Another option, for a bit more separation of concerns, is to wrap a plain-dart class in a with ChangeNotifier intermediary class which in turn handles the public API for the pure-class so that notifyListeners() can be called when necessary. This approach might be useful when that inner plain-dart class is written by a 3rd-party!

Beyond such a trivial piece of state to manage: In this class we call notifyListeners() just once — in the method where state is modified. For a real app, the state will be more complex — there may be multiple methods that will modify state. Include a call to notifyListeners() after updates to state are completed. If you have chained calls, you might include notifyListeners() in the lowest-level method only — where they all meet. E.g. if there are 3 public methods that all modify the same piece of state — but all call a common private helper-method that does the actual modification(s), then it may be best to have that private method be the only one that actually calls notifyListeners(). Try to avoid having a single state-modification that calls notifyListeners() more than once in the same update. But I digress.

That’s it — our state is ready to be used, aka “consumed”.

Back to main.dart (in case you created the above CounterState in a separate file).

The top-level Widget in our App is MyApp — a Stateless Widget, whose build method returns a MaterialApp — no change (yet).

The MyHomePage Widget — we will convert this to a Stateless Widget:

  1. Modify the declaration to class MyHomePage extends StatelessWidget
  2. delete from @override _MyHomePageState … to the end of the void _incrementCounter() method definition.
  3. Also, update the line title: Text(widget.title), to be slightly simpler as: title: Text(title), since we no longer have the 2-piece widget — we can access the title string directly.

This has deleted the closing of the MyHomePage class and the opening of the class _MyHomePageState. The build method that previously belonged to the State object is now the build for our Stateless Widget. So, right now, our MyHomePage class has an instance property title and a build method … and some errors, due to _counter and _incrementCounter being now undefined.

How to introduce some state for our widgets to use?

Firstly we’ll go back to the MyApp widget and its build method. Locate the line home: MyHomePage(title: ‘Flutter Demo Home Page’),. We’ll change that line, to instantiate a CounterState object, wrapped in a Provider, like so:

home: ChangeNotifierProvider(
create: (context) => CounterState(),
child: MyHomePage(title: 'Stateless Flutter Demo'),
),

Now the MyHomePage Widget has a CounterState instance attached just above its own context. But how to access & modify that?

In the build method of MyHomePage, just below the method declaration, add these lines:

final counterState = Provider.of<CounterState>(context);
final _counter = counterState.value;
void _incrementCounter() => counterState.increment();

Having obtained a handle to our state (contained in counterState above), we can do anything with it… muahaha.

The magic to make our state object accessible from inside the widget is the Provider.of<…>(context) call. The type of object we’re looking for is included inside the angle-brackets <…>. Specifying the type is required so that the correct Provider object is found — there could be multiple, of different types, in a real world app. Notice that context is required as a starting point in the widget tree from which to search (upwards) for the app-state. Stateless Widgets only provide access to their build context within the build method. Since much of the logic determining the widgets appearance will likely be based on the values within the state, we essentially must bring all of that code into the build method. So, our Stateless Widget code will be largely concentrated in the build method — this might be something to consider if you’re averse to bloated build methods. De-bloating our build methods can still be achieved, after obtaining a handle to our state via Provider.of, by either having other methods defined in our Stateless Widget (to which we explicitly pass our state handle) which are then called from within build. Another way is breaking the content of those methods out into their own new custom Stateless Widgets (which can also access the Provider state data via Provider.of<…>(context)), since they will be even lower in the widget-tree.

Having now gained access to the data and methods of the CounterState object, we can read the value (_counter) and modify it (via _incrementCounter) exactly as was done before — hence we don’t need to modify any more code below this point. Continued use of private vars (i.e. those whose name begin with underscore) in build here is only to avoid altering more of the original code. No need to to this in your own projects written Stateless from the beginning.

Ensure you have a Simulator (iOS) or Emulator (Android) running or a physical device attached and go ahead with flutter run to see the app running. It should look and operate…exactly like the original start app. But you will have that warm, smug satisfaction that comes from being a purist and zealot of Stateless-ness!

Down with the State(ful Widget)?

So are Stateful Widgets dead? Can we banish them — to far off lands — to be used by those so wholly unsophisticated as to not appreciate a life of blissful Stateless-ness?

Well … no.

Stateful Widgets still have a place, even for us Stateless purists. The distinction between “App State” and pure “UI State” is one way to distinguish which way to go. For the former, we might prefer State management such as Provider (or BLoC, etc.). For the latter (pure UI state), Stateful Widgets may be more concise. Many of the Flutter builtins are in fact Stateful Widgets. One I use often is the ExpansionTile — this widget, when the user taps it, will toggle animating between an expanded form (show detail) and a more brief form (hide detail). Internally, ExpansionTile maintains a boolean state that determines whether that tile should be displayed as expanded or not, using setState() to update its appearance.

The beauty of Stateful Widgets is that we can use them without having to write them. They’re typically self-contained, often including animations and whatever other localised-state necessary for maintaining their UI appearance, that can then form the building blocks of our own Stateless Widget code. So if, like mine, your app UI is composed mostly of the built-in Flutter Widgets (and perhaps some excellent 3rd party ones), then we get the best of both worlds — we can use Stateful Widgets (without even knowing they are!) as components of the Stateless Widgets that we write.

Next episode: Local Data Persistence in Flutter

--

--

Richard Shepherd

Flutter enthusiast. I enjoy learning & creating with Flutter for mobile. Also enjoy cruising SE Asia on Motorcycle. Oh and messing about with sourdough.