Xử lý Networking trong Android: Tạm biệt try-catch với NetworkResult
Retrofit CallAdapter: Quy trình "Đóng Gói & Vận Chuyển" Cao Cấp
Chào mọi người, lại là mình đây! 👋
Ở bài viết trước, chúng ta đã bàn về NetworkBoundResource và triết lý "Ví tiền & Cây ATM". Hôm nay, mình muốn đi sâu hơn vào phần Network - cụ thể là làm thế nào để xử lý các phản hồi từ API (API Responses) một cách "Clean" và "Sang trọng" nhất.
1. Mở đầu: Nỗi đau của việc "Nhận hàng trần trụi"
Hãy tưởng tượng bạn đặt mua một món đồ dễ vỡ từ nước ngoài (Gọi API). Thông thường, khi dùng Retrofit cơ bản, quy trình sẽ diễn ra như thế này:

Ông Shipper (Retrofit) chạy đi lấy hàng và ném toẹt vào mặt bạn (ViewModel/Repository) một gói hàng trần trụi (Response<T> hoặc T).
Hậu quả là:
- Hàng vỡ (Exception): Nếu xe bị tai nạn (Mất mạng, Timeout), Shipper la làng và ném thẳng cái
IOExceptionvào mặt bạn. Nếu khôngtry-catchkịp? Crash App! 💥 - Hàng lỗi (HttpException): Server trả về hộp rỗng (404, 500). Shipper vẫn bảo "Giao thành công nhé", nhưng mở ra thì bên trong toàn rác. Bạn phải if-else check
response.isSuccessful()mệt nghỉ. - Hỗn loạn: Chỗ nào gọi API cũng phải
try-catch. Code base nhìn như một bãi chiến trường với hàng đống logic lặp lại (boilerplate).
Mong muốn của chúng ta: Có một Dịch vụ trung gian (CallAdapter) đứng ra lo liệu mọi thứ. Dù hàng vỡ, hàng hỏng hay hàng ngon, về đến tay mình (ViewModel) luôn là một cái Hộp (Sealed Class) được đóng gói đẹp đẽ, an toàn.
2. Giải pháp: Mô hình "Xưởng Đóng Gói" (CallAdapter Pattern)
Chúng ta sẽ áp dụng mô hình Sealed Class kết hợp với Retrofit CallAdapter để giải quyết vấn đề này.

Mô hình này gồm 4 thành phần chính:
- Cái Hộp (
NetworkResult<T>): Sealed Class chứa kết quả đã đóng gói. - Nhân viên kiểm hàng (
NetworkResultCall): Đứng chặn giữa Retrofit và App để kiểm tra, bắt lỗi. - Ông chủ xưởng (
NetworkResultCallAdapter): Điều phối nhân viên làm việc. - Khách hàng (
ViewModel): Chỉ việc nhận hộp và mở quà.
Kết quả cuối cùng, luồng dữ liệu của chúng ta sẽ "nuột" như thế này:

3. Bước 1: Thiết kế cái Hộp (NetworkResult<T>)
Đầu tiên, hãy tạo ra cái hộp để chứa hàng. Chúng ta dùng Sealed Class để chia ra 3 ngăn rõ rệt:
sealed class NetworkResult<T : Any> {
// Ngăn 1: Hàng ngon (Success)
class Success<T : Any>(val data: T) : NetworkResult<T>()
// Ngăn 2: Lỗi từ Server (Error - 4xx, 5xx)
class Error<T : Any>(val code: Int, val message: String?) : NetworkResult<T>()
// Ngăn 3: Lỗi vận chuyển (Exception - Mất mạng, Timeout)
class Exception<T : Any>(val e: Throwable) : NetworkResult<T>()
}
4. Bước 2: Tuyển nhân viên kiểm hàng (NetworkResultCall)
Đây là phần "Core Tech" nhất. Chúng ta cần một class kế thừa từ Call<NetworkResult<T>> để đánh tráo gói hàng của Retrofit.
Nhiệm vụ của anh nhân viên này là:
- Nhận hàng từ Shipper gốc (
proxy.enqueue). - Kiểm tra hàng (
onResponse):- Nếu ngon (
isSuccessful) -> Đóng vào Hộp Success. - Nếu lỗi (
!isSuccessful) -> Đóng vào Hộp Error.
- Nếu ngon (
- Nếu Shipper gặp nạn (
onFailure):- Viết Thư Exception.
class NetworkResultCall<T : Any>(
private val proxy: Call<T>
) : Call<NetworkResult<T>> {
override fun enqueue(callback: Callback<NetworkResult<T>>) {
proxy.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val networkResult = try {
val body = response.body()
if (response.isSuccessful && body != null) {
NetworkResult.Success(body)
} else {
NetworkResult.Error(code = response.code(), message = response.message())
}
} catch (e: Exception) {
NetworkResult.Exception(e)
}
callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val networkResult = NetworkResult.Exception<T>(t)
callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
}
})
}
// Các hàm khác override đơn giản (clone, execute, isExecuted...)
override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone())
// ... (Rút gọn cho đỡ dài dòng) ...
}
5. Bước 3: Mở xưởng & Cấp giấy phép (CallAdapter & Factory)
Bây giờ cần đăng ký dịch vụ này với Retrofit.

class NetworkResultCallAdapter(
private val resultType: Type
) : CallAdapter<Type, Call<NetworkResult<Type>>> {
override fun responseType(): Type = resultType
override fun adapt(call: Call<Type>): Call<NetworkResult<Type>> {
return NetworkResultCall(call)
}
}
class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// Chỉ nhận đơn hàng nào yêu cầu trả về NetworkResult
if (getRawType(returnType) != Call::class.java) {
return null
}
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
if (getRawType(callType) != NetworkResult::class.java) {
return null
}
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
return NetworkResultCallAdapter(resultType)
}
companion object {
fun create() = NetworkResultCallAdapterFactory()
}
}
Đừng quên đăng ký nó khi khởi tạo Retrofit nhé:
Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) // <--- Đăng ký ở đây
.addConverterFactory(GsonConverterFactory.create())
.build()
6. Thành quả: Code sạch, Tâm an!
Sau khi đã setup xong cái "Xưởng đóng gói" này, việc gọi API trở nên nhàn tênh.
API Interface: Không cần Response<T>, trả về thẳng NetworkResult.
interface MovieService {
@GET("movies")
suspend fun getMovies(): NetworkResult<List<Movie>>
}
ViewModel: Nhận hàng và mở hộp thôi!
viewModelScope.launch {
when (val result = repository.getMovies()) {
is NetworkResult.Success -> {
showMovies(result.data) // Hàng ngon, dùng luôn
}
is NetworkResult.Error -> {
showError("Lỗi Server: ${result.code}") // Hàng lỗi, chửi Server
}
is NetworkResult.Exception -> {
showError("Mất mạng rồi: ${result.e.message}") // Hàng vỡ, chửi nhà mạng
}
}
}

7. Kết luận
Bằng cách sử dụng CallAdapter kết hợp với Sealed Class, chúng ta đã:
- Loại bỏ hoàn toàn việc
try-catchthủ công ở từng API call. - Tách biệt logic xử lý lỗi mạng ra khỏi logic nghiệp vụ.
- An toàn tuyệt đối: App không bao giờ crash vì lỗi network không được bắt (uncaught exception).
Hãy biến việc xử lý API trở nên chuyên nghiệp như một dịch vụ vận chuyển cao cấp, thay vì kiểu "đem con bỏ chợ" nhé!
Hẹn gặp lại các bạn trong các bài viết tiếp theo về Android Architecture! Happy Coding! 🚀
All rights reserved