Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Decodificação Base64 em Dart: um guia completo

Ela chega numa resposta de API, dentro de uma URL, ou colada em um chamado de suporte: uma longa sequência de letras e dígitos com o ocasional +, /, - ou _, e talvez um par de sinais = pendurados no final. Alguém chama isso de Base64, e você precisa do que está dentro. Este guia é a receita em Dart para trazer isso de volta. Uma orientação rápida, porque a página inicial passeia pelo formato em profundidade: o Base64 reescreve cada três bytes de entrada como quatro caracteres de um alfabeto de 64 caracteres, e acrescenta um ou dois pads de = no final quando o último bloco fica curto. A decodificação é a direção de encolher dessa troca: quatro caracteres entram, três bytes saem, então o resultado sempre ocupa cerca de um quarto a menos de espaço que a entrada.

Boa notícia: não há nada para instalar. O Base64 vem na biblioteca dart:convert desde o Dart 1.13 em 2015, e a API está estável desde o Dart 2.0 em 2018. Um import te dá um decodificador rápido e estrito que lê tanto o alfabeto padrão quanto o alfabeto URL-safe.

Um limite honesto: este é o lado decodificador da história. Você vai aprender o que o decodificador aceita e recusa, como o padding funciona, como transformar bytes de volta em texto sem mojibake, e como encontrar o Base64 em JWTs, data URIs, arquivos, streams, e-mail, configuração e linha de comando. A outra direção, empacotar bytes em uma string, tem o seu próprio guia, linkado no final deste.

Quatro portas para uma única máquina estrita

Aqui está a superfície pública inteira que você vai usar, toda ela no dart:convert:

Entrada O que é Quando usar
base64Decode(source) função de nível superior, decodifica para um Uint8List decodificação do dia a dia, quase sempre esta
base64.decode(source) o método de decodificação do codec, comportamento idêntico você quer o codec para fuse ou transformações de stream
base64Url.decode(source) o método de decodificação do codec URL-safe a entrada foi documentada como URL-safe (a máquina é a mesma)
base64Url.normalize(source) valida e repara uma string, devolve ela com padding a entrada pode vir sem padding, misturar alfabetos ou usar percent-escapes

Dois pontos para notar. Primeiro, os quatro caminhos levam ao mesmo decodificador: uma máquina de estados estrita com uma tabela de busca. Segundo, a última linha nem é um decodificador. É um posto de reparo, e ele se paga na primeira vez que um JWT sem padding ou um valor de config só meio limpo aparecerem.

Sua primeira decodificação

Noventa por cento da vida de decodificação cabe em cinco linhas. Aqui está o menor exemplo que mostra a forma inteira do trabalho:

import 'dart:convert';
void main() {
  final bytes = base64Decode('TWFu');
  final text = utf8.decode(bytes);
  print(text); // Man
}

Três frases sobre o que acabou de acontecer. Primeiro, o ponto de entrada devolve bytes, não texto: o base64Decode retorna um Uint8List, e isso é de propósito, porque o payload pode ser uma frase, um JPEG ou um hash, e nenhum deles deve ser tratado igual antes de você saber o que tem. Segundo, o salto de bytes para texto é uma etapa separada e explícita, com uma codificação explícita, e é nessa etapa que "café" vira mojibake se você for descuidado. Terceiro, a string vazia é um valor de primeira classe: o base64Decode('') te dá uma lista de comprimento zero, sem exceção e sem alarde.

O que o decodificador aceita e recusa

O decodificador do Dart é estrito por design. A RFC 4648 diz que as implementações devem rejeitar entrada com caracteres fora do alfabeto, e o Dart segue essa leitura à risca: sem pular espaços, sem ignorar quebras de linha, sem segundas chances. Quando a entrada está errada, você recebe um FormatException que mostra a entrada e aponta o caractere exato. Aqui está o comportamento com os causadores de problemas clássicos:

