Flutter. Remove ifs from the widget tree. Final solution.
New State to Widget pattern explained.
Flutter. Remove ifs from the widget tree. Final solution.
New State to Widget pattern explained.
I have written about sanitizing the view from the logic (ifs) here and here.

If you are a member, please continue, **otherwise, read the full story here.**
I came back to the subject because I work on a relatively big production project, and the same case shows up again and again:
- We load the content;
- The content loading can result in success or an error;
- On each state (loading, success, error), we show the user different widgets.
In the case of loading — loading indicator, in the case of error — reload button, in the case of success — the actual content.
Here is the code. ViewModel:
String htmlContent = '';
bool isLoading = true;
bool isError = false;
String error = '';
Future<void> loadContent() async {
try {
isLoading = true;
isError = false;
error = '';
htmlContent = '';
update();
htmlContent = await _datasource.loadContent();
if (htmlContent.isEmpty) {
throw Exception('Failed to load content');
}
error = '';
isLoading = false;
isError = false;
update();
} catch (e) {
log(e.toString());
error = 'Failed to load content';
htmlContent = '';
isLoading = false;
isError = true;
update();
}
}
View (the code goes inside a GetBuilder):
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (controller.isError) {
return Center(
// a lot of code
);
}
//success
return SingleChildScrollView(
// a lot of code
);
It’s not that bad, and many people are doing exactly that in their projects, and it works.
So, why do I need to improve something that works? The philosophy behind my decision looks as follows:
- Both View and ViewModel are parts of the Presentation layer.
- The ViewModel is responsible for all presentation logic, including navigation.
- Ideally, the View should not contain any (presentation or, God save, business) logic.
The solution is called the State To Widget pattern. The word State refers to loading/error/success state and not to the application state as a whole. The word pattern is just a propaganda gimmick to increase the perceived value of the solution 😎.
So, the first thing we will do is to refactor the View to display custom widgets for any state. (It is a good practice to do it anyway.)
I have created a separate widget for each state:

Now the View looks as follows (the code goes inside a GetBuilder):
if (controller.isLoading) {
return LoadingWidget();
}
if (controller.isError) {
return ErrorWidget()
}
return SuccessWidget();
The next step is to create a state_2_widgetlibrary.

I deliberately put it in the widgetsfolder to brush off SoC arguments.
sealed class State2Widget {
Function get widgetBuilder;
}
class Success extends State2Widget {
@override
Function get widgetBuilder => () {
return SuccessWidget();
};
}
class Loading extends State2Widget {
@override
Function get widgetBuilder => () {
return LoadingWidget();
};
}
class Error extends State2Widget {
@override
Function get widgetBuilder => () {
return ErrorWidget();
};
}
It is very simple: contains a sealed class State2Widgetwhich defines the widgetBuildergetter and three implementations for each state: Success, Loadingand Errror. The widgetBuilderreturns a Functionthat builds a widget when called.
I think the code is simple and self-explanatory, so why am I trying to explain it?
The library is generic and can be reused as long as we use the same widget names (LoadingWidget, ErrorWidget, and SuccessWidget).
Now, let’s rewrite the ViewModel:
State2Widget state = Loading();
String htmlContent = '';
String error = '';
Future<void> loadContent() async {
try {
state = Loading();
error = '';
htmlContent = '';
update();
htmlContent = await _datasource.loadContent();
if (htmlContent.isEmpty) {
throw Exception('Failed to load lesson content');
}
error = '';
state = Success();
update();
} catch (e) {
log(e.toString());
error = 'Failed to load lesson content';
htmlContent = '';
state = Error();
update();
}
}
And View:
return controller.state.widgetBuilder();
Let’s compare two versions side by side:
ViewModel:

The ViewModel became a bit more concise and much more maintainable: we have one State2Widgetvariable instead of two booleans, which are very easy to misuse and introduce subtle bugs.
The View (the code goes inside a GetBuilder):

Here, the mission of removing ifswas definitely accomplished. The View looks a bit magical for someone who is not familiar with the State to Widget pattern but..

- It is easy to learn.
The approach doesn’t win by the number of LOC (lines of code), since there is an extra state_2_widget library with an extra 15 LOC, but as I already (proudly) said, it is very simple and going to be reused as is (only different imports) with every feature that requires loading content. That means with almost every feature. So, I and anyone who decides to use it in their projects will become used to it very quickly.
Agentic skill
Agentic skills became a trend. Every second post on FlutterDev is about somebody sharing their skill set. Most of those skills just confuse agents and increase the context size.
But this particular skill is going to be very useful since the pattern is completely new (or not?) and LLMs are not familiar with it.
---
name: state-to-widget
description: State to Widget design pattern. Use when content loading suggests three states-- Loading, Error, Success
---
1. Refactor view into custom widgets.
Folder structure:
module-x/ widgets/ loading_widget.dart error_widget.dart success_widget.dart x_view.dart x_controller.dart
2. Create state_2_widget library
sealed class State2Widget { Function get widgetBuilder; }
class Success extends State2Widget { @override Function get widgetBuilder => () { return SuccessWidget(); }; }
class Loading extends State2Widget { @override Function get widgetBuilder => () { return LoadingWidget(); }; }
class Error extends State2Widget { @override Function get widgetBuilder => () { return ErrorWidget(); }; }
Put it in the `widgets` folder
3. Use State2Widget in ViewModel
XController:
State2Widget state = Loading();
String content = '';
String error = '';
Future<void> loadContent() async {
try {
state = Loading();
error = '';
content = '';
update();
content = await _datasource.loadContent();
if (content.isEmpty) {
throw Exception('Failed to load lesson content');
}
error = '';
state = Success();
update();
} catch (e) {
log(e.toString());
error = 'Failed to load content';
content = '';
state = Error();
update();
}
}
4. Use State2Widget in View
XView:
body: GetBuilder<XController>(
builder: (controller) {
return controller.state.widgetBuilder();
},
),
That’s it, thank you for reading!
Update. I got a hint that the word status suits better than state. I.e. Status To Widget instead of State To Widget. Cannot disagree. Gemini agrees as well.

Our loading/error/success are semantically statuses and not states. Also, using the word state causes confusion with the application state.
I am too lazy to rewrite the article, but I am going to change several features in my recent codebase and the agent skill.
메타데이터
- post_id
- 273aa511ce4d
- slug
- flutter-remove-ifs-from-the-widget-tree-final-solution-273aa511ce4d
- url
- https://medium.com/easy-flutter/flutter-remove-ifs-from-the-widget-tree-final-solution-273aa511ce4d
- canonical_url
- https://medium.com/easy-flutter/flutter-remove-ifs-from-the-widget-tree-final-solution-273aa511ce4d
- author_url
- https://medium.com/@yurinovicow
- status
- ok
- fetched_at
- 2026-08-21 11:48:15