Bỏ qua để đến nội dung

Coil - Load hình ảnh

Khi xây dựng ứng dụng Android, bạn thường cần hiển thị hình ảnh từ internet. Ví dụ:

  • Avatar của user
  • Hình sản phẩm trong app bán hàng
  • Banner quảng cáo
  • Ảnh trong feed mạng xã hội

Vấn đề: Android không có sẵn cách đơn giản để load ảnh từ URL. Bạn phải tự:

  1. Download ảnh từ internet
  2. Decode ảnh thành Bitmap
  3. Xử lý cache để không download lại
  4. Hiển thị placeholder khi đang load
  5. Xử lý lỗi nếu không load được

Giải pháp: Coil (Coroutine Image Loader) - thư viện load ảnh hiện đại cho Android, được viết bằng Kotlin và hỗ trợ tốt Jetpack Compose.

Thư viện Ưu điểm Nhược điểm
Coil Kotlin-first, nhẹ, Compose support Mới hơn
Glide Phổ biến, nhiều tài liệu Java-based
Picasso Đơn giản Ít tính năng

Thêm dependency vào block dependencies:

dependencies {
// Coil cho Jetpack Compose
implementation("io.coil-kt:coil-compose:2.5.0")
}

Click Sync Now hoặc vào File > Sync Project with Gradle Files

Mở file app/src/main/AndroidManifest.xml, thêm permission:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Thêm dòng này -->
<uses-permission android:name="android.permission.INTERNET" />
<application ...>
...
</application>
</manifest>

Tại sao cần permission INTERNET?

  • Android bảo vệ người dùng bằng cách yêu cầu app khai báo các quyền cần sử dụng
  • Load ảnh từ URL = truy cập internet = cần permission

import coil.compose.AsyncImage
@Composable
fun UserAvatar() {
AsyncImage(
model = "https://picsum.photos/200", // URL của ảnh
contentDescription = "Avatar", // Mô tả cho accessibility
modifier = Modifier.size(100.dp) // Kích thước
)
}

Giải thích từng tham số:

  • model: URL của ảnh cần load (có thể là String, Uri, hoặc ImageRequest)
  • contentDescription: Mô tả ảnh cho người dùng khiếm thị (screen reader sẽ đọc)
  • modifier: Điều chỉnh kích thước, padding, shape…

Thêm UserAvatar() vào MainActivity:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
UserAvatar()
}
}
}

Chạy app → Bạn sẽ thấy một ảnh ngẫu nhiên từ Picsum!


Khi load ảnh từ internet, có 3 trạng thái:

  1. Loading: Đang tải ảnh
  2. Success: Load thành công
  3. Error: Lỗi (mất mạng, URL sai, server lỗi…)
import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
@Composable
fun UserAvatarWithStates() {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = "Avatar",
modifier = Modifier
.size(100.dp)
.clip(CircleShape), // Bo tròn ảnh
// Ảnh hiển thị khi ĐANG LOAD
placeholder = painterResource(R.drawable.placeholder),
// Ảnh hiển thị khi LỖI
error = painterResource(R.drawable.error_image),
// Cách scale ảnh
contentScale = ContentScale.Crop
)
}

Giải thích contentScale:

  • ContentScale.Crop: Cắt ảnh để fill khung, giữ tỷ lệ
  • ContentScale.Fit: Ảnh vừa khung, có thể có khoảng trống
  • ContentScale.FillBounds: Kéo dãn ảnh để fill, có thể méo

Nếu chưa có ảnh placeholder, dùng Box với màu:

@Composable
fun UserAvatarSimple() {
SubcomposeAsyncImage(
model = "https://picsum.photos/200",
contentDescription = "Avatar",
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
) {
val state = painter.state
when (state) {
is AsyncImagePainter.State.Loading -> {
// Hiển thị khi đang load
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
}
}
is AsyncImagePainter.State.Error -> {
// Hiển thị khi lỗi
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Warning,
contentDescription = "Error",
tint = Color.White
)
}
}
else -> {
// Hiển thị ảnh khi load thành công
SubcomposeAsyncImageContent()
}
}
}
}

Bước 4: Ví dụ thực tế - Danh sách sản phẩm

Phần tiêu đề “Bước 4: Ví dụ thực tế - Danh sách sản phẩm”
data class Product(
val id: Int,
val name: String,
val price: Double,
val imageUrl: String
)
// Dữ liệu mẫu
val sampleProducts = listOf(
Product(1, "iPhone 15", 999.0, "https://picsum.photos/seed/1/300/300"),
Product(2, "MacBook Pro", 1999.0, "https://picsum.photos/seed/2/300/300"),
Product(3, "AirPods Pro", 249.0, "https://picsum.photos/seed/3/300/300"),
Product(4, "iPad Air", 599.0, "https://picsum.photos/seed/4/300/300"),
)
@Composable
fun ProductCard(product: Product) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Ảnh sản phẩm
AsyncImage(
model = product.imageUrl,
contentDescription = product.name,
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop,
placeholder = ColorPainter(Color.LightGray),
error = ColorPainter(Color.Red.copy(alpha = 0.3f))
)
Spacer(modifier = Modifier.width(16.dp))
// Thông tin sản phẩm
Column {
Text(
text = product.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "$${product.price}",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
@Composable
fun ProductList(products: List<Product>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
items(products, key = { it.id }) { product ->
ProductCard(product)
}
}
}
// Sử dụng
@Composable
fun ProductScreen() {
ProductList(products = sampleProducts)
}

Coil tự động cache ảnh để không phải download lại. Nhưng bạn có thể tùy chỉnh:

// Trong Application class hoặc Hilt module
val imageLoader = ImageLoader.Builder(context)
.crossfade(true) // Animation fade khi load xong
.crossfade(300) // Duration 300ms
.memoryCachePolicy(CachePolicy.ENABLED) // Cache trong RAM
.diskCachePolicy(CachePolicy.ENABLED) // Cache trên disk
.build()
// Sử dụng
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
contentDescription = null,
imageLoader = imageLoader
)
val context = LocalContext.current
val imageLoader = context.imageLoader
// Preload ảnh trước khi hiển thị
LaunchedEffect(Unit) {
val request = ImageRequest.Builder(context)
.data("https://example.com/large-image.jpg")
.build()
imageLoader.enqueue(request)
}

AsyncImage(
model = "https://example.com/avatar.jpg",
contentDescription = "Avatar",
modifier = Modifier
.size(60.dp)
.clip(CircleShape) // Bo tròn
.border(2.dp, Color.White, CircleShape), // Viền trắng
contentScale = ContentScale.Crop
)
AsyncImage(
model = "https://example.com/product.jpg",
contentDescription = "Product",
modifier = Modifier
.size(120.dp)
.clip(RoundedCornerShape(16.dp)), // Bo góc 16dp
contentScale = ContentScale.Crop
)
AsyncImage(
model = R.drawable.my_image, // Resource ID
contentDescription = "Local image"
)
val file = File("/path/to/image.jpg")
AsyncImage(
model = file,
contentDescription = "File image"
)

  1. Thêm dependency: implementation("io.coil-kt:coil-compose:2.5.0")
  2. Thêm permission INTERNET trong Manifest
  3. Dùng AsyncImage(model = url, contentDescription = "...")
  • Đã thêm dependency?
  • Đã thêm INTERNET permission?
  • Có placeholder cho loading state?
  • Có error image cho trường hợp lỗi?
  • Đã set contentScale phù hợp?
Component Khi nào dùng
AsyncImage Load ảnh đơn giản
SubcomposeAsyncImage Cần custom UI cho loading/error
rememberAsyncImagePainter Dùng với Image thường