Strings #

String in Dart is an immutable sequence of UTF-16 code units — every operation that appears to “modify” a string actually produces a new String object. Dart’s built-in library provides dozens of methods for string manipulation: searching, replacing, splitting, case transformations, padding, encoding, and more. Understanding these methods and when each is best used is a day-to-day skill in almost every Dart program.

Creating Strings #

// Literals — single or double quotes, both equivalent
String s1 = 'Hello, World!';
String s2 = "Hello, World!";

// Multi-line — triple quotes
String multiline = '''
First line
Second line
Third line
''';

// Raw strings — backslashes aren't interpreted as escapes
String path = r'C:\Users\budi\Documents'; // \U and \b aren't escape sequences
String regex = r'\d+\.\d{2}';            // easier to read for regex

// String interpolation
String nama = 'Budi';
int umur = 25;
print('Name: $nama, Age: $umur years');          // simple variables
print('Birth year: ${2024 - umur}');              // expressions in ${}
print('Uppercase: ${nama.toUpperCase()}');          // method calls in ${}

// Concatenation — use interpolation, not +
String gabung = 'Hello' + ', ' + 'World';         // okay but verbose
String lebihBaik = 'Hello, World';                // direct
String dinamis = 'Hello, $nama!';                 // most idiomatic

Basic Properties #

String teks = 'Pemrograman Dart';

// Length in code units (UTF-16), not visual character count
print(teks.length);          // 16

// Check for empty strings
print(teks.isEmpty);         // false
print(teks.isNotEmpty);      // true
print(''.isEmpty);           // true

// Character access
print(teks[0]);              // 'P' — first character
print(teks[teks.length - 1]); // 't' — last character

// Code unit access (UTF-16 integer value)
print(teks.codeUnitAt(0));   // 80 (ASCII code for 'P')
print(teks.codeUnits);       // [80, 101, 109, ...] — all code units

// Runes — for Unicode characters beyond the BMP
String emoji = '👋🌍';
print(emoji.length);         // 4 (because each emoji = 2 UTF-16 code units)
print(emoji.runes.length);   // 2 (actual Unicode characters)
for (final rune in emoji.runes) {
  print(String.fromCharCode(rune)); // '👋', '🌍'
}

Searching and Matching #

String teks = 'Belajar pemrograman Dart dengan Dart';

// Contains a substring
print(teks.contains('Dart'));              // true
print(teks.contains('Python'));           // false
print(teks.contains(RegExp(r'\d+')));    // false — no digits

// Starts/ends with
print(teks.startsWith('Belajar'));        // true
print(teks.endsWith('Dart'));             // true
print(teks.startsWith('dart'));           // false — case sensitive!

// Occurrence positions
print(teks.indexOf('Dart'));             // 20 — first occurrence
print(teks.lastIndexOf('Dart'));         // 32 — last occurrence
print(teks.indexOf('Python'));           // -1 — not found

// indexOf with a starting position
print(teks.indexOf('Dart', 21));         // 32 — search after index 21

// Check whether a string matches a pattern
final emailRegex = RegExp(r'^[\w\.-]+@[\w\.-]+\.\w{2,}$');
print(emailRegex.hasMatch('[email protected]'));  // true
print(emailRegex.hasMatch('bukan-email'));       // false

String Transformations #

Case #

String campur = 'hElLo WoRlD';

print(campur.toUpperCase());    // 'HELLO WORLD'
print(campur.toLowerCase());    // 'hello world'

// Title case — no built-in, needs a manual implementation
String titleCase(String s) {
  return s.split(' ')
      .map((kata) => kata.isEmpty
          ? kata
          : '${kata[0].toUpperCase()}${kata.substring(1).toLowerCase()}')
      .join(' ');
}
print(titleCase('pemrograman dart')); // 'Pemrograman Dart'

Trim — Remove Whitespace #

String kotor = '   spasi di mana-mana   \n\t';

print(kotor.trim());        // 'spasi di mana-mana' — trim both sides
print(kotor.trimLeft());    // 'spasi di mana-mana   \n\t' — trim left only
print(kotor.trimRight());   // '   spasi di mana-mana' — trim right only

