Skip to Content

Coil - Load hình ảnh trong Android

Coil là gì?

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.

Tại sao chọn Coil?

Thư việnƯu điểmNhược điểm
CoilKotlin-first, nhẹ, Compose supportMới hơn
GlidePhổ biến, nhiều tài liệuJava-based
PicassoĐơn giảnÍt tính năng

Bước 1: Thêm Coil vào dự án

1.1. Mở file build.gradle.kts (Module: app)

Thêm dependency vào block dependencies:

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

1.2. Sync Gradle

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

1.3. Thêm permission Internet

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

Bước 2: Hiển thị ảnh đơn giản

2.1. Composable AsyncImage cơ bản

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…

2.2. Chạy thử

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!


Bước 3: Thêm Placeholder và Error handling

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

Cách tạo placeholder đơn giản

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

4.1. Tạo data class

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"), )

4.2. Tạo ProductCard

@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 ) } } } }

4.3. Tạo ProductList

@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) }

Bước 5: Caching và Performance

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

5.1. Custom ImageLoader

// 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 )

5.2. Preload ảnh (load trước)

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) }

Bước 6: Các tình huống đặc biệt

6.1. Ảnh tròn (Avatar)

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 )

6.2. Ảnh với góc bo (Rounded)

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 )

6.3. Load ảnh từ drawable local

AsyncImage( model = R.drawable.my_image, // Resource ID contentDescription = "Local image" )

6.4. Load ảnh từ File

val file = File("/path/to/image.jpg") AsyncImage( model = file, contentDescription = "File image" )

📝 Tóm tắt cho người mới

Các bước cơ bản:

  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 = "...")

Checklist khi dùng Coil:

  • Đã 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?

Các component chính:

ComponentKhi nào dùng
AsyncImageLoad ảnh đơn giản
SubcomposeAsyncImageCần custom UI cho loading/error
rememberAsyncImagePainterDùng với Image thường
Last updated on