Giới thiệu
Chuyển ảnh (Bitmap) thành Base64 string dùng để: gửi lên server qua JSON, lưu vào database/text, chia sẻ qua Intent, embed trong HTML/CSS. Bài viết hướng dẫn encode/decode Base64 hiệu quả, tránh OOM.
Các bước thực hiện
Bước 1: Tạo dự án mới
Android Studio → File → New Project → Empty View Activity.
Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
tools:context=".MainActivity">
<ImageView
android:id="@+id/ivSource"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/sample_image"
android:contentDescription="Ảnh gốc" />
<Button
android:id="@+id/btnEncode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Encode sang Base64" />
<Button
android:id="@+id/btnDecode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Decode về Bitmap" />
<TextView
android:id="@+id/tvBase64"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:padding="12dp"
android:background="@android:color/darker_gray"
android:textSize="12sp"
android:scrollbars="vertical"
tools:text="Base64 string sẽ hiển thị ở đây..." />
<ImageView
android:id="@+id/ivDecoded"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:scaleType="centerCrop"
android:visibility="gone"
android:contentDescription="Ảnh decode" />
</LinearLayout>
Bước 3: Utility Class (Base64Utils.kt)
package com.example.base64image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import java.io.ByteArrayOutputStream
object Base64Utils {
/**
* Encode Bitmap thành Base64 String
* @param bitmap Bitmap nguồn
* @param format Định dạng nén (JPEG/PNG)
* @param quality Chất lượng 0-100 (chỉ áp dụng JPEG)
* @return Base64 string (không có prefix "data:image/...;base64,")
*/
@Throws(Exception::class)
fun encodeToBase64(
bitmap: Bitmap,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 90
): String {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(format, quality, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()
return Base64.encodeToString(byteArray, Base64.NO_WRAP) // NO_WRAP: không xuống dòng
}
/**
* Decode Base64 String thành Bitmap
* @param base64String Base64 string (có thể có hoặc không có prefix data:image/...;base64,)
* @return Bitmap hoặc null nếu lỗi
*/
fun decodeFromBase64(base64String: String): Bitmap? {
var cleanBase64 = base64String.trim()
// Loại bỏ prefix "data:image/...;base64," nếu có
if (cleanBase64.contains("base64,")) {
cleanBase64 = cleanBase64.substringAfter("base64,")
}
return try {
val decodedBytes = Base64.decode(cleanBase64, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* Encode file ảnh trực tiếp thành Base64 (không load Bitmap vào memory trước)
* Tiết kiệm memory cho ảnh lớn
*/
@Throws(Exception::class)
fun encodeFileToBase64(filePath: String): String {
val bytes = java.io.File(filePath).readBytes()
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}
}
Bước 4: MainActivity.kt
package com.example.base64image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.base64image.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var originalBitmap: Bitmap? = null
private var base64String: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Load bitmap gốc từ drawable
originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.sample_image)
binding.btnEncode.setOnClickListener { encodeToBase64() }
binding.btnDecode.setOnClickListener { decodeFromBase64() }
}
private fun encodeToBase64() {
originalBitmap?.let { bitmap ->
try {
// Encode
base64String = Base64Utils.encodeToBase64(bitmap, Bitmap.CompressFormat.JPEG, 85)
// Hiển thị (cắt ngắn nếu quá dài)
val display = if (base64String.length > 500) {
"${base64String.substring(0, 500)}... [độ dài: ${base64String.length} chars]"
} else base64String
binding.tvBase64.text = display
Toast.makeText(this, "Đã encode (${base64String.length} chars)", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this, "Lỗi encode: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
private fun decodeFromBase64() {
if (base64String.isBlank()) {
Toast.makeText(this, "Chưa có Base64 string", Toast.LENGTH_SHORT).show()
return
}
val decodedBitmap = Base64Utils.decodeFromBase64(base64String)
decodedBitmap?.let {
binding.ivDecoded.setImageBitmap(it)
binding.ivDecoded.visibility = View.VISIBLE
Toast.makeText(this, "Decode thành công", Toast.LENGTH_SHORT).show()
} ?: run {
Toast.makeText(this, "Decode thất bại", Toast.LENGTH_SHORT).show()
}
}
}
Gửi Base64 lên Server (JSON)
// Retrofit + Gson
data class UploadRequest(
val imageBase64: String,
val fileName: String,
val mimeType: String = "image/jpeg"
)
// API Interface
@POST("upload")
suspend fun uploadImage(@Body request: UploadRequest): Response
// Gọi
val request = UploadRequest(
imageBase64 = base64String,
fileName = "photo_${System.currentTimeMillis()}.jpg"
)
// Thêm prefix cho server dễ xử lý: "data:image/jpeg;base64,$base64String"
Tối ưu Memory cho ảnh lớn
Vấn đề: Bitmap lớn → ByteArrayOutputStream → byte[] → Base64 → **3-4x memory**.
Giải pháp 1: Resize trước khi encode
fun resizeBitmap(bitmap: Bitmap, maxDimension: Int): Bitmap {
val scale = maxDimension.toFloat() / max(bitmap.width, bitmap.height)
if (scale >= 1f) return bitmap
return Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true)
}
Giải pháp 2: Stream-based encode (không load full bitmap)
// Chỉ dùng cho file, không cho Bitmap đã có trong memory
fun encodeFileStream(file: File): String {
val buffer = ByteArrayOutputStream()
file.inputStream().use { input ->
input.copyTo(buffer)
}
return Base64.encodeToString(buffer.toByteArray(), Base64.NO_WRAP)
}
Giải pháp 3: Coroutine + IO Dispatcher
suspend fun encodeBitmapAsync(bitmap: Bitmap): String = withContext(Dispatchers.IO) {
Base64Utils.encodeToBase64(bitmap)
}
Base64 Flags (android.util.Base64)
| Flag | Mô tả |
|---|---|
DEFAULT | Có xuống dòng (line wrap 76 chars) - chuẩn RFC 2045 |
NO_WRAP | Khuyên dùng - không xuống dòng, gọn cho JSON/network |
NO_PADDING | Bỏ padding '=' ở cuối |
URL_SAFE | Thay '+' → '-', '/' → '_' (an toàn cho URL) |
NO_WRAP or URL_SAFE | Kết hợp (dùng cho URL param) |
Lưu ý quan trọng
- Base64 tăng kích thước ~33% so với binary gốc.
- Không dùng cho ảnh lớn (>1MB) trong JSON - dùng multipart upload (Retrofit @Part).
- Luôn dùng
Base64.NO_WRAPcho network/JSON. - Decode: Xử lý prefix
data:image/jpeg;base64,nếu server trả về Data URI. - Bitmap.compress() với PNG: quality bị bỏ qua (lossless). JPEG: quality 0-100.
- Test decode trên thiết bị thật - một số OEM modify Base64 behavior (hiếm).