Flutter: WidgetView — A Simple Separation of Layout and Logic

Shawn Blais
gskinner
Published in
12 min readMar 9, 2020

One of the most interesting aspects of Flutter, is the way it mixes declarative markup-style code, with imperative business logic style code, all within the same Dart programming language and file. This creates a really nice coupling between interface and function. When compared to editing XAML for UWP apps, or XML for native Android, building interfaces in Flutter can be a very rapid workflow.

While this is really nice from a productivity standpoint, it also manifests as one of Flutters biggest issues…

Signal to Noise Ratio

Every Flutter developer is familiar with the ‘Widget Tree of Doom’. As your Widget grows in complexity and size, it’s very easy for the Widget layout to get confusing, or the business logic to get lost. This is especially true if you embed that code directly into event handlers in your widget tree.

An example of a worst case scenario, consider this 83 line ImaginaryLoginForm Widget. The actual business logic (aka the important stuff that you'll need to debug in the future) is buried on lines 31, 63-64, and 72:

class ImaginaryLoginForm extends StatefulWidget {
@override
_ImaginaryLoginFormState createState() => _ImaginaryLoginFormState();
}
class _ImaginaryLoginFormState extends State<ImaginaryLoginForm> {MainModel _model;
bool _isLoading;
String _email;
String _pass;
String _loginFailed;
@override
void initState() {
_model = Provider.of<MainModel>(context, listen: false);
super.initState();
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Container(
child: Column(
children: [
//Back Button
RaisedButton(
child: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
}),
SizedBox(height: 10),
Text(
"ACCOUNT\nLOGIN",
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
Flexible(child: SizedBox.expand(), flex: 1),
TextInput("Email", (v) {
_email = v;
}, initialValue: _email),
SizedBox(height: 10),
TextInput("Password", (v) => _pass = v, isPassword: true),
SizedBox(height: 10),
//Show login error?
if (_loginFailed)
ErrorBox("Login failed. Please try again."),
Flexible(
child: SizedBox.expand(),
flex: 2,
),
SizedBox(height: 10),
//Show a loading spinner if we're loading, or buttons if we're not
(_isLoading)
? ProgressSpinner(color: Colors.white)
: Column(
children: <Widget>[
//Sign In btn
WhiteRoundedTextBtn(
label: "SIGN IN!",
onPressed: () {
//Do sign-in stuff
//Validate email, validate pass, etc
},
),
//Login btn
RoundedTextBtn(
bgColor: Colors.transparent,
label: "FORGOT PASSWORD?",
onPressed: () {
//Open the password reminder url
},
)
],
),
SizedBox(height: 40),
],
),
),
);
}
}

And this is not even a large tree by Flutter standards! Many Flutter layouts can easily stretch to 200–300+ lines if they contain multiple components or sections. The obvious issue with this, is your code becomes harder and harder to maintain, as you must sift through mounds of declarative markup, to find the code that really matters.

Ok, so we can admit the above example is a bit contrived. Most experienced Flutter developers are probably not making this mistake. A much better and more common practice is to define discrete buttons handlers for each UI event:

class _ImaginaryLoginFormState extends State<ImaginaryLoginForm> {... //Initialization code, state vars etc, all go here//Control logic grouped together, at top of file
void _handleLoginPressed(){
//Do sign-in stuff
}
void _handlePasswordResetPressed(){
//Open the password reminder url
}
void _handleBackPressed(){
Navigator.pop(context);
}
... //All your view code, various builds methods, whatever goes below
}

The above is a solid approach, and following it is quite effective up to a point. Here you can see the much improved class outline, with all the important stuff grouped at the top, and the layout code at the bottom.

So this works up to a point, but eventually build() methods, which contain mounds of declarative markup code, will get created next to your control methods, which are imperative logic, and the whole thing can become a bit of a mess. Additionally, the markup code tends to be much more verbose than the control logic and will usually overpower it when they are mixed together in a single Class.

So what to do then?

There are several recommended approaches in the Flutter community today:

  • Break your Widget into multiple smaller Widgets
  • Use sub-build methods (buildTop, buildBottom, etc)
  • Use a framework like redux, or bloc, to isolate much of the business logic and state away from the Widget completely

These are all totally solid approaches, and they do work, but they also all have their own set of drawbacks.

#1 Multiple Widgets

The problem with the first option is the boilerplate of creating classes and passing parameters around. Each class that you create will add about 6 lines to your file, plus one for every parameter it takes. It also requires refactoring as parameters change, which can slow things down.