Entrada O que há de errado Erro exato
'SGVs bG8s' um espaço se infiltrou FormatException: Invalid character (at character 5)
'SGVs\nbG8s' uma quebra de linha se infiltrou FormatException: Invalid character (at character 5)
'SGVs$bG8s' um sinal de dólar não está no alfabeto FormatException: Invalid character (at character 5)
'Zm8' nenhum padding FormatException: Invalid length, must be multiple of four (at character 4)
'Zm8==' dois pads onde cabe um FormatException: Invalid padding character (at character 5)
'Zm=8' padding no meio dos dados FormatException: Invalid encoding before padding (at character 3)
'Zm8=xx' lixo depois dos pads FormatException: Invalid padding character (at character 5)
'Zé' um caractere não-ASCII FormatException: Invalid character (at character 2)

A posição na mensagem é uma contagem de caracteres começando em um, e a entrada é impressa logo abaixo do aponte, então bissecar um payload corrompido é rápido. Uma surpresa agradável se esconde na estriteza: o decodificador aceita os dois alfabetos. Um - ou _ no meio de uma string padrão não é problema, e um + ou / numa string URL-safe também não. A escolha do alfabeto só importa quando você é quem produz o texto, não quando está lendo.

Padding: o inegociável

Aqui está a regra que surpreende a maioria das pessoas: o decodificador do Dart exige padding correto. A entrada tem que ter um comprimento múltiplo de quatro caracteres, e os sinais = finais têm que estar presentes na quantidade exata certa. Não existe modo tolerante, não existe flag para afrouxar, e não existe configuração para mudar. Os motivos são sólidos: decodificação sem padding é ambígua em casos extremos, e a RFC avisa que decodificação liberal pode abrir um canal encoberto, então a leitura estrita é a segura. O que isso significa na prática:

Entrada Resultado
'' Uint8List vazio, sem erro
'QQ==' 1 byte: A
'QUI=' 2 bytes: AB
'QUJD' 3 bytes: ABC
'Zm8' FormatException: comprimento inválido
'Zm8==' FormatException: caractere de padding inválido

Quando a entrada vem de um sistema que remove o padding, e JWTs estão cheios de valores sem padding, a etapa de reparo é uma chamada ao normalize. Ele valida a string, converte os caracteres URL-safe para o alfabeto padrão e adiciona os pads faltantes:

import 'dart:convert';
void main() {
  final stripped = '-__--Q';
  final repaired = base64Url.normalize(stripped);
  print(repaired); // +//++Q==
  final bytes = base64Decode(repaired);
  print('decoded ${bytes.length} bytes'); // decoded 4 bytes
}

A surpresa do sinal de porcentagem

Esta aqui é uma originalidade do Dart. Quando o Base64 aparece numa data URI, algumas ferramentas fazem percent-encoding do padding, escrevendo %3D no lugar de =, porque um = nu pode significar "separador de parâmetro" na sintaxe de URL. A maioria das linguagens pediria para você fazer unescape primeiro. O decodificador do Dart não: a tabela de busca dele trata %3D como uma grafia nativa do caractere de padding, então você pode entregar o payload cru:

import 'dart:convert';
void main() {
  final fromDataUri = 'SGVsbG8%3D';
  final bytes = base64Decode(fromDataUri);
  print(utf8.decode(bytes)); // Hello
}

O escape é aceito exatamente onde o padding é legal, que é a posição final. Coloque %3D onde um = seria rejeitado e ele é rejeitado da mesma forma, e %25 falha na verificação de padding em vez disso - o % é o caractere de escape de padding nativo do Dart, então o decodificador o lê como um = escapado e rejeita o 2 com Invalid padding character. Na prática, isso significa que um payload ;base64, copiado direto das ferramentas de desenvolvimento de um navegador decodifica sem nenhum pré-processamento, um truque pequeno, mas genuinamente conveniente.

Base64 URL-safe

