Encoding binary data as readable ASCII text is an essential task for modern developers. Whether you are passing basic authorization credentials, sending image attachments in JSON payloads, transmitting security keys, or persisting encrypted values, Base64 is the industry-standard mechanism to get it done.
However, if you are working in Kotlin, navigating the different ways to achieve this can be confusing. Depending on whether your target platform is the JVM, Android, or Kotlin Multiplatform (KMP), your architectural requirements will vary significantly.
In this comprehensive guide, we will dive deep into everything you need to know about implementing base64 kotlin solutions. We will explore the modern Kotlin standard library API (kotlin.io.encoding.Base64), look at platform-specific approaches like JVM's java.util.Base64 and Android's android.util.Base64, discuss how to handle multiplatform projects seamlessly, and outline best practices to avoid common memory and character encoding pitfalls.
Understanding Base64: How the Math Works Under the Hood
Before exploring the implementation APIs of base64 kotlin, it is vital to understand what Base64 actually does. Base64 is not encryption, nor is it compression. It is a binary-to-text encoding scheme. Its primary goal is to safely translate arbitrary binary data—such as image files, cryptographic signatures, or serialized models—into a sequence of printable ASCII characters.
The 8-bit to 6-bit Conversion Mechanics
Traditional computer memory works with 8-bit bytes (values ranging from 0 to 255). Many legacy internet protocols (like HTTP headers or email protocols) were designed exclusively to handle standard 7-bit ASCII text. Passing raw 8-bit binary data through these protocols often leads to character truncation, interpretation errors, or corruption.
Base64 solves this by mapping binary data to a safe subset of 64 characters. Let's break down the exact mathematical mapping:
- The encoder collects chunks of three 8-bit bytes (a total of 24 bits).
- It splits these 24 bits into four 6-bit groups (each with a value between 0 and 63).
- It maps each 6-bit value to its corresponding character in the Base64 alphabet table:
- Index 0 to 25:
AthroughZ - Index 26 to 51:
athroughz - Index 52 to 61:
0through9 - Index 62:
+(or-in the URL-safe schema) - Index 63:
/(or_in the URL-safe schema)
- Index 0 to 25:
- If the binary payload does not divide perfectly by three, padding characters (
=) are appended to complete the final 4-character block.
For example, if you encode the word "Hi":
- "H" in ASCII is binary
01001000(72 in decimal) - "i" in ASCII is binary
01101001(105 in decimal) - Combined binary stream:
0100100001101001(16 bits) - Split into 6-bit groups:
010010(18 -> S),000110(6 -> G), and the leftover1001padded with zeros becomes100100(36 -> k) - The last chunk is completely missing, so we append
=as padding. - Final Base64 encoding:
SGk=
Understanding these fundamentals clarifies why raw string transformations require explicit byte array handling when utilizing kotlin base64 methods.
1. The Modern Way: Kotlin Standard Library Base64 (kotlin.io.encoding.Base64)
Historically, Kotlin developers targeting multiplatform environments faced a severe dilemma: they either had to write platform-specific wrappers (expect/actual) using Java APIs for the JVM and Objective-C/Swift APIs for iOS, or include heavy external libraries. Fortunately, Kotlin introduced a native base64 standard library API under the kotlin.io.encoding package, which has been fully stabilized and optimized in modern versions of the language.
This API runs seamlessly across JVM, Android, iOS, Native (macOS, Linux, Windows), JavaScript, and WebAssembly (Wasm). It is now the absolute gold standard for base64 kotlin operations.
Basic String Encoding & Decoding
To execute standard kotlin base64 processes, the library provides a global singleton object called Base64.Default. Here is a complete, ready-to-run implementation showing how to safely encode and decode plain-text strings:
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class) // Not required in modern Kotlin, but standard for legacy projects
fun main() {
val originalText = "Hello, Kotlin Multiplatform!"
// Convert the string to UTF-8 ByteArray
val textBytes = originalText.encodeToByteArray()
// Encode to a clean Base64 string
val encodedString: String = Base64.Default.encode(textBytes)
println("Encoded Text: $encodedString")
// Output: SGVsbG8sIEtvdGxpbiBNdWx0aXBsYXRmb3JtIQ==
// Decode the Base64 string back to a ByteArray
val decodedBytes: ByteArray = Base64.Default.decode(encodedString)
// Convert the ByteArray back to String
val decodedText = decodedBytes.decodeToString()
println("Decoded Text: $decodedText")
// Output: Hello, Kotlin Multiplatform!
}
Encoding and Decoding Raw Byte Arrays
Often, your input is not a plain string but raw binary data (like an image file or network buffer). Because Base64 operates naturally on bytes, this is even more straightforward:
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun processBinaryData() {
val rawData = byteArrayOf(0, 15, 30, 45, 60, 75, 90)
// Encode raw ByteArray directly to String
val base64Output = Base64.Default.encode(rawData)
println("Binary Base64: $base64Output")
// Decode directly back to ByteArray
val restoredBytes = Base64.Default.decode(base64Output)
assert(rawData.contentEquals(restoredBytes))
}
Using URL and Filename Safe Encoding
When passing encoded tokens inside URL query strings, special character substitutions are required. Standard Base64 characters like + and / can disrupt router paths or parameter parsers. To address this, use Base64.UrlSafe instead of Base64.Default. It replaces safe characters defined by RFC 4648:
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun urlSafeDemo() {
val inputWithSpecialSymbols = byteArrayOf(251.toByte(), 255.toByte(), 191.toByte())
// Standard Base64 uses + and /
val standardResult = Base64.Default.encode(inputWithSpecialSymbols)
println("Standard Base64: $standardResult") // Output: +/+7
// URL-safe swaps them for - and _
val urlSafeResult = Base64.UrlSafe.encode(inputWithSpecialSymbols)
println("URL-Safe Base64: $urlSafeResult") // Output: -_u7
}
Managing Padding Configurations
Padding is standard, but some web frameworks and compact APIs require padding characters (=) to be excluded from the payload to minimize payload size. You can modify your encoder's padding strategy programmatically using withPadding():
import kotlin.io.encoding.Base64
import kotlin.io.encoding.PaddingOption
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun paddingConfiguration() {
val textToEncode = "foobar123".encodeToByteArray()
// Create a new Base64 encoder that skips writing padding characters
val unpaddedEncoder = Base64.Default.withPadding(PaddingOption.NONE)
val encodedUnpadded = unpaddedEncoder.encode(textToEncode)
println("Unpadded: $encodedUnpadded")
// Decode the unpadded string using the configured encoder
val decoded = unpaddedEncoder.decode(encodedUnpadded)
println("Decoded: ${decoded.decodeToString()}")
}
2. The JVM-Specific Way: Utilizing java.util.Base64
If you are writing software strictly targeted for backend applications running on Java Virtual Machines (such as Spring Boot, Quarkus, or Micronaut microservices), you can rely on the mature Java 8 standard library class java.util.Base64.
Java's implementation is highly stable, incredibly fast, and utilizes JVM intrinsic optimizations under the hood. To perform base64 encode kotlin operations on the JVM, you can integrate this Java API natively within your Kotlin functions.
Standard JVM Encoding and Decoding
To encode standard string data, we first fetch the encoder instance using Base64.getEncoder(). This encodes the byte array into a Base64-compliant ASCII string conforming to RFC 4648 standards.
import java.util.Base64
fun jvmEncodingDemo() {
val rawMessage = "Hello JVM Server!"
// Step 1: Always convert the input string to byte array using an explicit Charset
val inputBytes = rawMessage.toByteArray(Charsets.UTF_8)
// Step 2: Encode to a Base64 String
val encoded: String = Base64.getEncoder().encodeToString(inputBytes)
println("Encoded: $encoded")
// Output: SGVsbG8gSlZNLVNlcnZlciE=
// Step 3: Decode from Base64 string to original bytes
val decodedBytes: ByteArray = Base64.getDecoder().decode(encoded)
// Step 4: Reconstruct string representation from bytes
val decodedMessage = String(decodedBytes, Charsets.UTF_8)
println("Decoded: $decodedMessage")
}
JVM URL-Safe and MIME Alternatives
Just like Kotlin’s multiplatform library, Java's utility class offers convenient factory methods for specific use cases:
Base64.getUrlEncoder()/Base64.getUrlDecoder(): Exclude unsafe URL characters by replacing standard operators with hyphens and underscores.Base64.getMimeEncoder()/Base64.getMimeDecoder(): Format output into blocks of 76 characters separated by system carriage returns. This is ideal for email attachments and legacy systems with payload constraints.
3. The Android Platform Way: android.util.Base64
When writing native Android applications, developers often encounter android.util.Base64. This Android-specific utility was introduced in API level 8 (Android 2.2) and has been a staple of mobile development for over a decade.
Encoding and Decoding with Android Base64
Unlike the modern standard library or JVM classes that return configured encoder instances, Android's implementation relies on integer flags passed directly into static methods. Here is a basic mobile implementation:
import android.util.Base64
fun androidEncodingDemo() {
val payload = "Android Core Engine"
val bytes = payload.toByteArray(Charsets.UTF_8)
// Standard Android encoding
val encodedString = Base64.encodeToString(bytes, Base64.DEFAULT)
// Decoding back to raw bytes
val decodedBytes = Base64.decode(encodedString, Base64.DEFAULT)
val originalText = String(decodedBytes, Charsets.UTF_8)
}
Master Android Bitmask Flags
The power of Android’s Base64 class lies in its flexible configuration flags:
Base64.DEFAULT: The standard RFC encoding, inserting line breaks automatically at set intervals.Base64.NO_PADDING: Completely skips writing trailing=padding characters.Base64.NO_WRAP: Disables line breaks completely. This is incredibly important when passing Authorization Bearer tokens via HTTP headers, as unexpected line breaks will corrupt the header value.Base64.URL_SAFE: Implements URL-safe character mapping (swapping+and/for-and_).
You can chain flags using the bitwise OR (or) infix function in Kotlin:
val cleanHeaderToken = Base64.encodeToString(bytes, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
The Stub Method JUnit Pitfall
One of the most persistent frustrations for Android engineers occurs when running local Unit Tests on their development machines. If you try to execute a unit test that runs a method calling android.util.Base64, your test will crash with a java.lang.RuntimeException: Method encodeToString in android.util.Base64 not mocked.
This happens because local unit tests run on a lightweight mock of the Android framework, which does not contain the actual underlying bytecode implementation of its utility classes. Developers historically had to mock these classes manually or run slower Emulator (instrumented) tests.
The Best Solution: Avoid android.util.Base64 entirely in new development. Transition your project's Base64 operations to the Kotlin standard library kotlin.io.encoding.Base64. It has zero dependencies on Android's runtime and compiles natively to your local computer's JVM, meaning your local unit tests will run flawlessly at lightning speed.
4. Multiplatform (KMP) Approaches with Third-Party Libraries
For larger enterprise codebases that are migrating slowly, or projects restricted to legacy versions of Kotlin (prior to standard library stabilization), third-party libraries provide a great bridge.
Square's Okio
Okio is an exceptionally mature, high-performance IO library from Square that fully supports Kotlin Multiplatform. It makes Base64 encoding extremely intuitive by introducing convenient extension functions directly on strings and ByteStrings:
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
fun okioMultiplatformDemo() {
val text = "Seamless Multiplatform with Okio"
// Encode a string straight to its Base64 representation
val base64Encoded: String = text.encodeUtf8().base64()
println("Okio Encoded: $base64Encoded")
// Decode a Base64 string back to human-readable text
val decodedText: String? = base64Encoded.decodeBase64()?.utf8()
println("Okio Decoded: $decodedText")
}
Because Okio compiles across all platforms (Android, iOS, JVM, JS, Native), this ensures your multiplatform code stays clean and uniform without needing target-specific blocks.
Ktor Utilities (Legacy Approach)
Historically, developers using the Ktor client used Ktor's built-in encodeBase64() and decodeBase64String() extensions from the io.ktor.util package.
However, in modern versions of Ktor, these utilities have been deprecated. The Ktor team now officially instructs developers to use Base64.Default.encode() directly from Kotlin's standard library. If you are maintaining a Ktor project, updating to the standard library native methods is the recommended path forward.
5. Edge Cases, Performance, and Best Practices
Writing bug-free, high-performance code requires attention to resource management and character encoding standards. Here are the top three best practices for production implementations.
1. Always Enforce Explicit Charsets
Converting text strings to byte arrays via myString.toByteArray() without arguments is a dangerous anti-pattern. This command relies on the platform’s default charset, which can vary between user devices. A string encoded on a Swedish Windows PC (using Windows-1252) will produce a different byte array—and thus a completely different Base64 output—than the same string encoded on a Linux server (using UTF-8). This mismatch can break token verification and database decryption.
Always enforce UTF_8 explicitly:
- In Modern Kotlin:
myString.encodeToByteArray()natively defaults to UTF-8. - In JVM/Android:
myString.toByteArray(Charsets.UTF_8).
2. Stream-Based Base64 for Large Files (Avoiding OutOfMemoryError)
Loading an entire 100MB file into memory as a ByteArray to convert it to a Base64 string is a recipe for an immediate OutOfMemoryError. A standard Base64 string is roughly 33% larger than its binary source. Combined with the temporary JVM allocations needed for conversion, your app may attempt to allocate 3x the file's size in RAM simultaneously.
To prevent crashes, utilize standard streaming architectures. By wrapping output streams, you can encode data incrementally using tiny byte buffers (which must always be multiples of three to prevent accidental intermediate padding characters):
import java.io.File
import java.util.Base64
fun streamLargeFileToBase64(sourceFile: File, destinationFile: File) {
val jvmEncoder = Base64.getEncoder()
sourceFile.inputStream().use { input ->
destinationFile.outputStream().use { rawOutput ->
// Wrap the raw output stream with the JVM Base64 stream encoder
jvmEncoder.wrap(rawOutput).use { base64Output ->
val buffer = ByteArray(12288) // 12KB buffer (perfectly divisible by 3!)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
base64Output.write(buffer, 0, bytesRead)
}
}
}
}
}
This streams the file chunk-by-chunk, meaning the memory consumption remains capped at just 12KB, regardless of whether you are encoding a 1MB PDF or a 4GB video file.
3. Base64 is Not a Security Layer
It cannot be overemphasized: Base64 is an encoding format, not an encryption standard. Its output characters are easily readable by anyone using basic decoding software. Never use Base64 to secure user passwords, database strings, or authorization payloads unless those payloads are fully encrypted beforehand using AES, JWE, or modern cryptography standards.
6. Functional Architecture Comparison
| Encoding Type | Character Set | Padding character | Formatting Rules | Primary Use Case |
|---|---|---|---|---|
| Standard | A-Z, a-z, 0-9, +, / |
= |
No line limits. | General-purpose storage, file transfer, JSON objects. |
| URL-Safe | A-Z, a-z, 0-9, -, _ |
= (Optional) |
No line limits. | Web URLs, routing paths, browser cookies, HTTP GET parameters. |
| MIME | A-Z, a-z, 0-9, +, / |
= |
Max 76 chars per line. | Email SMTP transmissions, legacy mainframe networking. |
7. Custom Extensions for Syntactic Sugar
In modern Kotlin development, wrapping standard functions in intuitive extension properties keeps your clean architecture code readable. Here is a production-ready extension module you can drop directly into your utility package:
package com.example.utils
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
inline val String.base64Encoded: String
get() = Base64.Default.encode(this.encodeToByteArray())
@OptIn(ExperimentalEncodingApi::class)
inline val String.base64Decoded: String
get() = Base64.Default.decode(this).decodeToString()
@OptIn(ExperimentalEncodingApi::class)
inline val String.base64UrlEncoded: String
get() = Base64.UrlSafe.encode(this.encodeToByteArray())
@OptIn(ExperimentalEncodingApi::class)
inline val String.base64UrlDecoded: String
get() = Base64.UrlSafe.decode(this).decodeToString()
@OptIn(ExperimentalEncodingApi::class)
fun ByteArray.toBase64(): String = Base64.Default.encode(this)
@OptIn(ExperimentalEncodingApi::class)
fun String.toBase64Bytes(): ByteArray = Base64.Default.decode(this)
With these helper extensions, your application code shrinks dramatically. Converting user credentials or session payloads is simple:
val secureToken = "admin:secret123".base64Encoded
val parsedCredentials = secureToken.base64Decoded
8. Frequently Asked Questions (FAQ)
How do I encode a ByteArray to Base64 in Kotlin?
In modern Kotlin, you can use kotlin.io.encoding.Base64.Default.encode(byteArray). If your project targets the JVM exclusively, you can utilize java.util.Base64.getEncoder().encodeToString(byteArray) as an alternative.
How do I decode a Base64 string in Kotlin?
To decode, run Base64.Default.decode(encodedString). This returns a ByteArray. To convert it back into a readable string, append .decodeToString(). If using the legacy JVM class, invoke java.util.Base64.getDecoder().decode(encodedString) followed by wrapping the byte array in a String constructor.
What is the difference between standard and URL-Safe Base64?
Standard Base64 contains + and / characters, which are treated as path separators and reserved markers in URLs. URL-Safe Base64 substitutes these with hyphens (-) and underscores (_), making the encoded string safe to use as URL paths or query parameters.
Why does android.util.Base64 fail in my JUnit tests?
Android’s utility classes are not present in the local machine’s JVM runtime during unit tests. Calling android.util.Base64 throws a Mocking Stub error. To fix this, replace all instances of android.util.Base64 with the Kotlin standard library kotlin.io.encoding.Base64 which runs natively on both local development machines and physical devices.
Conclusion
Implementing high-performance, robust encoding doesn't have to be complex. While legacy approaches like java.util.Base64 and android.util.Base64 are still viable for platform-specific codebases, migrating your codebase to Kotlin's native kotlin.io.encoding.Base64 standard library is the ultimate best practice.
By unifying your base64 kotlin workflows under the modern standard library, enforcing explicit UTF-8 character encoding, and leveraging streaming techniques for heavy binary objects, you'll ensure your codebase remains fast, elegant, and highly portable across every major platform. Happy coding!










