Easily navigate through your Flutter code by separating view and view-model

If you don’t already know about Flutter, it is an amazing developer-friendly, cross-platform mobile framework.

Code within stateful widgets can get messy.

This class is from the starter code when you run flutter create myappname.

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
  void _incrementCounter() {
setState(() {
_counter++;
});
}
  @override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}

This class looks small and manageable in this class. However, after adding a few widgets, the build method can quickly get big and clutter the whole class.

So let’s separate the UI from everything else.

  1. Remove the build method and make the class abstract.
abstract class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
  void _incrementCounter() {
setState(() {
_counter++;
});
}
}

2. Make the class public by removing the underscore.

abstract class MyHomePageState extends State<MyHomePage>

3. Make any state or methods in the class to be protected. This is the same as private, but accessible by classes that extend it.

  • (It is also possible to keep it public if you do not care about restricting access)
@protected
int counter = 0;
@protected
void incrementCounter() {
setState(() {
counter++;
});
}

4. Create a view class in a different file that extends the state class and put the build method there.

- MyHomePageView.dart
class MyHomePageView extends MyHomePageState {
@override
Widget build(BuildContext context) {
return new Scaffold(
// hidden for brevity
);
}
}

5. Replace wherever the state class is used with the view class.

-- BEFORE --
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
-- AFTER --
class MyHomePage extends StatefulWidget {
@override
MyHomePageView createState() => new MyHomePageView();
}

The view-model and the view

We have separated our one class into two classes:

  • the MyHomePageState class is now our view-model
  • and MyHomePageView class is our view.

The view-model

The view-model should only contain state

int counter = 0;

…and methods that modify state or have business logic.

void incrementCounter() {
setState(() {
counter++;
});
}

The view

The view should contain UI code including the build method

@override
Widget build(BuildContext context) {
return new Scaffold(
// hidden for brevity
);
}

… and helper build methods …

Widget _buildFloatingActionButton() {
return new FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
}

and UI converter methods.

Color _convertCounterToColor(int counter) {
return (counter > 0) ? Colors.green : Colors.red;
}

Keep note:

  • Do not put any UI code within the view-model. Any methods that build UI code should be in the view class.
--- BAD  ---
- MyHomePageState.dart
Widget _buildTitle(String title) {
return Text(
title,
style: new TextStyle(fontSize: 20.0),
);
}
--- GOOD ---
- MyHomePageView.dart
Widget _buildTitle(String title) {
return Text(
title,
style: new TextStyle(fontSize: 20.0),
);
}
  • Also, do not modify state or have business logic in the view class. Instead, create a method in the view-model class, which can then be used in the view class.
- MyHomePageView.dart
@override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
--- BAD ---
onPressed: () {
setState(() {
counter++;
})
},
--- GOOD ---
onPressed: incrementCounter
------------
),
);
}

Conclusion

The view-model class is totally unaware of the view class. Therefore:

Separating your view and view-model into separate classes (and separate files) allows you to modify business logic without navigating through a bunch of UI code.
A simple todo Flutter app with this architecture can be found here.

More where this came from

This story is published in Noteworthy, where thousands come every day to learn about the people & ideas shaping the products we love.

Follow our publication to see more stories featured by the Journal team.