One-Line Trick to Intersperse a Dart List

One-Line Trick to Intersperse a Dart List

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:

  1. Use the MainAxisAlignment property

  2. Use the Spacer() widget or the SizedBox() 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] ;.

  1. Create a new list of lists by iterating over each element of list1

     List newList = list1
         .map((element) => ["Interspersed Element", element])
         .toList();
    
  2. This will return a new List like such:

     print(newList);
     // [[Interspersed Element, 0], [Interspersed Element, 0], [Interspersed Element, 0], [Interspersed Element, 0]]
    
  3. Now, flatten newList and remove the first element by skipping it. DO NOT use the remove methods since they are dangerous when the List 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 initial count elements. If this has fewer than count 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...