A RFC 4648 define um segundo alfabeto por um motivo: o alfabeto padrão tem três caracteres, +, / e =, que colidem com a sintaxe de URL. O alfabeto URL-safe, chamado de base64url na RFC, troca + por - e / por _, e muitas vezes joga fora o padding também. É o alfabeto dos JWTs, dos IDs de objeto, dos links de compartilhamento e de qualquer coisa que vive dentro de uma URL ou de um nome de arquivo.

No lado da decodificação, o Dart te dá uma resposta única: os dois alfabetos são lidos pela mesma máquina. base64Decode e base64Url.decode são dois nomes para o mesmo decodificador, então o único trabalho de verdade é o padding, porque produtores URL-safe muito frequentemente entregam sem ele. É exatamente para isso que o normalize existe:

import 'dart:convert';
void main() {
  final bytes = [0xfb, 0xff, 0xfe, 0xf9];
  final urlSafe = base64UrlEncode(bytes);
  print(urlSafe); // -__--Q==
  final repaired = base64Url.normalize(urlSafe.replaceAll('=', ''));
  print(repaired); // +//++Q==
  print(base64Decode(repaired).length); // 4
}

Dois percalços para te deixar. Não faça na mão uma substituição de - para + antes de decodificar; é desnecessário, e o normalize já faz a conversão de alfabeto quando precisa. E não assuma que uma string URL-safe chega sem padding: alguns produtores mantêm os pads, e o decodificador aceita os dois, desde que o padding esteja correto.

De bytes para texto: a decisão do charset

A decodificação Base64 te entrega bytes. Se esses bytes são texto, você tem que escolher a codificação que os transforma de volta numa String, e essa escolha é sua para fazer explicitamente. A suposição padrão em sistemas modernos é UTF-8, e o utf8.decode é o cavalo de batalha:

import 'dart:convert';
void main() {
  final payload = base64Encode(utf8.encode('Héllo Wörld'));
  final bytes = base64Decode(payload);
  print(utf8.decode(bytes)); // Héllo Wörld
  final legacy = base64Encode(latin1.encode('Héllo'));
  print(latin1.decode(base64Decode(legacy))); // Héllo
}

Quando os bytes não são UTF-8 válido, o utf8.decode lança um FormatException, que é o comportamento certo, muito melhor do que mojibake em silêncio. Se você sabe que os dados são texto legado de um byte, use a codificação correspondente:

Codificação Quando usar Decodifique com
utf8 texto moderno, JSON, qualquer coisa na web utf8.decode(bytes)
latin1 dados ocidentais legados de um byte latin1.decode(bytes)
ascii texto puro de 7 bits ascii.decode(bytes)

Uma armadilha merece o seu próprio aviso: o String.fromCharCodes não é um charset. Ele lê bytes como unidades de código UTF-16, então alimente ele com os bytes UTF-8 de Héllo e ele imprime Héllo com a maior naturalidade. Se você vir esse padrão de mojibake na sua saída, a correção quase sempre é o utf8.decode.

JWTs: lendo o token

Um JSON Web Token é três partes base64url unidas por pontos: cabeçalho, payload, assinatura. O Base64 é usado aqui por compactação e segurança de URL, não por sigilo. Qualquer pessoa com o token pode ler o cabeçalho e o payload, e isso é por design. A assinatura é o que você verifica, com o segredo compartilhado ou a chave pública do emissor. Decodificar as partes legíveis no Dart leva poucas linhas:

import 'dart:convert';
Map<String, dynamic> readJwtPayload(String token) {
  final parts = token.split('.');
  if (parts.length != 3) {
    throw FormatException('Not a compact JWT');
  }
  final padded = base64Url.normalize(parts[1]);
  final bytes = base64Decode(padded);
  return jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
}
void main() {
  const token =
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
      '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRhcnQgRGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ'
      '.c2lnbmF0dXJl';
  print(readJwtPayload(token)['name']); // Dart Dev
}

