Bismillahir-Rahmanir-Rahim
UPDATE: spacing
property introduced in Flutter 3.27, alhamdulillah!
Yep, you heard that right. If you want to achieve spacing between elements in either a Row
or Column
widget, you can use the spacing
property.
As-salamu 'alaykum wa rahmatullahi wa barakaatuh!
Result
The Problem
So you're developing your super amazing app and you love the Row
and Column
widgets in Flutter.
Let's say you want to add spacing between elements. There are two popular methods:
Use the
MainAxisAlignment
propertyUse the
Spacer()
widget or theSizedBox()
widget in Flutter
The limitation of the first method is that it does not allow you to specify a minimum gap between elements. If the row is shrunk too much, the elements will collide into each other. To avoid that, you may use a SizedBox
with a width of, let's say, 10. Let's take an example of a Row
with three Text
widgets:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Element 1"),
Text("Element 2"),
Text("Element 3"),
],
)
If you now want to insert a SizedBox(width: 10)
, a naive approach would be to manually insert it between each element of the Row
like this:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Element 1"),
SizedBox(width: 10),
Text("Element 2"),
SizedBox(width: 10),
Text("Element 3"),
],
)
Note that the problem gets even tougher if you have a row or column of conditional children. What I mean by conditional is this: Imagine you're making a task productivity application. Your Task
model has a property called description
. However, you won't display the description text in your task tile if it is empty, right? You might do something like this:
Column(
children: [
Text(task.title),
task.description.isNotEmpty
? Text(task.description)
: null
].whereType<Widget>().toList(),
)
If you want to insert a SizedBox
between the title and description, you have to check whether there is a description or not. Otherwise, you'll just be adding empty space. You could do this:
Column(
children: [
Text(task.title),
...task.description.isNotEmpty
? [SizedBox(height: 10), Text(task.description)]
: [null]
].whereType<Widget>(),
)
You will soon realize you're code is getting more and more unreadable and unnecessarily complex.
That is NOT Cool!
Yeah, I know you would say that. What if we had 100 elements? Imagine the pain...
After bending over backwards and hitting my head hard against the wall, I have found an easy solution, Alhamdulillah. We will use dart's built-in List
extension methods.
Logic
Let's assume you have a List list1 = [0, 0, 0, 0] ;
.
Create a new list of lists by iterating over each element of
list1
List newList = list1 .map((element) => ["Interspersed Element", element]) .toList();
This will return a new
List
like such:print(newList); // [[Interspersed Element, 0], [Interspersed Element, 0], [Interspersed Element, 0], [Interspersed Element, 0]]
Now, flatten
newList
and remove the first element by skipping it. DO NOT use theremove
methods since they are dangerous when theList
is empty.skip
is safe.Creates an Iterable that provides all but the first
count
elements.When the returned iterable is iterated, it starts iterating over
this
, first skipping past the initialcount
elements. Ifthis
has fewer thancount
elements, then the resulting Iterable is empty. After that, the remaining elements are iterated in the same order as in this iterable.https://api.flutter.dev/flutter/dart-core/Iterable/skip.html
newList = newList.expand((e) => e).skip(1).toList(); print(newList); // [0, Interspersed Element, 0, Interspersed Element, 0, Interspersed Element, 0]
One-Liner
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Element 1"),
Text("Element 2"),
Text("Element 3"),
]
.map((e) => [SizedBox(width: 10), e])
.expand((e) => e)
.skip(1)
.toList()
)
To show interspersion visually, I would like to use a colored Container
instead:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text("Element 1"),
Text("Element 2"),
Text("Element 3"),
]
.map((e) => [Container(width: 10, color: Colors.blue), e])
.expand((e) => e)
.skip(1)
.toList(),
),
Creating an Extension Method
You can also create an extension method on List<Widget>
.
extension on List<Widget> {
List<Widget> intersperse(Widget item) {
return map((e) => [item, e])
.expand((e) => e)
.skip(1)
.toList();
}
}
Recalling our task app example, instead of all that complicated code, we can just do:
Column(
children: [
Text(task.title),
task.description.isNotEmpty
? Text(task.description)
: null
]
.whereType<Widget>()
.toList()
.intersperse(
SizedBox(height: 10),
),
)
Outer-Interspersion
What if you wanted the SizedBox
to be wrapped around the List
as well? Something like:
[
SizedBox(width: 10.0),
Text("1"),
SizedBox(width: 10.0),
Text("2"),
SizedBox(width: 10.0),
Text("3"),
SizedBox(width: 10.0),
]
For that, try:
import 'package:collection/collection.dart';
[Text("1"), Text("2"), Text("3")]
.mapIndexed((i, e) {
Widget it = SizedBox(width: 10);
return [if (i == 0) it, e, it];
})
.expand((e) => e)
.toList()
Do not forget to import the built-in collection
library.
If you don't want to import the library, you can do:
[Text("1"), Text("2"), Text("3")]
.indexed
.map((e) {
Widget it = SizedBox(width: 10);
return [if (e.$1 == 0) it, e.$2, it];
})
.expand((e) => e)
.toList()
JazakAllah for reading!
Was-salamu 'alaykum...