// Custom character trimming isn't natively supported — use replaceAll
String dolar = '$$harga$$';
String bersih = dolar.replaceAll(RegExp(r'^\$+|\$+$'), ''); // 'harga'

Replace #

String teks = 'Dart adalah bahasa yang bagus. Dart sangat cepat.';

// Replace all occurrences
print(teks.replaceAll('Dart', 'Kotlin'));
// 'Kotlin adalah bahasa yang bagus. Kotlin sangat cepat.'

// Replace only the first occurrence
print(teks.replaceFirst('Dart', 'Flutter'));
// 'Flutter adalah bahasa yang bagus. Dart sangat cepat.'

// Replace with a regex
String camelCase = 'namaDepanPengguna';
String snakeCase = camelCase.replaceAllMapped(
  RegExp(r'[A-Z]'),
  (m) => '_${m.group(0)!.toLowerCase()}',
); // 'nama_depan_pengguna'

// replaceRange — replace a substring by index
String s = 'Halo, Dunia!';
print(s.replaceRange(6, 11, 'Flutter')); // 'Halo, Flutter!'

Split and Join #

String csv = 'Jakarta,Bandung,Surabaya,Medan';

// Split by a delimiter
List<String> kota = csv.split(',');
print(kota); // ['Jakarta', 'Bandung', 'Surabaya', 'Medan']

// Split with a regex
String teks = 'kata1  kata2   kata3';
List<String> kata = teks.split(RegExp(r'\s+'));
print(kata); // ['kata1', 'kata2', 'kata3']

// Split into individual characters
List<String> huruf = 'Dart'.split('');
print(huruf); // ['D', 'a', 'r', 't']

// Split with a limited number of parts
// No built-in — manual implementation
List<String> splitN(String s, String sep, int n) {
  final parts = s.split(sep);
  if (parts.length <= n) return parts;
  return [
    ...parts.sublist(0, n - 1),
    parts.sublist(n - 1).join(sep),
  ];
}

// Join — combine a list into a string
List<String> buah = ['apel', 'jeruk', 'mangga'];
print(buah.join(', '));  // 'apel, jeruk, mangga'
print(buah.join(' - ')); // 'apel - jeruk - mangga'
print(buah.join());      // 'apeljerukmanggga' — no separator

Substring and Slicing #

String teks = 'Pemrograman Dart';

// substring(start) — from the index to the end
print(teks.substring(12));        // 'Dart'

// substring(start, end) — from start to end (exclusive)
print(teks.substring(0, 11));     // 'Pemrograman'
print(teks.substring(12, 16));    // 'Dart'

// First and last characters
print(teks[0]);                   // 'P'
print(teks[teks.length - 1]);     // 't'

// Extract based on a search
final idx = teks.indexOf(' ');
final kata1 = teks.substring(0, idx);   // 'Pemrograman'
final kata2 = teks.substring(idx + 1);  // 'Dart'

Padding #

// padLeft — add characters to the left until a certain length
print('42'.padLeft(5));         // '   42' — padding with spaces
print('42'.padLeft(5, '0'));    // '00042' — padding with '0'
print('Dart'.padLeft(8, '-')); // '----Dart'

// padRight — add characters to the right
print('Dart'.padRight(8));      // 'Dart    '
print('Dart'.padRight(8, '.')); // 'Dart....'

// Common use: format numbers with a fixed width
for (int i = 1; i <= 10; i++) {
  print('${i.toString().padLeft(2)}: item');
  // ' 1: item'
  // ' 2: item'
  // '10: item'
}

// Digital clock formatting
int jam = 9, menit = 5, detik = 3;
print('${'$jam'.padLeft(2, '0')}:${'$menit'.padLeft(2, '0')}:${'$detik'.padLeft(2, '0')}');
// '09:05:03'

Encoding and Decoding #

import 'dart:convert';

// String to bytes and back
String teks = 'Halo, Dunia! 🌍';