Repare na dança do padding: JWTs são construídos sem padding, então uma parte vai falhar num base64Decode direto sempre que o comprimento não for múltiplo de quatro. (No exemplo acima o cabeçalho acontece de ter 36 caracteres e decodifica direto; o payload tem 74 caracteres e não decodifica.) A chamada ao normalize torna o reparo uniforme, independente do comprimento. Mais dois avisos. Decodificar não é verificar: checar a assinatura e o claim exp é uma etapa separada e obrigatória, geralmente com o pacote crypto para algoritmos HMAC. E desconfie de tokens que declaram alg: none; um parser que os aceita é uma vulnerabilidade, não um recurso.

Data URIs: arquivos vestindo fantasia de URL

Uma data URI, definida pela RFC 2397, é uma URL cujo payload é o próprio dado: data:image/png;base64, seguido dos bytes codificados. Elas existem para que canais apenas de texto - atributos HTML, regras CSS, documentos JSON - possam carregar binário sem um arquivo separado. O Base64 é o formato de payload da vez porque a alternativa, o percent-encoding, fica muito mais longo para dados binários.

E o Dart sabe parsear elas nativamente: o suporte a data URIs está no dart:core desde 2016, então nenhuma biblioteca de URI é necessária:

import 'dart:convert';
void main() {
  final uri = Uri.parse('data:image/png;base64,iVBORw0KGgo=');
  final data = uri.data!;
  print(data.mimeType); // image/png
  print(data.isBase64); // true
  print('decoded ${data.contentAsBytes().length} bytes');
  final textUri = Uri.parse('data:text/plain;base64,SGVsbG8sIERhcnQh');
  print(textUri.data!.contentAsString()); // Hello, Dart!
}

O objeto UriData te dá o tipo MIME, a flag isBase64, o texto do payload cru, e o conteúdo decodificado como string ou como bytes. Dois percalços: o tipo MIME declarado pode mentir, então em código sensível a segurança confira os bytes mágicos de verdade; e data URIs são para assets pequenos, porque o payload inteiro viaja junto dentro do documento que referencia ele.

Arquivos: Base64 no disco

Arquivos Base64 aparecem em formatos de exportação, bundles de provisionamento e em qualquer transferência só de texto que precise carregar binário. A receita é: leia o texto, achate ele, decodifique, escreva os bytes:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final encoded = await File('image.b64').readAsString();
  final flat = encoded.replaceAll(RegExp(r'\s+'), '');
  final bytes = base64Decode(flat);
  await File('image.png').writeAsBytes(bytes);
  print('wrote ${bytes.length} bytes');
}

Aquele replaceAll está fazendo trabalho de verdade. Arquivos de texto estão cheios de quebras de linha, muitas vezes o envoltório MIME de 76 caracteres, e o decodificador estrito as rejeita, então achate primeiro. A regex remove todo caractere de espaço em branco, que é exatamente o que você quer para um arquivo base64 puro. Se o arquivo pode conter outras anotações, como um cabeçalho PEM, remova elas explicitamente antes de decodificar, e deixe os erros do decodificador pegarem o que está realmente corrompido.

HTTP e APIs

O Base64 no HTTP veste duas fantasias. Primeiro, respostas de API: um campo JSON que carrega binário como string. Segundo, o cabeçalho Authorization: Basic, onde as credenciais são codificadas em base64 com o alfabeto padrão e padding:

import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
  final response = await http.get(
    Uri.parse('https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt'),
  );
  final payload = jsonDecode(response.body) as Map<String, dynamic>;
  final args = payload['args'] as Map<String, dynamic>;
  final bytes = base64Decode(args['attachment'] as String);
  print('got ${bytes.length} bytes');
  final credentials = utf8.decode(base64Decode('b2N0b2NhdDpzZWNyZXQ='));
  print(credentials.split(':').first); // octocat
}