Additionally, when designing multiple widgets, it can get a bit murky as to which code should go where, and you will often tend to move Widgets into their own files. This separates related code, and can make your life a little harder later, when you are trying to locate a specific piece of functionality or layout.

That is not to say that you shouldn’t use this technique, in fact, you should still use it heavily. Just keep in mind that it has its limitations and leads to its own issues when overused. This is a great approach when a certain Widget is clearly re-usable and makes sense as a standalone component, it’s much less effective when you’re doing it simply to shuffle around your code and essentially hide complexity across multiple files.

#2 Multiple build methods

With this method boilerplate and refactoring are still a bit of an issue, but significantly reduced. While creating a new StatelessWidget adds 6 lines + 1 or 2 for each parameter, a Function only adds 4 lines, and generally has no extra 'line-cost' for parameters.

Unfortunately, this approach does still have a couple issues:

  • It spreads the code out throughout the widget, forcing you to jump around in the file. Related layout code tends to get spread throughout the file, making it harder to find things, and increasing cognitive load when reading the tree.
  • If you aren’t careful, you can dirties up your class outline, mixing build methods with control logic, which can quickly get out of control.
  • There is still a decent amount of boilerplate with Functions, and parameter passing (themes, colors, padding etc) which is a still a bit of an efficiency drag.

In both cases, the widgets don’t tend to scale very well as your business logic and layout grows. If you imagine a large settings panel, things would get very large and unwieldy when everything is combined all in one class. You end up being forced to split things into multiple files in order to stay clean, but this adds extra boilerplate, and hardens your architecture, which in turn can make iteration and refactoring slower.

Much of the time, with both approaches, you can end up feeling like you’re just shuffling code around. Hiding the complexity, without actually reducing it in a meaningful way. You end up creating widgets that don’t really need to be Widgets, and/or splitting a single purpose-built widget into multiple related files which can lower overall readability.

#3 Frameworks

The issue with the 3rd approach is almost the opposite of the others. The various frameworks can tend to be too scale-able and too opinionated.

  • Redux: If you’re not building the next Facebook, the chances are you probably don’t need anything as complex as this. There is a lot of boilerplate required, refactoring can be slow, and the learning curve is fairly steep.
  • Bloc: Bloc is easier to take up than Redux, but it’s still very opinionated about how you should manage your data-flow, requires a lot of learning, and is probably overkill for 99% of Apps.
  • : Doesn’t really address this issue head on, although you will usually tend to place some business logic in the ChangeNotifier/Model, which can reduce scaling issue to a degree.

Sometimes you just want to ‘keep it simple’, but still have the ability to scale your views in a manageable way.

The ‘WidgetView’ Pattern

The WidgetView design pattern is simple to implement, has minimal boilerplate, and helps easily divide your views business logic from its layout. This separation helps keep your Widgets organized and maintainable as they scale.

The idea is pretty simple, and can apply to either a Stateless or Stateful Widget: Each State (or StatelessWidget), has a child WidgetView, which contains the declarative view code.

Widget 
--> WidgetController (State)
--> WidgetView (StatelessWidget)

The State acts as a stand-in Controller/Mediator/Presenter for the WidgetView, responding to view events and providing access to state. The WidgetView is a just a StatelessWidget that is pure layout.

This tweak to your Widget architecture is very subtle, and might seem inconsequential, but it enforces a strong high-level separation of concerns, bringing with it quite a few subtle benefits:

  • Business logic can grow up to 150+ lines and still feel very manageable and easy to sort through
  • Initialization code, event handlers and logic calculators, are all grouped together in one discrete place
  • A widget tree stretching to 200+ lines, is not really an issue with this setup, as it’s all grouped together and nicely separated from your business logic
  • The large Widget trees lets you (usually) stick to a single layout tree with a generous amount of comments, rather than a bunch of build methods, or child Widgets, which can often just obfuscate the tree.
  • If you do split out build methods, at least they are not mixed directly with your control functions
  • This all keeps your class outline stays very clean, important functions are easier to locate

It works similar to the Bloc pattern, in its desire to separate logic and layout, but without the dependency on Streams, or the additional layers of architectural complexity. Rather than an opinionated structure, WidgetView is an agnostic 'best practice' that can be combined with many different state management techniques. Specifically, community favorites like Provider or GetIt work quite nicely in conjunction with this approach.

Why use state?

You might be wondering why we would use the State object rather than just creating a dedicated ViewController object that sits on top. It seems a bit weird to have a State standing in a controller object. The simple answer is: boilerplate.