// UTF-8 encoding — the standard for web and files
List<int> utf8Bytes = utf8.encode(teks);
print(utf8Bytes.length); // larger than teks.length because the emoji = 4 bytes

String decoded = utf8.decode(utf8Bytes);
print(decoded); // 'Halo, Dunia! 🌍'

// Latin-1 / ISO-8859-1
List<int> latin1Bytes = latin1.encode('Halo'); // only ASCII characters
String latin1Decoded = latin1.decode([72, 97, 108, 111]); // 'Halo'

// Base64
String original = 'Data rahasia: 12345';
String base64Encoded = base64Encode(utf8.encode(original));
print(base64Encoded); // 'RGF0YSByYWhhc2lhOiAxMjM0NQ=='

String base64Decoded = utf8.decode(base64Decode(base64Encoded));
print(base64Decoded); // 'Data rahasia: 12345'

// URL encoding
String url = 'https://example.com/pencarian?q=dart programming&lang=id';
String encoded = Uri.encodeFull(url);
print(encoded); // URL with special characters encoded

String queryParam = 'dart & flutter';
String paramEncoded = Uri.encodeComponent(queryParam);
print(paramEncoded); // 'dart%20%26%20flutter'
print(Uri.decodeComponent(paramEncoded)); // 'dart & flutter'

StringBuffer — Building Strings Efficiently #

For building strings from many parts incrementally, StringBuffer is far more efficient than repeated + concatenation:

// ANTI-PATTERN: concatenation in a loop — O(n²) memory
String hasilBuruk = '';
for (int i = 0; i < 10000; i++) {
  hasilBuruk += 'item $i\n'; // ✗ creates a new string every iteration
}

// CORRECT: StringBuffer — O(n) memory
final buffer = StringBuffer();
for (int i = 0; i < 10000; i++) {
  buffer.write('item $i');
  buffer.writeln();  // add a newline
}
final hasilBaik = buffer.toString(); // ✓ only materialized once

// StringBuffer methods
final sb = StringBuffer();
sb.write('Halo');           // add a string without a newline
sb.writeln(', Dunia!');     // add a string with a newline
sb.writeAll(['a', 'b', 'c'], ', '); // join a list with a separator
sb.writeCharCode(33);       // add a character from a code point ('!')
sb.clear();                 // reset the buffer

print(sb.length);           // current accumulated length
print(sb.isEmpty);          // true after clear

String Comparison #

// Equality — case sensitive
print('dart' == 'dart');   // true
print('Dart' == 'dart');   // false

// compareTo — lexicographic (based on Unicode order)
print('a'.compareTo('b')); // -1 (a before b)
print('b'.compareTo('a')); // 1  (b after a)
print('a'.compareTo('a')); // 0  (equal)

// Case-insensitive comparison
bool samaIgnoreCase(String a, String b) =>
    a.toLowerCase() == b.toLowerCase();

print(samaIgnoreCase('Dart', 'dart')); // true
print(samaIgnoreCase('FLUTTER', 'flutter')); // true

// Sorting string lists
List<String> bahasa = ['Kotlin', 'dart', 'Python', 'go'];
bahasa.sort(); // lexicographic sort — uppercase before lowercase
print(bahasa); // ['Kotlin', 'Python', 'dart', 'go']

// Case-insensitive sort
bahasa.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
print(bahasa); // ['dart', 'go', 'Kotlin', 'Python']

Unicode and Runes #

Dart strings use UTF-16 — characters beyond the BMP (Basic Multilingual Plane) use two code units (a surrogate pair). To work with Unicode characters correctly:

// A string with an emoji
String s = 'Dart 🎯';

// length counts UTF-16 code units, not visual characters
print(s.length);           // 7 (5 ASCII characters + 2 code units for the emoji)

// runes counts the actual Unicode code points
print(s.runes.length);     // 6 (5 letters + 1 emoji)

// Iterate per Unicode character (not per code unit)
for (final rune in s.runes) {
  final karakter = String.fromCharCode(rune);
  print('$karakter (U+${rune.toRadixString(16).toUpperCase()})');
}