O pacote http é o cliente padrão, a um dart pub add http de distância. Para Basic auth, você decodifica a parte depois do prefixo Basic . Dois percalços: algumas APIs enviam valores URL-safe ou sem padding onde a documentação diz base64, então se a decodificação direta lançar erro, passe o valor pelo base64Url.normalize primeiro; e lembre-se de que Basic auth é ofuscação, não proteção, que é por que ele só cabe em conexões TLS.

E-mail e MIME: o problema das quebras de linha

E-mail é o cliente mais antigo do base64. O MIME quebra linhas base64 a cada 76 caracteres - 76 mais CRLF cabe com folga num display de 80 colunas - e a RFC 2045 manda os decodificadores ignorarem as quebras de linha. O decodificador do Dart não, de propósito: ele as rejeita. A correção é achatar antes de decodificar:

import 'dart:convert';
List<int> decodeMimeBody(String wrapped) {
  final flat = wrapped.replaceAll(RegExp(r'\s+'), '');
  return base64Decode(flat);
}
void main() {
  const wrapped =
      'SGVsbG8gZnJvbSBhbiBlbWFpbCBhdHRhY2htZW50LCB3cmFwcGVkIGF0IDc2IGNoYXJhY3RlcnMg'
      '\r\n'
      'dGhlIHdheSBNSU1FIHdhbnRzIGl0IHRvIGJlLCB3aXRoIENSTEYgYmV0d2VlbiB0aGUgbGluZXMu';
  print(utf8.decode(decodeMimeBody(wrapped)));
}

A regra é simples: remova espaços em branco, nada mais. Não remova outros caracteres na esperança de ser útil; o decodificador é o validador, e você quer que ele reclame de corrupção de verdade. Se você está processando e-mail em escala, a etapa de achatar é barata, uma passada de regex, e ela mantém o resto do pipeline honesto.

Configuração e variáveis de ambiente

Tokens e credenciais que vivem em configuração baseada em texto às vezes são codificados em base64 para caberem em uma linha e parecerem tokens. O enquadramento honesto: base64 é ofuscação, não criptografia, então esse padrão é para organização, nunca para sigilo. O padrão em si é trivial:

import 'dart:convert';
import 'package:dotenv/dotenv.dart';
Future<void> main() async {
  final env = DotEnv()..load();
  final encoded = env['API_TOKEN_B64'];
  if (encoded == null) {
    return;
  }
  final token = utf8.decode(base64Decode(encoded));
  print('loaded a ${token.length}-char token');
}

Com o pacote dotenv, o valor fica num arquivo .env como API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= e volta como texto simples depois da decodificação. A mesma forma funciona com String.fromEnvironment para valores de dart-define em tempo de compilação, com um aviso: valores dart-define são assados no binário compilado, então qualquer coisa secreta pertence à configuração em runtime ou a um gerenciador de segredos, não lá.

Streams: bloco por bloco

Quando o texto codificado chega em pedaços - um stream de rede, um arquivo grande lido em blocos - o decodificador dá conta. A máquina de estados dele carrega o grupo parcial pelas fronteiras dos blocos, então os blocos não precisam se alinhar em fronteiras de quatro caracteres:

import 'dart:convert';
Future<void> main() async {
  final incoming = Stream.fromIterable(['TWF', 'uaGVsbG8=']);
  final text = await incoming
      .transform(base64.decoder)
      .map(utf8.decode)
      .join();
  print(text); // Manhello
}

A chamada transform usa o decodificador como um transformador de stream; o primeiro bloco, três caracteres, estaciona os bits dele no estado do decodificador, e o segundo bloco completa o grupo. Erros aparecem como erros de stream com os mesmos detalhes de FormatException, e um stream vazio simplesmente não produz saída. Se você prefere sinks, o base64.decoder.startChunkedConversion te dá um StringConversionSink conectado à mesma máquina de estados.

Dados massivos: a matemática e a memória

Decodificar encolhe: quatro caracteres viram três bytes, então a saída sempre fica um pouco abaixo de três quartos do comprimento da entrada. Isso significa que o tamanho da saída é conhecido antes de você decodificar, o que torna a memória previsível. Um helper pequeno calcula isso a partir da string sozinha:

import 'dart:convert';
int decodedLength(String encoded) {
  var padding = 0;
  for (var i = encoded.length - 1; i >= 0 && padding < 2; i--) {
    if (encoded.codeUnitAt(i) == 0x3d) {
      padding++;
    } else {
      break;
    }
  }
  return (encoded.length ~/ 4) * 3 - padding;
}
void main() {
  print(decodedLength('QQ==')); // 1
  print(decodedLength('QUI=')); // 2
  print(decodedLength('QUJD')); // 3
}

O decodificador embutido é rápido: uma passada única por uma tabela de busca, sem alocações de string por caractere, então strings de vários megabytes são rotina. Onde o base64 custa é no lado da entrada: o texto codificado fica uns 33 por cento maior do que os dados, e ele é uma string, que na VM vive como unidades de código UTF-16, aproximadamente o dobro do comprimento em bytes dos caracteres codificados. Para payloads que podem crescer muito, decodifique em stream em vez de juntar uma string gigante.

Da linha de comando

A VM do Dart faz uma CLI limpa a partir do decodificador. Esta ferramenta pequena lê um argumento de arquivo ou a entrada padrão, achata os espaços em branco e escreve bytes crus na saída padrão:

import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
  String encoded;
  if (args.isNotEmpty) {
    encoded = await File(args[0]).readAsString();
  } else {
    encoded = await stdin
        .transform(utf8.decoder)
        .join();
  }
  final flat = encoded.replaceAll(RegExp(r'\s+'), '');
  stdout.add(base64Decode(flat));
  await stdout.flush();
}

Salve como bin/decode.dart e rode dart run bin/decode.dart image.b64 > image.png, ou use pipe: cat token.b64 | dart run bin/decode.dart. A chamada stdout.add recebe o Uint8List diretamente, sem string intermediária, que é exatamente como binário deveria viajar por um pipeline.

Percalços que mordem quem desenvolve em Dart

  • O muro do padding. Entrada estilo JWT e de ferramentas de URL frequentemente chega sem os sinais =, e o decodificador a recusa com Invalid length, must be multiple of four. Passe entrada não confiável pelo base64Url.normalize primeiro.
  • A armadilha do espaço em branco. Arquivos de texto, e-mail e copiar-colar introduzem quebras de linha, e o decodificador nunca as pula. Achate com replaceAll(RegExp(r'\s+'), '') antes de decodificar.
  • Confiança no alfabeto. Como os dois alfabetos decodificam em qualquer lugar, não construa lógica sobre qual decodificador produziu uma string. A string é o contrato, não as configurações do produtor.
  • String.fromCharCodes não é um charset. Ele lê unidades de código UTF-16, então transforma texto UTF-8 em mojibake. Use utf8.decode ou uma codificação explícita.
  • Dois tipos de erro diferentes. Problemas de decodificação são FormatExceptions; o codificador lança ArgumentError para valores fora da faixa de 0 a 255. Pegue eles separadamente se você está construindo uma fronteira.
  • O resultado tem comprimento fixo. Uint8List não pode crescer, então bytes.add(1) lança um UnsupportedError. Copie com List<int>.from(bytes) quando precisar de uma lista que cresça.
  • Não faça unescape de %3D na mão. O decodificador lê padding com percent-escape nativamente; um replaceAll('%3D', '=') prematuro acopla seu código a um detalhe que o SDK já domina.
  • Decodificar um payload de JWT não é verificar ele. Ler os claims e confiar neles é um bug de segurança esperando por um usuário determinado.

Boas práticas, lista curta

  • Padronize no base64Decode; vá ao normalize só na fronteira onde a entrada não é confiável.
  • Seja explícito com o charset usando utf8.decode(bytes), mesmo quando assume UTF-8.
  • Mantenha bytes como bytes até saber o que são; o Uint8List viaja limpo para o File.writeAsBytes e companhia.
  • Nas fronteiras de confiança, pegue o FormatException e registre a posição da entrada que a mensagem te dá.
  • Use stream para qualquer coisa que possa passar de alguns megabytes.
  • Trate o base64 como um formato, não como uma proteção: ele não esconde nada de ninguém que saiba que é base64.