If we created a dedicated view controller, we’d have to pass through at least the initState() call, and likely other lifecycle events like didChangeDependencies or didUpdateWidget. By keeping it in State, we keep things simple, avoid fighting the framework, and end up with virtually no boilerplate.

Implementation

In its simplest form, the implementation looks like this:

class MyWidget extends StatefulWidget {
@override
_MyWidgetController createState() => _MyWidgetController();
}
class _MyWidgetController extends State<MyWidget> {
@override
Widget build(BuildContext context) => _MyWidgetView(this);
//////////////////////////////////////////////////////////
// UI event handlers, init code, etc goes here
//////////////////////////////////////////////////////////
}class _MyWidgetView extends WidgetView<MyWidget, _MyWidgetController> {
_MyWidgetView(_MyWidgetController state) : super(state);
@override
Widget build(BuildContext context) {
//////////////////////////////////////////////////////////
// Widget tree goes here.
//////////////////////////////////////////////////////////
return Container();

}
}

You can see that what we’re doing is extremely simple, it’s just creating one extra class to create a clear separation of roles.

To take it a step further, lets take a look at the famous ‘counter app’:

////////////////////////////////////////////////////////
/// Widget defines external parameters
////////////////////////////////////////////////////////
class MyCounter extends StatefulWidget {
final Color textColor;

const MyCounter({Key key, this.textColor}) : super(key: key);

@override
_MyCounterController createState() => _MyCounterController();
}

////////////////////////////////////////////////////////
/// Controller holds state, and all business logic
////////////////////////////////////////////////////////
class _MyCounterController extends State<MyCounter> {
int counter = 0;

@override
Widget build(BuildContext context) => _MyCounterView(this);

void handleCounterPressed() => setState(() => counter += 1);
}

////////////////////////////////////////////////////////
/// View is dumb, and purely declarative.
/// Easily references values on the controller and widget
////////////////////////////////////////////////////////
class _MyCounterView extends StatelessWidget {
final _MyCounterController state;
const _MyCounterView(this.state, {Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: state.handleCounterPressed,
child: Text(
"${state.counter}",
style: TextStyle(color: state.widget.textColor),
),
);
}
}

Again, nothing fancy here, but the separation of duties is already beginning to shine through. State and business logic are nicely contained, and the widget tree is a nice contained blob of layout. We’ve added a textColor property to the Widget, so you can see how the WidgetView could access it.

As a final example, lets take a look at how we might refactor the above login form to use a WidgetView.

WidgetController (aka State)

First, we would define the WidgetController which is responsible for business logic. Specifically, it will manage:

  • Local view state
  • References to global state
  • UI event handlers
  • Calculation / Init methods
class _LoginFormController extends State<LoginForm> {//Create the "View", passing ourselves in as view.state
Widget build(BuildContext context) => _ImaginaryLoginFormView(this);
MainModel model;
String email;
String pass;
String loginFailed;
@override
void initState() {
model = Provider.of<MainModel>(context, listen: false);
super.initState();
}
void handleLoginPressed() {
//Set some globalState, rebuild view
setState(()=>model.isLoading = true);
//Do sign-in stuff
//Validate email, validate pass, etc
}
void handlePasswordResetPressed() {
//Open the password reminder url
}
void handleBackClicked() {
Navigator.pop(context);
}
}

WidgetView

Next, we would define the WidgetView, which should be almost pure declarative code. This can either sit in the same file, or its own, depending on your own preference.

class _LoginFormView extends StatelessWidget {
final _LoginFormController state;
LoginForm get widget => state.widget;const _LoginFormView(this.state, {Key key}) : super(key: key);@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Container(
child: Column(
children: [
//Back Button
RaisedButton(child: Icon(Icons.arrow_back), onPressed: state.handleBackClicked),
...
(state.model.isLoading)
? ProgressSpinner(color: Colors.white)
: Column(
children: <Widget>[
//Sign In btn
WhiteRoundedTextBtn(
label: "SIGN IN!",
onPressed: state.handleLoginPressed,
),
//Login btn
RoundedTextBtn(
bgColor: Colors.transparent,
label: "FORGOT PASSWORD?",
onPressed: state.handlePasswordResetPressed,
)
],
),
SizedBox(height: 40),
],
),
),
);
}
}

