This is the 21st day of my participation in the August More Text Challenge. As a Flutter developer, I’m sure you’ve heard the popular phrase “** Everything is a widget” at least once in your development life, in preparation for nuggets August challenge. This is the mantra of Flutter and it reveals the inherent power of this very good SDK!
When we look in the Widgets directory, we can see many widgets, such as Padding, Align, SizedBox, and so on. We combine them to create other widgets, which I find extensible, powerful, and easy to understand.
But when I read some of the source code I found on the Internet or written by new adopters, one thing struck me: the tendency to have lots of build methods, instantiating lots of widgets! I find it hard to read, understand and maintain.
As software developers, we must remember that the real life of software begins when it is first released to users. The source code of the software will be read and maintained by others, including your future self, which is why it is important to keep our code simple, easy to read and understand.
An example of “Everything in widgets” can be found in the Flutter documentation itself. The goal of this tutorial is to show how to build this layout:
In the end, the code does what it does: it shows how to create the layout simply. As we’ve seen, there are even variables and methods that provide semantics for various parts of the layout. This is a good point because it makes the code easier to understand.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { Widget titleSection = Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Oeschinen Lake Campground', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Text( 'Kandersteg, Switzerland', style: TextStyle( color: Colors.grey[500], ), ), ], ), ), Icon( Icons.star, color: Colors.red[500], ), Text('41'), ], ), ); Color color = Theme.of(context).primaryColor; Widget buttonSection = Container( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildButtonColumn(color, Icons.call, 'CALL'), _buildButtonColumn(color, Icons.near_me, 'ROUTE'), _buildButtonColumn(color, Icons.share, 'SHARE'), ], ), ); Widget textSection = Container( padding: const EdgeInsets.all(32), child: Text('Lake Oeschinen lies at the foot of the Bluemlisalp in the Bernese ' 'Alps. Situated 1,578 meters above sea level, it is one of the ' 'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a ' 'half-hour walk through pastures and pine forest, leads you to the ' 'lake, which warms to 20 degrees Celsius in the summer. Activities ' 'enjoyed here include rowing, and riding the summer toboggan run.', softWrap: true, ), ); return MaterialApp( title: 'Flutter layout demo', home: Scaffold( appBar: AppBar( title: Text('Flutter layout demo'), ), body: ListView( children: [ Image.asset( 'images/lake.jpg', width: 600, height: 240, fit: BoxFit.cover, ), titleSection, buttonSection, textSection, ], ), ), ); } Column _buildButtonColumn(Color color, IconData icon, String label) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: color, ), ), ), ], ); }}Copy the code
In fact, it could be worse. Here’s the typical all-in-one widget version of this code that I don’t like:.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { Color color = Theme.of(context).primaryColor; return MaterialApp( title: 'Flutter layout demo', home: Scaffold( appBar: AppBar( title: Text('Flutter layout demo'), ), body: ListView( children: [ Image.asset( 'images/lake.jpg', width: 600, height: 240, fit: BoxFit.cover, ), Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Oeschinen Lake Campground', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Text( 'Kandersteg, Switzerland', style: TextStyle( color: Colors.grey[500], ), ), ], ), ), Icon( Icons.star, color: Colors.red[500], ), Text('41'), ], ), ), Container( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.call, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( 'CALL', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: color, ), ), ), ], ), Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.near_me, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( 'ROUTE', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: color, ), ), ), ], ), Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.share, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( 'SHARE', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: color, ), ), ), ], ), ], ), ), Container( padding: const EdgeInsets.all(32), child: Text('Lake Oeschinen lies at the foot of the Bluemlisalp in the Bernese ' 'Alps. Situated 1,578 meters above sea level, it is one of the ' 'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a ' 'half-hour walk through pastures and pine forest, leads you to the ' 'lake, which warms to 20 degrees Celsius in the summer. Activities ' 'enjoyed here include rowing, and riding the summer toboggan run.', softWrap: true, ), ), ], ), ), ); }}Copy the code
In the second release, we had a large build method widget that was difficult to read, understand, and maintain.
Now let’s see how I would rewrite it:
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter layout demo', home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Flutter layout demo'), ), body: ListView( children: [ const _Header(), const _SubHeader(), const _Buttons(), const _Description(), ], ), ); } } class _Header extends StatelessWidget { const _Header({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Image.asset( 'images/lake.jpg', width: 600, height: 240, fit: BoxFit.cover, ); } } class _SubHeader extends StatelessWidget { const _SubHeader({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _Title(), const _SubTitle(), ], ), ), const _Likes(), ], ), ); } } class _Title extends StatelessWidget { const _Title({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Oeschinen Lake Campground', style: TextStyle( fontWeight: FontWeight.bold, ), ), ); } } class _SubTitle extends StatelessWidget { const _SubTitle({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Text( 'Kandersteg, Switzerland', style: TextStyle( color: Colors.grey[500], ), ); } } class _Likes extends StatelessWidget { const _Likes({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Row( children: <Widget>[ Icon( Icons.star, color: Colors.red[500], ), Text('41'), ], ); } } class _Buttons extends StatelessWidget { const _Buttons({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Container( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const _Button(icon: Icons.call, text: 'CALL'), const _Button(icon: Icons.share, text: 'ROUTE'), const _Button(icon: Icons.share, text: 'SHARE'), ], ), ); } } class _Button extends StatelessWidget { const _Button({ Key key, @required this.icon, @required this.text, }) : assert(icon ! = null), assert(text ! = null), super(key: key); final IconData icon; final String text; @override Widget build(BuildContext context) { Color color = Theme.of(context).primaryColor; return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( text, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: color, ), ), ), ], ); } } class _Description extends StatelessWidget { const _Description({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), child: Text('Lake Oeschinen lies at the foot of the Bluemlisalp in the Bernese ' 'Alps. Situated 1,578 meters above sea level, it is one of the ' 'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a ' 'half-hour walk through pastures and pine forest, leads you to the ' 'lake, which warms to 20 degrees Celsius in the summer. Activities ' 'enjoyed here include rowing, and riding the summer toboggan run.', softWrap: true, ), ); }}Copy the code
Don’t you think it’s easier to read?
What are the advantages of 🤔?
I understand why tutorials don’t do this often: It requires more lines (100 in my case), and people might wonder why we’re creating so many other widgets. Since tutorials are meant to focus on one concept, writing them this way can be counterproductive. But as a result, new adopters may be inclined to place a large widget tree in their build method. Let’s look at the benefits of having a unique widget for each part of the layout:
readability
We create a widget for each semantic part of the layout. Therefore, each widget has a smaller build method. It’s easier to read because you don’t have to scroll to get to the end of the widget.
understandability
Each widget has a name that matches its role, which is called semantic naming. By doing this, when we read the code, it is easier to map in our mind what part of the code matches what we see on the application. I see two improvements in comprehensibility here: \1. When we read such a widget referenced elsewhere, we almost know what it does without having to look at its implementation. 2. Before we read about how to build a semantically named widget, we have a general idea of its content.
maintainability
If you have to replace a component or change a part, it will only be in one place, separated from the rest of the other widgets. Thanks to this approach, it is less error-prone because the role of each widget is well defined. It will also be easier to share parts of the layout in your application or even on another page in another application.
Performances
All of the previous reasons should be enough for you to create a Flutter application in this way, but there is another benefit: we improve the performance of the application because each widget can be rebuilt separately from the others (this is not true if we use methods to separate parts of our layout). For example, suppose we have to increment the number next to the red star when we click on it. In this version, we can make _Likes a StatefulWidget and handle the increments here. When the user clicks on a star, only the _Likes widget is rebuilt. In the first version, MyApp if we set it to StatefulWidget.
The Flutter documentation also explains this best practice:
When setState() is raised in state, all descendant widgets are rebuilt. Therefore, localize the setState() call to the subtree part of the UI that actually needs to change. If the change is contained in a small part of the tree, avoid calling setState() high up in the tree.
Another advantage is the ability for const to use the keyword more frequently. Widgets can then be cached and reused. As the Flutter documentation states:
It is much more efficient to reuse widgets than to create new (but identically configured) widgets.
⚡️ How to improve work efficiency?
As you can see, we wrote more code by creating a widget for each semantic part of the layout. We can use the Stless and Stful fragments provided with the Dart extension in Visual Studio Code,
For my own needs, I created new fragments called Sless and Sful so THAT I was more productive than ever before. If you want to use them in Visual Studio Code, you must follow this document and add the following:
{
"Flutter stateless widget": {
"scope": "dart",
"prefix": "sless",
"description": "Insert a StatelessWidget",
"body": [
"class $1 extends StatelessWidget {",
" const $1({",
" Key key,",
" }) : super(key: key);",
"",
" @override",
" Widget build(BuildContext context) {",
" return Container(",
" $2",
" );",
" }",
"}"
]
},
"Flutter stateful widget": {
"scope": "dart",
"prefix": "sful",
"description": "Insert a StatefulWidget",
"body": [
"class $1 extends StatefulWidget {",
" const $1({",
" Key key,",
" }) : super(key: key);",
"",
" @override",
" _$1State createState() => _$1State();",
"}",
"",
"class _$1State extends State<$1> {",
" @override",
" Widget build(BuildContext context) {",
" return Container(",
" $2",
" );",
" }",
"}"
]
},
}
Copy the code
Conclusion 📕
I believe this is a good way to write a Flutter application, and I hope you do too. If not, I would be interested in your opinion 😉!
From now on, remember these words: “Everything’s a widget but don’t put Everything in one widget!” .