Encode and decode a string
Dart strings are UTF-16 internally, so convert to UTF-8 bytes with utf8.encode before encoding:
import 'dart:convert';
void main() {
final enc = base64Encode(utf8.encode('héllo wörld'));
print(enc); // aMOpbGxvIHfDtnJsZA==
print(utf8.decode(base64Decode(enc))); // héllo wörld
}
base64Encode(bytes) is shorthand for base64.encode(bytes), and base64Decode(s) for base64.decode(s). If the decoded bytes are not UTF-8, utf8.decode throws FormatException: Invalid UTF-8 byte; pass allowMalformed: true to get replacement characters instead.
The same two calls build an HTTP Basic Auth header, for example for package:http:
final headers = {'Authorization': 'Basic ${base64Encode(utf8.encode('user:pass'))}'};
// Authorization: Basic dXNlcjpwYXNz
The Basic Auth header generator shows the same value for any credentials.
URL-safe Base64 and normalize()
base64Url is a second codec with the -_ alphabet. Both encoders always write padding, so strip it if your format wants none:
final d = utf8.encode('<<???>>');
print(base64Url.encode(d)); // PDw_Pz8-Pg==
print(base64Url.encode(d).replaceAll('=', '')); // PDw_Pz8-Pg
Decoding is where Dart is friendlier than most languages. Both decoders accept both alphabets, but they require correct padding. normalize fixes that and also converts the alphabet:
base64Url.normalize('PDw_Pz8-Pg'); // PDw_Pz8-Pg==
base64.normalize('PDw_Pz8-Pg'); // PDw/Pz8+Pg==
utf8.decode(base64Url.decode(base64Url.normalize('PDw_Pz8-Pg'))); // <<???>>
utf8.decode(base64Decode('PDw_Pz8-Pg==')); // <<???>>
normalize also throws FormatException if the input is not valid Base64, so it doubles as a validator. JWT segments are unpadded Base64URL; see Base64 vs Base64URL and padding explained, or paste one into the JWT decoder.
Reading the payload of a JWT takes one line once you have normalize:
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(jwt.split('.')[1])));
print(payload); // {"sub":"1234"}
print(jsonDecode(payload)['sub']); // 1234
This only reads the claims; it does not verify the signature, so never trust them for authorization on the client.
Encode and decode a file
import 'dart:io';
final bytes = await File('hello.txt').readAsBytes();
print(base64Encode(bytes)); // aGVsbG8K
await File('copy.txt').writeAsBytes(base64Decode('aGVsbG8K'));
Streaming large files
base64.encoder and base64.decoder are stream transformers, so a file can be converted chunk by chunk:
// encode: bytes -> Base64 text -> ASCII bytes on disk
await File('big.bin')
.openRead()
.transform(base64.encoder)
.transform(ascii.encoder)
.pipe(File('big.b64').openWrite());
// decode: ASCII bytes -> String chunks -> bytes
await File('big.b64')
.openRead()
.transform(ascii.decoder)
.transform(base64.decoder)
.pipe(File('big.out').openWrite());
The transformers keep leftover bytes between chunks, so chunk sizes do not matter. In both directions, a 300 KB test file came out byte for byte the same as with the in-memory functions.
Flutter: images and data URIs
To show a Base64 image:
Image.memory(base64Decode(b64))
To produce Base64 from an asset or a picked file, read the bytes and call base64Encode. For data URIs, UriData handles the prefix:
UriData.parse('data:text/plain;base64,aGk=').contentAsBytes(); // [104, 105]
UriData.fromBytes(pngBytes, mimeType: 'image/png').toString(); // data:image/png;base64,...
Passing a data URI straight to base64Decode fails with Invalid character (at character 5), the : after data. To check what an unknown string contains, Base64 to image previews it in the browser.
Validation and FormatException messages
All decoding errors are FormatException. toString() includes the message, the input and a caret under the problem; e.message and e.offset give the parts separately. With base64Decode:
| Input | Message |
|---|---|
aGk | Invalid length, must be multiple of four (at character 4) |
a | Invalid length, must be multiple of four (at character 2) |
aGk=!! | Invalid padding character (at character 5) |
aGk== | Invalid padding character (at character 5) |
aGl= | Invalid encoding before padding (at character 4) |
"aGVs\nbG8K" | Invalid character (at character 5) |
Dart is strict on two points where many languages are lenient: it rejects newlines (strip them for MIME or PEM input) and rejects non-zero padding bits (aGl=). A validity check:
bool isBase64(String s) {
try {
base64Decode(s);
return true;
} on FormatException {
return false;
}
}
If a string fails and the message does not tell you enough, the Base64 validator marks the problem; common Base64 errors covers the causes.
Pitfalls
- Encoding
codeUnits.base64Encode('é'.codeUnits)returns6Q==(Latin-1, not UTF-8), and'✓'.codeUnitsthrowsArgumentError: Invalid argument (Not a byte value at index 0: 0x2713). Always go throughutf8.encode. - Line breaks from other systems. MIME output from email or
base64on Linux contains\n; remove whitespace withs.replaceAll(RegExp(r'\s'), '')first. - Assuming URL-safe output from
base64Encode. It writes+and/. Usebase64Url.encodefor anything that goes into a URL. - No MIME wrapping. The codecs never insert line breaks. Split into 76-character lines yourself if needed.
FAQ
How do I show a Base64 image in Flutter?
Decode to bytes and use Image.memory(base64Decode(b64)). If the string is a data URI, strip everything up to the comma first, or use UriData.parse(uri).contentAsBytes().
Why does base64Decode say 'Invalid length, must be multiple of four'?
The input has no padding, which is typical for JWTs and URL-safe tokens. Call base64.normalize(s) or base64Url.normalize(s) first; it adds the missing =.
Does Dart's base64Decode accept URL-safe input?
Yes. The decoder accepts both +/ and -_, so base64Decode('PDw_Pz8-Pg==') works. Only the encoders differ: base64 writes +/, base64Url writes -_.