skip to content

In Dart, why build a long string such as a booking confirmation with StringBuffer instead of += in a loop, and what does StringBuffer offer?

level: middleimportance: should knowfreq 42%

answer

  1. String is immutable
  2. each += allocates a new string
  3. concatenation deferred until toString()
  4. write, writeln, writeAll with separator
  5. writeln always emits \n

basics

~10 s

Dart strings are immutable, so every += creates a new string and copies what came before. StringBuffer collects pieces with write, writeln and writeAll and joins them once when toString() is called.

solid answer

~40 s

A Dart `String` cannot change, so `s += piece` builds a brand-new string containing everything so far plus the piece; in a loop that repeats the copying of an ever-longer prefix. `StringBuffer` implements `StringSink` and only accumulates: the dart.dev tour notes that it does not create a new `String` until `toString()` is called. Its API is small: `write(object)` appends `object.toString()`, `writeln([obj])` appends and then `\n`, always `\n` and never `\r\n`, `writeAll(iterable, separator)` joins a collection, `writeCharCode` appends one code point, and `length`, `isEmpty` and `clear()` let you reuse it. For a handful of pieces, interpolation is clearer; the buffer pays off when the number of pieces grows with the data, such as one line per night of a stay.

code

dart · 15 lines
dart
String confirmation(String guest, List<(String, double)> nights) {
  final out = StringBuffer()
    ..writeln('Booking for $guest')
    ..writeln('-' * 24);
  for (final (date, price) in nights) {
    out.writeln('${date.padRight(12)}${price.toStringAsFixed(2).padLeft(12)}');
  }
  out.write('Nights: ');
  out.writeAll(nights.map((n) => n.$1), ', ');
  return out.toString();
}

void main() {
  print(confirmation('Ana', [('2026-10-24', 149.5), ('2026-10-25', 162.0)]));
}

go deeper

for a junior

Remember that strings are immutable and that StringBuffer with write, writeln and toString() is how you build text in a loop.

for a middle

Explain the repeated copying of +=, the deferred join in toString(), writeAll with a separator, and the fixed newline of writeln.

for a senior

Choose the right tool per case: interpolation for a few pieces, join for lists, StringBuffer for data-driven output, and a stream sink for very large output.

for a principal

Accept a StringSink in report-building APIs so the same code writes to a buffer in tests and to a file or network sink in production.

## Strings cannot change The `String` documentation says it directly: strings are immutable. Every operation that seems to modify one, such as `+`, `substring`, `replaceAll` or `toUpperCase`, returns a new string and leaves the original alone. That makes `+=` in a loop wasteful: 1. The first iteration creates a string with piece 1. 2. The second creates a new string with pieces 1 and 2, copying piece 1. 3. The n-th copies everything written so far again. The total copying grows with the square of the output length in the naive model, and every intermediate string becomes garbage for the collector. For three pieces nobody will notice. For a confirmation that lists every night of a long stay, or an export of many bookings, it adds up. ## What StringBuffer does instead `StringBuffer` is a class in `dart:core` that implements `StringSink`. It collects pieces and joins them once. The library tour puts it as: a `StringBuffer` does not generate a new `String` object until `toString()` is called. | Member | Behaviour | |---|---| | `StringBuffer([Object content = ""])` | starts with optional initial content | | `write(Object? object)` | appends `object.toString()`, so numbers and your own types work | | `writeln([Object? obj = ""])` | appends `obj`, then `"\n"` | | `writeAll(Iterable objects, [String separator = ""])` | appends each element with the separator between | | `writeCharCode(int charCode)` | appends one Unicode code point | | `length`, `isEmpty`, `isNotEmpty` | constant-time size checks | | `clear()` | empties the buffer for reuse | | `toString()` | returns the accumulated text as one `String` | ## Details that show real use - **`writeln` is always `\n`.** The documentation notes it never uses a platform-specific ending such as `\r\n` on Windows. - **`writeln(null)` writes the text `null`** before the newline. Call `writeln()` with no argument, or with an empty string, to emit only a newline. - **Cascades read well:** `StringBuffer()..write('a')..writeln('b')` builds and configures in one expression. - **`StringSink` is the shared interface**, so a function that writes a report can accept any `StringSink`, a `StringBuffer` in tests or an output sink elsewhere. ## Everyday string methods around it Building text usually needs a few `String` methods too: - `padLeft(width, [padding])` and `padRight` align columns: `'42'.padLeft(4, '0')` is `'0042'`. - `trim()`, `trimLeft()` and `trimRight()` clean input. - `split(pattern)` and `join` on a list convert between text and pieces. - `contains`, `startsWith`, `replaceAll` and `substring` search and slice; `replaceAll` and `contains` accept a `Pattern`, so a `String` or a `RegExp`. - `'-' * 24` repeats a string, handy for separator lines. ## Measuring instead of guessing Whether `+=` matters for your output size is an empirical question. `dart:core` has a `Stopwatch` for exactly this: 1. Create it with `Stopwatch()`; it starts stopped with zero elapsed time. 2. Call `start()`, run the code, then `stop()`. 3. Read `elapsed` as a `Duration`, or `elapsedMilliseconds` and `elapsedMicroseconds` as integers. 4. Call `reset()` to measure again; it keeps the running state. `Stopwatch` measures only while it is running, and the `DateTime` documentation itself points to it for measuring timespans. Run each variant several times and compare; a single run mostly measures warm-up. ## When not to reach for it - **A few fixed pieces:** interpolation such as `'Booking for $guest'` is clearer, and the Effective Dart guide prefers interpolation for composing strings and values. - **Joining a list with a separator:** `list.join(', ')` is one call and already efficient. - **Very large output going to a file or socket:** write directly to that sink instead of building one huge string in memory. ## Summary for an interview Say that strings are immutable, so repeated `+=` recopies, that `StringBuffer` defers the join to `toString()`, and name `write`, `writeln` and `writeAll` with its separator. Mentioning the `writeln(null)` quirk or the fixed `\n` line ending shows hands-on experience.

  • What does buffer.writeln(null) write?
    The four characters `null` followed by a newline. `writeln` writes the argument's `toString()` and then `\n`, and `null.toString()` is `'null'`. To emit only a newline, call `writeln()` with no argument or with an empty string.
  • Is StringBuffer ever the wrong choice?
    Yes. For a few known pieces, interpolation is shorter and clearer. For joining a list, `join` is one call. For very large output headed to a file or socket, write to that sink directly so the whole text never has to sit in memory as one string.

saying these in an interview costs you the question

  • += on a String modifies the existing string in place
  • StringBuffer.writeln uses \r\n on Windows
  • StringBuffer only accepts String arguments
  • Interpolation should always be replaced by StringBuffer for performance
  • writeln(null) writes just a newline