// Create a string from a code point
print(String.fromCharCode(9786));  // '☺' (U+263A)
print(String.fromCharCodes([72, 97, 108, 111])); // 'Halo'

// The characters package — for proper grapheme clusters
// dart pub add characters
import 'package:characters/characters.dart';
final characters = '🇮🇩 Dart'.characters; // flag emoji = 2 code points but 1 grapheme
print(characters.length); // 7 grapheme clusters

Complete Method Reference — Quick Cheat Sheet #

String s = '  Halo, Dart!  ';

// Information
s.length          // length in code units
s.isEmpty         // whether empty
s.isNotEmpty      // whether not empty
s.codeUnitAt(0)   // code unit at a specific index
s.codeUnits       // all code units
s.runes           // all Unicode code points

// Searching
s.contains('Dart')          // whether it contains a substring
s.startsWith('  Halo')      // whether it starts with
s.endsWith('!  ')           // whether it ends with
s.indexOf('Dart')           // position of the first occurrence (-1 if absent)
s.lastIndexOf('a')          // position of the last occurrence

// Transformations
s.trim()                    // remove left and right whitespace
s.trimLeft()                // remove left whitespace
s.trimRight()               // remove right whitespace
s.toUpperCase()             // all uppercase
s.toLowerCase()             // all lowercase
s.padLeft(20)               // left padding with spaces
s.padLeft(20, '0')          // left padding with a specific character
s.padRight(20)              // right padding

// Extraction
s.substring(2)              // substring from an index
s.substring(2, 6)           // substring range
s.split(',')                // split by a delimiter
s[0]                        // character access (code unit)

// Replacement
s.replaceAll('Dart', 'Kotlin')          // replace all
s.replaceFirst('Dart', 'Kotlin')        // replace the first
s.replaceRange(2, 6, 'Baru')           // replace an index range
s.replaceAllMapped(regex, (m) => ...)  // replace with a function

String Anti-Patterns #

Concatenation in a Loop #

// ANTI-PATTERN: O(n²) because every + creates a new object
String hasil = '';
for (final item in daftarPanjang) {
  hasil += item + ', '; // ✗ very slow for large lists
}

// CORRECT: join for lists
String hasil = daftarPanjang.join(', '); // ✓ O(n)

// CORRECT: StringBuffer if you need more complex logic
final buffer = StringBuffer();
for (int i = 0; i < daftarPanjang.length; i++) {
  buffer.write(daftarPanjang[i]);
  if (i < daftarPanjang.length - 1) buffer.write(', ');
}
String hasil = buffer.toString(); // ✓ O(n)

Accidental Case-Sensitive Comparisons #

// ANTI-PATTERN: comparing user input case-sensitively
String input = ambilInputUser(); // 'ADMIN'
if (input == 'admin') { // ✗ won't match
  ijinkanAkses();
}

// CORRECT: normalize the case before comparing
if (input.toLowerCase() == 'admin') { // ✓
  ijinkanAkses();
}

Summary #

  • Dart strings are immutable — every transformation operation produces a new String, not a modification of the existing one. This is safe but requires StringBuffer for efficiency in loops.
  • length counts UTF-16 code units, not visual characters. For Unicode characters beyond the BMP (emojis, many Asian characters), use runes.length or the characters package.
  • Raw strings r'...' are very useful for regex and file paths — backslashes aren’t interpreted as escape sequences.
  • String interpolation '$variable' and '${expression}' is more idiomatic than + concatenation. Use + only to join two string literals.
  • StringBuffer for building strings in loops — far more efficient than += concatenation, which becomes O(n²) for large strings.
  • split() + join() for list-string transformations — cleaner than manual loops.
  • padLeft(n, '0') for formatting numbers with a fixed width — very useful for clocks, dates, and sequence numbers.
  • toUpperCase()/toLowerCase() before comparing strings from user input — don’t assume consistent capitalization.
  • replaceAllMapped for dynamic transformations — when the replacement text depends on the matched content, not a static string.
  • utf8.encode/utf8.decode from dart:convert for converting strings to bytes — essential when working with files, networks, and APIs.

← Previous: Articles & Resources   Next: IO →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact