Three Base64 APIs in the Kotlin World
Kotlin doesn't have one canonical Base64 API — it has three, and which one you use depends on your target platform and the level of API modernization you want. Understanding the tradeoffs prevents subtle bugs when mixing them in one project:
kotlin.io.encoding.Base64(Kotlin 1.9+ stdlib,experimental) — Multiplatform (JVM, JS, Native, Wasm). Preferred for new code. Requires@OptIn(ExperimentalEncodingApi::class).java.util.Base64(Java 8+, JVM-only) — Battle-tested, no opt-in required, well-known to any Java developer. Best JVM-only choice for enterprise codebases.android.util.Base64(Android, deprecated on API 26+) — Uses integer bit-flags (DEFAULT, NO_WRAP, URL_SAFE, NO_PADDING) that combine with bitwise OR. Legacy — replace with java.util.Base64 on modern Android.
Kotlin 1.9+ Stdlib — Multiplatform Pattern
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun main() {
val text = "Hello 世界 👋"
// Encode
val encoded: String = Base64.encode(text.toByteArray())
println(encoded) // SGVsbG8g5LiW55WMIPCfkYs=
// Decode
val decoded: ByteArray = Base64.decode(encoded)
println(String(decoded)) // Hello 世界 👋
// URL-safe variant (no + or /)
val urlSafe: String = Base64.UrlSafe.encode(text.toByteArray())
println(urlSafe)
// MIME variant (line-wrapped at 76 chars)
val mime: String = Base64.Mime.encode(text.toByteArray())
println(mime)
}JVM — java.util.Base64 (Java 8+)
import java.util.Base64
fun main() {
val text = "Hello 世界 👋"
// Standard encoder
val encoded: String = Base64.getEncoder().encodeToString(text.toByteArray(Charsets.UTF_8))
println(encoded)
// URL-safe encoder (no + or /, no padding)
val urlSafe: String = Base64.getUrlEncoder().withoutPadding()
.encodeToString(text.toByteArray(Charsets.UTF_8))
println(urlSafe)
// MIME encoder (line-wrapped)
val mime: String = Base64.getMimeEncoder().encodeToString(text.toByteArray(Charsets.UTF_8))
println(mime)
// Decode
val decodedBytes = Base64.getDecoder().decode(encoded)
println(String(decodedBytes, Charsets.UTF_8))
}Idiomatic Kotlin — Extension Functions
// File: Base64Extensions.kt
import java.util.Base64
// String extensions
fun String.encodeBase64(): String =
Base64.getEncoder().encodeToString(toByteArray(Charsets.UTF_8))
fun String.encodeBase64UrlSafe(): String =
Base64.getUrlEncoder().withoutPadding()
.encodeToString(toByteArray(Charsets.UTF_8))
fun String.decodeBase64(): String =
String(Base64.getDecoder().decode(this), Charsets.UTF_8)
// ByteArray extensions
fun ByteArray.encodeBase64(): String =
Base64.getEncoder().encodeToString(this)
fun ByteArray.encodeBase64UrlSafe(): String =
Base64.getUrlEncoder().withoutPadding().encodeToString(this)
// Usage — reads like English
fun main() {
val encoded = "Hello 世界".encodeBase64()
println(encoded) // SGVsbG8g5LiW55WM
val decoded = encoded.decodeBase64()
println(decoded) // Hello 世界
val jwtPart = """{"user_id":42}""".encodeBase64UrlSafe()
println(jwtPart)
}Android — android.util.Base64 with Flags
import android.util.Base64
// The four flags — combine with bitwise OR
// Base64.DEFAULT — line-wrapped every 76 chars (MIME-style)
// Base64.NO_WRAP — single-line output (use for JSON, HTTP headers, JWTs)
// Base64.URL_SAFE — uses - and _ instead of + and /
// Base64.NO_PADDING — omits trailing = characters
// The "correct" flag combination for HTTP/JSON/JWT contexts:
val flags = Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING
fun encodeForApi(bytes: ByteArray): String {
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}
fun encodeForJwt(bytes: ByteArray): String {
return Base64.encodeToString(bytes,
Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING)
}
fun decodeFromApi(encoded: String): ByteArray {
return Base64.decode(encoded, Base64.NO_WRAP)
}
// AVOID Base64.DEFAULT for API responses — the injected line breaks
// will break your JSON parser. Always specify NO_WRAP explicitly.Encoding a Bitmap on Android
import android.graphics.Bitmap
import android.util.Base64
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
// Bitmap → JPEG bytes → Base64 string
// Suspend function so we can dispatch to IO thread
suspend fun Bitmap.toBase64(quality: Int = 80): String = withContext(Dispatchers.IO) {
val output = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.JPEG, quality, output)
Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
}
// Bitmap → PNG (lossless, larger)
suspend fun Bitmap.toBase64Png(): String = withContext(Dispatchers.IO) {
val output = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.PNG, 100, output)
Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
}
// Data URL for a WebView <img src>
suspend fun Bitmap.toDataUrl(quality: Int = 80): String {
val base64 = toBase64(quality)
return "data:image/jpeg;base64,\$base64"
}
// Usage in a ViewModel with viewModelScope
// viewModelScope.launch {
// val encoded = bitmap.toBase64()
// _state.value = state.value.copy(encodedImage = encoded)
// }Encoding a File with Coroutines
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Base64
suspend fun File.readAsBase64(): String = withContext(Dispatchers.IO) {
Base64.getEncoder().encodeToString(readBytes())
}
// Streaming encode for large files — bounded memory
suspend fun encodeLargeFile(input: File, output: File): Unit = withContext(Dispatchers.IO) {
input.inputStream().use { source ->
Base64.getEncoder().wrap(output.outputStream()).use { encoder ->
source.copyTo(encoder)
}
}
// encoder.close() (via use) flushes final padding automatically
}
// Usage
suspend fun uploadImage(imageFile: File) {
val encoded = imageFile.readAsBase64()
// Send encoded string in JSON body via Retrofit / Ktor
}Encoding a Serializable Data Class
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.Base64
@Serializable
data class User(
val id: Int,
val email: String,
val roles: List<String>,
val joined: String
)
fun User.toBase64(): String {
val json = Json.encodeToString(this)
return Base64.getEncoder().encodeToString(json.toByteArray(Charsets.UTF_8))
}
fun decodeUser(encoded: String): User {
val jsonBytes = Base64.getDecoder().decode(encoded)
return Json.decodeFromString<User>(String(jsonBytes, Charsets.UTF_8))
}
fun main() {
val user = User(42, "[email protected]",
listOf("admin", "editor"), "2026-01-15")
val encoded = user.toBase64()
println(encoded)
val roundTripped = decodeUser(encoded)
println(roundTripped)
}HTTP Basic Auth Header in Kotlin
import java.util.Base64
fun basicAuthHeader(user: String, password: String): String {
val creds = "\$user:\$password"
val token = Base64.getEncoder().encodeToString(creds.toByteArray(Charsets.UTF_8))
return "Basic \$token"
}
// Ktor client integration
// suspend fun callProtectedApi(): String = HttpClient().use { client ->
// client.get("https://api.example.com/protected") {
// header("Authorization", basicAuthHeader("admin", "secret123"))
// }.bodyAsText()
// }
// OkHttp / Retrofit interceptor
// class BasicAuthInterceptor(private val user: String, private val pass: String) : Interceptor {
// override fun intercept(chain: Interceptor.Chain): Response {
// val request = chain.request().newBuilder()
// .header("Authorization", basicAuthHeader(user, pass))
// .build()
// return chain.proceed(request)
// }
// }Common Pitfalls in Kotlin Base64 Code
- Using Base64.DEFAULT on Android for API payloads — DEFAULT adds line breaks every 76 chars, which break JSON parsers, HTTP header validation, and JWT verifiers. Always specify
Base64.NO_WRAPexplicitly for anything that isn't email/MIME. - Missing @OptIn for stdlib Base64 — kotlin.io.encoding.Base64 is experimental in 1.9+ and requires
@OptIn(ExperimentalEncodingApi::class)at every call site (or file-level@file:OptIn). Missing it produces a compiler warning that's easy to miss. - Encoding Bitmaps on the main thread — Compressing and Base64-encoding even a small camera photo can take 200-500ms. On the main thread this drops frames and causes ANRs on lower-end devices. Always use
withContext(Dispatchers.IO)or a coroutine on a background dispatcher. - Using getMimeEncoder for JWTs — MIME encoders line-wrap output. JWTs cannot contain whitespace or line breaks. Use
getUrlEncoder().withoutPadding()for JWT payloads. - Missing charset in toByteArray() —
text.toByteArray()uses the JVM default charset which can differ between machines. Always specifyCharsets.UTF_8explicitly. This bit thousands of Android developers beforetoByteArray()defaulted to UTF-8 on the JVM.
Command Line Alternative
# Kotlin script (kotlinc-jvm required)
echo 'import java.util.Base64; println(Base64.getEncoder().encodeToString("Hello".toByteArray()))' > /tmp/b64.kts
kotlinc -script /tmp/b64.kts
# Or use Kotlin's kotlin runner
kotlin -e 'println(java.util.Base64.getEncoder().encodeToString("Hello".toByteArray()))'
# System base64 CLI (independent of Kotlin/JVM)
echo -n "Hello" | base64
# Output: SGVsbG8=Key Facts
- Stdlib (1.9+):
- kotlin.io.encoding.Base64 — multiplatform, requires @OptIn
- JVM standard:
- java.util.Base64.getEncoder() — no opt-in, Java 8+
- Android legacy:
- android.util.Base64.encodeToString(bytes, Base64.NO_WRAP)
- URL-safe (JVM):
- Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
- Android flags:
- Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING
- Charset:
- Always specify Charsets.UTF_8 explicitly on toByteArray()
- Coroutines:
- Bitmap and file encoding on Dispatchers.IO — never main thread
- Extensions:
- String.encodeBase64() pattern preferred over free-standing helpers
Related Base64 Tools
- Base64 Encode Online — general-purpose browser encoder
- Base64 Encode in Java — java.util.Base64 in Java syntax
- Base64 Encode in Python — Python 3 equivalent
- Base64 Encode in Go — Go encoding/base64
- Base64 Encode in JavaScript — Node.js and browser
- URL-Safe Base64 — cross-language URL encoding
- JWT Debugger — inspect JWT tokens