State management was an obvious thing.
“Do your own work. No one’s going to do it for you.”
Have you ever been taught something like this? I always thought that this was a given, especially in the programming world.
In particular, think about the “state management” ( people use this word as managing data that should be displayed in UI ).
For example, in Java, I would often manage the status of the data displayed in a UI as follows.
// A listener interface
public interface DataChangeListener {
void onDataChanged();
}
// A model class
public class SampleModel {
private String name;
private String email;
private List<DataChangeListener> listeners = new ArrayList<DataChangeListener>();
public void setData(String name, String email){
this.name = name;
this.email = email;
}
public void addListener(DataChangeListener listener){
this.listeners.add(listener);
}
public notifyAllListeners(){
for(DataChangeListener listener : listeners){
listener.onDataChanged();
}
}
:
}
// A UI class
public class SampleUI implements DataChangeListener{
private SampleModel model;
SampleUI(SampleModel model){
this.model = model;
this.model.addListener(this);
}
:
@overide
public void onDataChanged(){
// update UI with model's data
:
}
:
}
This is an old-fashioned but natural way to manage the Model and UI separately. In Visual Basic, C#, C++, etc., I’ve always done state management this way. I had thought that I could easily devise this level of mechanism myself, no matter what programming language I used…
Flutter is different.
I started learning Flutter relatively recently. Soon I fell in love with Flutter because of its smart style architecture. But I was surprised when I started to learn Flutter’s state management. They said, “You need to use one of the state management packages.” Despite my doubts, I learned Provider, the most popular and Google endorsed package. However, the deeper I knew and used it, the more I felt that something was wrong, especially when looking at the class structure below.
These classes are meant to link multiple states to each other, but to me, it looked like a desperate measure, no matter how I looked at it.
At that time, I heard about the Riverpod, roughly speaking, the next version of Provider. So, to resolve my frustration, I started to study it as well, but after seeing this flowchart, I gave up. 😣 (UPDATE: the flowchart seems to be removed now)
In my experience, any state management mechanism had been very simple and easy to understand. But in Flutter, not only Provider and Riverpod, all of the packages about the state management are very complicated. Gradually, I began to wonder there was something wrong with that.
Flutter state management for minimalists!
Then one day, I found a great article, “Flutter state management for minimalists” written by Suragch. This was what I was looking for for a long time. He says about Flutter’s state management:
There were either too many options or too much magic was happening behind the scenes. My brain needed simple and understandable. It needed minimal.
And he explains in his article that we can do well about state management with Flutter’s own built-in StatefulWidget, ValueNotifier, and ValueListenableBuilder classes.
I was immediately hooked on this idea. So I looked into these factors in more detail. In the rest of this article, I tell you my insight of StatefulWidget and ValueNotifier and also explain what “Flutter state management for purists” means.
(For more information on how to achieve state management with these, please refer to his article.)
About StatefulWidget
People who use state management packages say you should avoid using StatefulWidget because it constantly rebuilds itself when the state changes. However, they overlook the fact StatefulWidget only rebuilds when setState() is called and the fact StatefulWidget has another use case that doesn’t use setState().
Actually, the official document explains this:
There are two primary categories of StatefulWidgets.
The first is one which allocates resources in State.initState and disposes of them in State.dispose, but which does not depend on InheritedWidgets or call State.setState.
So the “Flutter state management for minimalists” says:
Finally, use a stateful widget whenever you need to initialize something when the page first loads.
About ValueNotifier
I’m embarrassed to say I didn’t know about ValueNotifier, a very useful class to notify state changes until I read that article.
People may mistakenly believe that ValueNotifier is made for primitive values. But it is untrue.
ValueNotifier’s set value method is below:
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
As you see, the class parameter T can become anything. With understanding the if statement’s comparison, you can use any class as T as long as the value is immutable.
Yes, Flutter has provided us a simple and straightforward way to state management with StatefulWidget and ValueNotifier from the beginning!
For purists 😶
“Flutter state management for minimalists” pointed me to a way to do state management on my own without third-party packages. I emphasize again and again, it is a pretty amazing article. However, its solution uses the package GetIt to allow all classes to use model classes or service classes anywhere. He explains why he doesn’t use the package instead of singleton classes;
You could even replace GetIt with a singleton if you wanted to. However, an advantage of GetIt over a singleton is that GetIt is easier to test.
Yes, GetIt seems to be a good and simple Service Locator in the Dart world. But I think that if we can do the state management without third-party packages, isn’t it true that we can make a simple Service Locator on our own?
Singleton classes make tests difficult, is it true?
Many people say that you should avoid using singleton classes because they are not mockable for unit tests. However, is it true? I don’t think so.
My personal preference, I wouldn’t say I like using many mock objects for tests. Because (1) they make my tests more complex, (2) the more you use it, the further away it gets from real-world behavior. Though, sometimes I have to make mock objects and use them for some tests. Then how do I make my singleton classes mockable? It’s not that much to think. See the below code.
abstract class ASingletonService {
// switch for using mock
static ASingletonService? mock;
// singleton logic
static ASingletonService? _instance;
static ASingletonService instance() {
_instance ??= mock != null ? mock : _ASingletonServiceImpl();
return _instance!;
}
// interfaces
void methodA();
:
}
class _ASingletonServiceImpl extends ASingletonService{
@override
void methodA(){
// write production code here.
}
}
// this class is located in the test code.
class _ASingletonServiceMock extends ASingletonService{
@override
void methodA(){
// write mock code here.
}
}
It’s easy, isn’t it? You can change whether or not to use the mock for the singleton by setting the mock property to your mock object. This kind of work is not so different from the installation and setup of third-party packages such as GetIt. And gladly, you won’t have to be on the lookout for the latest information and version control of those packages! 👍
UPDATE: Following Michele’s comment, I remade that singleton test implementation into an externally injectable one.
Sample project
I’ve made a very simple app to demonstrate the above techniques. This app’s structure is following.
This app only gets two states( a text and a button color ) from pseudo-server and reflects them to its two widgets(ButtonContainer and ServerDataWidget), respectively. SampleService class is a singleton, and you can change its behavior as mock just by only writing one code in your test code ( I’ve already written it in widget_test.dart)
This app does not use any third-party packages for state management! Please check it out on GitHub.
Conclusion
Yes, state management packages for Flutter are quite exquisite. However, there is a benefit to using any package only if you really need it. Why not reduce dependence on packages as much as possible, and make it on your own where you can?
I hope this article is helpful in that regard. 😊
2 Responses
Hi Inclu cat,
I found this article very interesting. Thank you.
One reason why the singleton class is not ideal is that it contains code that is meant for testing. You are polluting your production code with testing code.
You can still avoid it by using an `InerithedWidget` that provides the service object to its subtree. It is a bit more boilerplate code, but it will help with testing as well.
I am going to add this article to my newsletter. You will find it soon in the 18th issue here: https://ishouldgotosleep.com/news/
Bye,
Michele
Thank you for your comment.
I appreciate teaching a shortcoming of my approach.
I agree that the testing code shouldn’t be in the production code.
If I can make that singleton’s test implementation externally injectable, it seems to me that problem can be avoided. (And that doesn’t seem that hard to do).