Looking at the above code, we can see a few things:

  • The clean conceptual break between Control and View code immediately feels more organized
  • Both ‘halves’ of the widget are a little more free to grow and expand. They are not polluting each-others class outline.
  • We can still include some layout logic in the view itself, leveraging Flutter’s strengths with dynamic layouts
  • Passing the entire State object to the WidgetView virtually eliminates the issue of boilerplate parameter declarations
  • All related code is grouped together by default, which makes each portion easier to work on
  • The additional line-count is only about 10 lines
  • One downside is that there is a small amount of boilerplate added, with the ‘state.’ accessor in the WidgetView

While this might seem like overkill for a Widget this small (and it probably is), if you picture it being 2 or 3 times more complex, with more fields, and some nice animations, you can see how this division could help keep things manageable.

Once you get used to implementing it, it takes almost no time at all. When used consistently it can make your code base more maintainable and easier to navigate.

Worker Smarter Not Harder

To make it easier to implement throughout your code-base, you can create a tiny Abstract class using Generics that all your WidgetViews can extend:

abstract class WidgetView<T1, T2> extends StatelessWidget {
final T2 state;
T1 get widget => (state as State).widget as T1;const WidgetView(this.state, {Key key}) : super(key: key);@override
Widget build(BuildContext context);
}

Note that we pass the top-level widget typewhich provides your WidgetView with access to the typed.widget parameters without any additional boilerplate.

This removes another 4 lines of boilerplate, and now our basic WidgeView is just 7 lines!

class _MyWidgetView extends WidgetView<MyWidget, _MyWidgetController> {
const _MyWidgetView (this.state, {Key key}) : super(key: key);

Widget build(BuildContext build){
//_MyWidgetView can now easily access everything on widget and state,
// properly typed, no parameter passing boilerplate :)
}
}

Not to be left out, we can make an Abstract class for StatelessWidget as well. In this case, there is no state to reference, so we’re just going to provide quick access to the parent widget:

abstract class StatelessView<T1> extends StatelessWidget {
final T1 widget;
const StatelessView(this.widget, {Key key}) : super(key: key);@override
Widget build(BuildContext context);
}

Implementation of that, would look like this:

class _MyWidgetView extends StatelessView<MyWidget> {
const _MyWidgetView (this.state, {Key key}) : super(key: key);

Widget build(BuildContext build){
//Can easily handlers and params values on .widget
}
}

If you’re using Android Studio you can make this even easier with a couple of code snippets:

Snippet #1: Add WidgetView to an existing Widget

class $NAME$View extends WidgetView<$T1$, $T2$>{
const $NAME$View ($T2$ state, {Key key}) : super(state, key: key);
Widget build(BuildContext build){
return $END$
}
}

Snippet #2: New Controller + View

class $NAME$ extends StatefulWidget {
@override
_$NAME$Controller createState() => _$NAME$Controller();
}
class _$NAME$Controller extends State<$NAME$> {
@override
Widget build(BuildContext context) => _$NAME$View(this);
}
class _$NAME$View extends WidgetView<$NAME$, _$NAME$Controller> {
_$NAME$View(_$NAME$Controller state) : super(state);
@override
Widget build(BuildContext context) {
return Container($END$);
}
}

With that in place it’s almost no work at all to use this for your views:

https://blog.gskinner.com/wp-content/uploads/2020/02/2020-02-27_02-56-071.mp4

An quick note on responsive…

One of the nice side-effects of organizing your code this way, is that it lends itself extremely well to having multiple views, with a shared controller, which is great for implementing responsive layouts.

For example, you could have a WidgetWatchView, WidgetPhoneView and WidgetTabletView. All with different layout code, but all accessing the same control logic and state.

It might look something like this:

class _LoginFormState extends State<LoginForm> { 
//
//All the shared event handlers and state could go here
//
...
//Create different views, depending on the size of the screen
Widget build(BuildContext context){
var size = MediaQuery.of(context).size;
if(size.width > 800) return _LoginTabletView(this);
if(size.width > 200) return _LoginPhoneView(this);
_LoginWatchView(this);
}
}

All views can easily share event handlers and data, using it in different ways, with absolutely no extra refactoring required. This is obviously an over-simplified example of device detection, but you get the idea.

If you made it this far, thanks for sticking with it, this was definitely a very text-heavy post! Hopefully it gives you a fresh perspective on organizing your code, and taming that Widget Tree of Doom!

Originally published at http://blog.gskinner.com on February 28, 2020.

--

--

Shawn Blais
gskinner

Over 20 years of experience building apps, websites and games. Shawn has shipped dozens of mobile apps and several games on console and Steam.