Coil - Load hình ảnh
Coil là gì?
Phần tiêu đề “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ự:
- Download ảnh từ internet
- Decode ảnh thành Bitmap
- Xử lý cache để không download lại
- Hiển thị placeholder khi đang load
- 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?
Phần tiêu đề “Tại sao chọn Coil?”| 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 |
Bước 1: Thêm Coil vào dự án
Phần tiêu đề “Bước 1: Thêm Coil vào dự án”1.1. Mở file build.gradle.kts (Module: app)
Phần tiêu đề “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
Phần tiêu đề “1.2. Sync Gradle”Click Sync Now hoặc vào File > Sync Project with Gradle Files
1.3. Thêm permission Internet
Phần tiêu đề “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
Phần tiêu đề “Bước 2: Hiển thị ảnh đơn giản”2.1. Composable AsyncImage cơ bản
Phần tiêu đề “2.1. Composable AsyncImage cơ bản”import coil.compose.AsyncImage
@Composablefun 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ử
Phần tiêu đề “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
Phần tiêu đề “Bước 3: Thêm Placeholder và Error handling”Khi load ảnh từ internet, có 3 trạng thái:
- Loading: Đang tải ảnh
- Success: Load thành công
- Error: Lỗi (mất mạng, URL sai, server lỗi…)
import coil.compose.AsyncImageimport coil.compose.SubcomposeAsyncImageimport coil.compose.SubcomposeAsyncImageContent
@Composablefun 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ốngContentScale.FillBounds: Kéo dãn ảnh để fill, có thể méo
Cách tạo placeholder đơn giản
Phần tiêu đề “Cách tạo placeholder đơn giản”Nếu chưa có ảnh placeholder, dùng Box với màu:
@Composablefun 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”4.1. Tạo data class
Phần tiêu đề “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ẫuval 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
Phần tiêu đề “4.2. Tạo ProductCard”@Composablefun 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
Phần tiêu đề “4.3. Tạo ProductList”@Composablefun ProductList(products: List<Product>) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(8.dp) ) { items(products, key = { it.id }) { product -> ProductCard(product) } }}
// Sử dụng@Composablefun ProductScreen() { ProductList(products = sampleProducts)}Bước 5: Caching và Performance
Phần tiêu đề “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
Phần tiêu đề “5.1. Custom ImageLoader”// Trong Application class hoặc Hilt moduleval 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ụngAsyncImage( 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)
Phần tiêu đề “5.2. Preload ảnh (load trước)”val context = LocalContext.currentval 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
Phần tiêu đề “Bước 6: Các tình huống đặc biệt”6.1. Ảnh tròn (Avatar)
Phần tiêu đề “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)
Phần tiêu đề “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
Phần tiêu đề “6.3. Load ảnh từ drawable local”AsyncImage( model = R.drawable.my_image, // Resource ID contentDescription = "Local image")6.4. Load ảnh từ File
Phần tiêu đề “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
Phần tiêu đề “📝 Tóm tắt cho người mới”Các bước cơ bản:
Phần tiêu đề “Các bước cơ bản:”- Thêm dependency:
implementation("io.coil-kt:coil-compose:2.5.0") - Thêm permission INTERNET trong Manifest
- Dùng
AsyncImage(model = url, contentDescription = "...")
Checklist khi dùng Coil:
Phần tiêu đề “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:
Phần tiêu đề “Các component chính:”| 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 |