Uma breve história do Base64 no Dart

O decodificador que você acabou de conhecer existe há mais tempo que o Dart 3, a null safety e a era do Flutter. A versão curta:

  • 18 de novembro de 2015, Dart 1.13: o Base64 chega no dart:convert como a constante BASE64 mais as classes Base64Codec, Base64Encoder e Base64Decoder. Antes deste release, o SDK não tinha base64 nenhum.
  • 28 de janeiro de 2016, Dart 1.14: o Base64Decoder.convert ganha os parâmetros de faixa start e end, e o mesmo release adiciona suporte a data URIs no dart:core, o caminho do Uri.parse no qual este artigo se apoia.
  • 26 de abril de 2016, Dart 1.16: o alfabeto URL-safe entra como BASE64URL e o construtor Base64Codec.urlSafe.
  • 7 de agosto de 2018, Dart 2.0: as constantes são renomeadas para minúsculas, base64 e base64Url, o base64Decode de nível superior e companhia chegam, a decodificação retorna um Uint8List em vez de uma List<int> que crescia, e o Base64Codec.normalize entra na família, transformando validação e reparo em uma etapa de uma chamada.
  • 2021, Dart 2.12: a null safety chega, e a história inteira do dart:convert, base64 incluído, fica null-safe.
  • Hoje, Dart 3.13: as classes são marcadas como final, e o comportamento que você conheceu acima é a mesma máquina estrita, dos dois alfabetos e atenta ao percent, que roda desde 2015.

A estriteza não é um acidente da implementação. É o decodificador seguindo a instrução da RFC 4648 de que as implementações devem rejeitar caracteres fora do alfabeto, com a tolerância estilo MIME deixada para as aplicações que precisam, o que no Dart significa uma etapa de achatar antes da decodificação.

Fatos curiosos

  • O decodificador lê %3D como padding nativo. Entregue ele o payload cru de uma data URI, escape e tudo, e ele decodifica. Muito poucos runtimes de linguagem fazem isso sem uma etapa de pré-processamento.
  • base64.decoder e base64Url.decoder são literalmente o mesmo objeto: ambos são a instância canônica const Base64Decoder(). O "decodificador URL-safe" é o decodificador padrão com outra fantasia.
  • O decodificador inteiro cabe numa única tabela de busca de 128 entradas, um Int8List compartilhado entre o interpretador e o código compilado AOT, com + e - ambos apontando para a posição 62 do alfabeto e / e _ ambos apontando para a 63.
  • O base64 do Dart e o suporte a data URIs dele aterrissaram em dois releases separados, no 1.13 e no 1.14, e claramente foram planejados como um par: um para ler o formato, outro para ler ele direto de uma URL.
  • A string vazia decodifica para um Uint8List vazio sem erro, e a string vazia codifica para a string vazia: o base64 trata a ausência de dados como uma mensagem perfeitamente válida.
  • Em 2018, quando o Dart 2.0 renomeou as constantes dele, BASE64 virou base64 como parte de uma mudança em todo o SDK para nomes de constantes em minúsculas, a mesma onda que te deu ascii, json e utf8.

Agora você tem o decodificador inteiro: o que ele aceita, o que ele recusa, como reparar entrada danificada, e como encontrá-lo em JWTs, data URIs, arquivos, streams, e-mail e o shell. A outra direção da troca, pegar bytes e produzir um dos dois alfabetos, com as decisões de padding e a matemática do tamanho, é coberta em detalhes no guia de codificação Base64, que está linkado no final desta página.

Última atualização: 2026-09-08

Artigo relacionado: Codificação Base64 em Dart: um guia completo