Thực Hiện Các Yêu Cầu Mạng với Retrofit và OkHttp

Chào mừng các bạn quay trở lại với series “Android Developer Roadmap“! Sau khi chúng ta đã cùng nhau tìm hiểu về cấu trúc ứng dụng, giao diện người dùng, quản lý dữ liệu và các thành phần cơ bản khác, đã đến lúc chúng ta bước vào một khía cạnh cực kỳ quan trọng của hầu hết các ứng dụng hiện đại: Kết nối mạng (Networking).

Trong thế giới kết nối ngày nay, rất ít ứng dụng chỉ hoạt động độc lập trên thiết bị. Hầu hết đều cần truy xuất dữ liệu từ máy chủ từ xa, gửi dữ liệu lên đó, hoặc tương tác với các dịch vụ web. Việc thực hiện các yêu cầu mạng một cách hiệu quả, an toàn và dễ quản lý là kỹ năng thiết yếu của một lập trình viên Android chuyên nghiệp. Hôm nay, chúng ta sẽ khám phá bộ đôi cực kỳ phổ biến và mạnh mẽ trong lĩnh vực này: RetrofitOkHttp.

Tại sao cần Retrofit và OkHttp? Bức tranh toàn cảnh

Trước khi đi sâu vào cách sử dụng, hãy cùng xem xét tại sao chúng ta cần những thư viện này. Theo truyền thống, việc thực hiện yêu cầu HTTP trong Android thường sử dụng các API gốc như `HttpURLConnection` hoặc `DefaultHttpClient` (đã lỗi thời). Tuy nhiên, cách tiếp cận này khá cồng kềnh và dễ gặp lỗi:

  • Phải tự viết mã để mở kết nối, đọc dữ liệu, xử lý các luồng (stream).
  • Thường phải xử lý trên các luồng nền (background threads) để tránh chặn luồng chính (UI thread), điều này đòi hỏi kiến thức về lập trình bất đồng bộ.
  • Việc phân tích dữ liệu phản hồi (parsing JSON/XML) phải làm thủ công hoặc sử dụng các thư viện khác.
  • Xử lý lỗi phức tạp (Timeout, lỗi kết nối, lỗi máy chủ).

Các thư viện như Retrofit và OkHttp ra đời để giải quyết những vấn đề này, mang lại một cách tiếp cận hiện đại, hiệu quả và dễ bảo trì hơn nhiều.

OkHttp: Cỗ máy HTTP mạnh mẽ

OkHttp là một thư viện HTTP client do Square phát triển. Nó là nền tảng cơ bản, xử lý tầng giao thức mạng. OkHttp cung cấp:

  • Các kết nối hiệu quả: Hỗ trợ HTTP/2 (ghép kênh các yêu cầu trên cùng một kết nối), nén dữ liệu, pooling các kết nối.
  • Caching: Hỗ trợ caching phản hồi để giảm tải cho mạng và cải thiện hiệu suất.
  • Retry/Redirect tự động.
  • Xử lý các vấn đề về kết nối một cách mạnh mẽ.

Nói cách khác, OkHttp là “động cơ” thực hiện công việc nặng nhọc của việc gửi/nhận byte qua mạng.

Retrofit: Vỏ bọc type-safe cho API REST

Retrofit, cũng do Square phát triển, là một thư viện type-safe HTTP client cho Android và Java. Thay vì phải tự viết mã để tạo URL, thiết lập header, và xử lý phản hồi byte bằng byte, Retrofit cho phép bạn định nghĩa các API endpoints dưới dạng interface của Kotlin/Java sử dụng các annotation. Retrofit sẽ tự động tạo mã thực thi (implementation) cho interface này.

Retrofit hoạt động dựa trên OkHttp. Retrofit xử lý việc:

  • Tạo yêu cầu HTTP từ các định nghĩa interface của bạn.
  • Tích hợp dễ dàng với các thư viện chuyển đổi dữ liệu (Converter) như Gson, Moshi (cho JSON), SimpleXML (cho XML), Protocol Buffers, … để tự động phân tích dữ liệu phản hồi thành các đối tượng Kotlin/Java (và ngược lại khi gửi dữ liệu).
  • Hỗ trợ lập trình bất đồng bộ (với Callbacks) hoặc tích hợp với các thư viện quản lý bất đồng bộ hiện đại như Coroutines (Flow) hay RxJava.

Tóm lại, Retrofit đơn giản hóa đáng kể công việc định nghĩa và gọi các API. Nó giống như lớp “phiên dịch” giữa ứng dụng của bạn và cỗ máy OkHttp, giúp bạn làm việc ở mức độ cao hơn, gần với logic nghiệp vụ hơn là chi tiết mạng cấp thấp.

Thiết lập Retrofit và OkHttp trong dự án Android

Để bắt đầu sử dụng, bạn cần thêm các dependency vào file `build.gradle (Module: app)` của mình. Bạn sẽ cần Retrofit, OkHttp và một thư viện chuyển đổi dữ liệu (thường là Gson):

dependencies {
    // ... các dependencies khác

    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.11.0") // Phiên bản mới nhất có thể thay đổi
    
    // Retrofit Converter (Gson - cho JSON)
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")

    // OkHttp (Retrofit sử dụng OkHttp mặc định, nhưng thêm dependency này giúp bạn kiểm soát phiên bản và thêm interceptors)
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // Phiên bản mới nhất có thể thay đổi

    // OkHttp Logging Interceptor (rất hữu ích để debug)
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    // Nếu sử dụng Coroutines
    implementation("com.squareup.retrofit2:converter-scalars:2.11.0") // Có thể cần cho một số trường hợp với Coroutines
    implementation("com.squareup.retrofit2:retrofit-kotlin-coroutines-adapter:0.9.2") // Adapter cho Coroutines (lưu ý phiên bản này có thể cũ, kiểm tra phiên bản mới nhất hoặc dùng thẳng với suspend functions)
}

Hãy chắc chắn thay thế phiên bản bằng phiên bản mới nhất hiện có. Sau khi thêm dependency, sync lại dự án (Sync Now).

Ngoài ra, bạn cần thêm quyền truy cập Internet vào file `AndroidManifest.xml`:

<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

Định nghĩa API Endpoints với Retrofit Interfaces

Đây là nơi Retrofit tỏa sáng. Bạn định nghĩa các yêu cầu HTTP bằng cách tạo một interface và sử dụng các annotation của Retrofit. Giả sử bạn muốn gọi một API đơn giản để lấy danh sách bài viết.

Đầu tiên, định nghĩa lớp dữ liệu (data class) cho bài viết. Nếu bạn đã đọc bài về KotlinOOP, đây là lúc áp dụng:

data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

Tiếp theo, tạo interface API:

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface ApiService {

    // Ví dụ GET request để lấy tất cả bài viết
    @GET("posts")
    fun getAllPosts(): Call<List<Post>>

    // Ví dụ GET request để lấy bài viết cụ thể theo ID
    @GET("posts/{id}")
    fun getPostById(@Path("id") postId: Int): Call<Post>

    // Ví dụ GET request với query parameter
    @GET("posts")
    fun getPostsByUserId(@Query("userId") userId: Int): Call<List<Post>>

    // Ví dụ POST request (cần thêm @Body và data class cho request body)
    // @POST("posts")
    // fun createPost(@Body post: Post): Call<Post>

    // Ví dụ PUT request
    // @PUT("posts/{id}")
    // fun updatePost(@Path("id") postId: Int, @Body post: Post): Call<Post>

    // Ví dụ DELETE request
    // @DELETE("posts/{id}")
    // fun deletePost(@Path("id") postId: Int): Call<Void> // Void nếu không có body phản hồi
}

Giải thích các Annotation:

  • `@GET`, `@POST`, `@PUT`, `@DELETE`: Chỉ định phương thức HTTP.
  • `@Path(“id”)`: Chèn giá trị của tham số vào đường dẫn URL (ví dụ: `/posts/1`).
  • `@Query(“userId”)`: Thêm tham số query vào URL (ví dụ: `/posts?userId=1`).
  • `@Body`: Chỉ định rằng tham số này sẽ được sử dụng làm body của yêu cầu (thường dùng cho POST/PUT). Retrofit sẽ dùng Converter để chuyển đối tượng thành JSON (hoặc định dạng khác).
  • `Call`: Kiểu trả về mặc định của Retrofit cho các yêu cầu bất đồng bộ. `T` là kiểu dữ liệu mong muốn của phản hồi (sau khi được parse).
  • `Response`: Đại diện cho toàn bộ phản hồi HTTP, bao gồm cả mã trạng thái, header và body. `Call` có thể chuyển thành `Call>` nếu bạn muốn truy cập thông tin Response đầy đủ.

Tạo Instance Retrofit và Thực hiện Yêu cầu

Bạn cần tạo một instance của `Retrofit` để xây dựng các dịch vụ API của mình. Instance này thường được tạo một lần và sử dụng lại trong toàn bộ ứng dụng (ví dụ: thông qua Dependency Injection hoặc Singleton pattern, như chúng ta đã thảo luận trong bài về Mẫu Thiết Kế Repository).

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {

    private const val BASE_URL = "https://jsonplaceholder.typicode.com/" // Ví dụ base URL

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            // .client(okHttpClient) // Có thể thêm OkHttpClient đã cấu hình ở đây
            .build()
    }

    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}

Trong ví dụ này, chúng ta sử dụng `lazy` để đảm bảo Retrofit và ApiService chỉ được tạo khi cần thiết lần đầu tiên.

Bây giờ, bạn có thể thực hiện yêu cầu. Retrofit hỗ trợ cả Callback (cho Java hoặc Kotlin cũ hơn) và Suspend functions (cho Kotlin Coroutines).

Sử dụng Callbacks (Cách truyền thống)

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

// Trong Activity/Fragment/ViewModel hoặc nơi bạn xử lý logic
fun fetchPostsWithCallback() {
    val call = RetrofitClient.apiService.getAllPosts()

    call.enqueue(object : Callback<List<Post>> {
        override fun onResponse(call: Call<List<Post>>, response: Response<List<Post>>) {
            if (response.isSuccessful) {
                val posts = response.body()
                // Xử lý dữ liệu posts ở đây
                // Lưu ý: Callback này chạy trên luồng chính (main thread) nếu adapter mặc định được sử dụng,
                // hoặc trên một luồng khác tùy cấu hình Executor trong Retrofit Builder.
                // Tuy nhiên, với Callbacks mặc định trên Android, onResponse/onFailure thường được gửi về Main Thread.
                println("Fetched ${posts?.size} posts")
            } else {
                // Xử lý lỗi HTTP (ví dụ: 404, 500)
                println("Error: ${response.code()} - ${response.message()}")
            }
        }

        override fun onFailure(call: Call<List<Post>>, t: Throwable) {
            // Xử lý lỗi mạng (ví dụ: không có kết nối internet)
            println("Network Error: ${t.message}")
            t.printStackTrace()
        }
    })
}

`enqueue()` là phương thức thực hiện yêu cầu một cách bất đồng bộ trên một luồng nền. Kết quả sẽ được trả về thông qua các phương thức `onResponse` hoặc `onFailure` của Callback.

Sử dụng Suspend Functions với Kotlin Coroutines (Cách hiện đại, khuyến khích)

Nếu bạn đang sử dụng Kotlin và Coroutines (điều rất phổ biến trong phát triển Android hiện đại, đặc biệt khi làm việc với kiến trúc MVVM và Jetpack ViewModel), Retrofit hỗ trợ rất tốt Suspend functions. Bạn chỉ cần thêm từ khóa `suspend` vào phương thức trong interface API và thay đổi kiểu trả về:

import retrofit2.Response
import retrofit2.http.GET

interface ApiService {

    // Sử dụng suspend function
    @GET("posts")
    suspend fun getAllPostsCoroutine(): Response<List<Post>> // Hoặc List<Post> trực tiếp
    // ... các phương thức khác với suspend
}

Và khi gọi:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope // Cần dependency lifecycle-viewmodel-ktx

class MyViewModel : ViewModel() { // Ví dụ trong ViewModel

    fun fetchPostsWithCoroutine() {
        viewModelScope.launch { // Sử dụng CoroutineScope của ViewModel
            try {
                // Chuyển sang IO dispatcher để thực hiện yêu cầu mạng
                val response = withContext(Dispatchers.IO) {
                    RetrofitClient.apiService.getAllPostsCoroutine()
                }

                if (response.isSuccessful) {
                    val posts = response.body()
                    // Xử lý dữ liệu posts trên luồng chính (viewModelScope.launch mặc định là Main)
                    println("Fetched ${posts?.size} posts using Coroutine")
                    // Cập nhật LiveData hoặc StateFlow
                } else {
                    // Xử lý lỗi HTTP
                    println("Coroutine Error: ${response.code()} - ${response.message()}")
                }
            } catch (e: Exception) {
                // Xử lý lỗi mạng hoặc lỗi khác
                println("Coroutine Network Error: ${e.message}")
                e.printStackTrace()
            }
        }
    }
}

Cách này giúp mã code ngắn gọn, dễ đọc và quản lý lỗi hơn nhiều so với Callback Hell. Bạn cần thêm các dependency cho Coroutines và Jetpack Lifecycle để sử dụng `viewModelScope` và `withContext(Dispatchers.IO)`.

Xử lý Dữ liệu Phản hồi và Lỗi

Retrofit, kết hợp với Converter (ví dụ: Gson), sẽ tự động parse body phản hồi thành đối tượng Kotlin/Java của bạn. Nếu quá trình parse gặp lỗi (ví dụ: định dạng JSON sai lệch so với data class), nó sẽ ném ngoại lệ.

Khi sử dụng `Response`:

  • `response.isSuccessful`: Trả về true nếu mã trạng thái HTTP nằm trong khoảng 200-299.
  • `response.code()`: Mã trạng thái HTTP (ví dụ: 200, 404, 500).
  • `response.body()`: Đối tượng dữ liệu đã được parse nếu yêu cầu thành công.
  • `response.errorBody()`: Chứa body của phản hồi lỗi (nếu có), thường là JSON mô tả lỗi từ server. Bạn có thể cần parse body này riêng.
  • `response.message()`: Tin nhắn đi kèm với mã trạng thái HTTP.

Trong khối `onFailure` (Callback) hoặc `catch` (Coroutine), bạn sẽ xử lý các lỗi không liên quan đến mã trạng thái HTTP thành công, như mất kết nối mạng, timeout, lỗi phân giải tên miền, v.v. Đối tượng `Throwable` (hoặc `Exception`) cung cấp thông tin về nguyên nhân lỗi.

OkHttp Nâng Cao: Interceptors

Một trong những tính năng mạnh mẽ của OkHttp là Interceptors. Interceptors cho phép bạn chặn và sửa đổi các yêu cầu và phản hồi HTTP ở các giai đoạn khác nhau của quá trình. Điều này cực kỳ hữu ích cho các tác vụ như:

  • Logging (ghi log) yêu cầu và phản hồi.
  • Thêm các Header cố định (ví dụ: API key, Authorization token).
  • Xử lý cache tùy chỉnh.
  • Thử lại yêu cầu khi gặp lỗi cụ thể.

Bạn thêm Interceptors vào `OkHttpClient.Builder` khi tạo OkHttpClient, sau đó gán OkHttpClient này cho Retrofit.

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object RetrofitClient {

    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    // Tạo OkHttpClient với Interceptor
    private val okHttpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .readTimeout(60, TimeUnit.SECONDS)
            .connectTimeout(60, TimeUnit.SECONDS)
            .addInterceptor(HttpLoggingInterceptor().apply {
                // Đặt cấp độ log: BODY bao gồm cả headers và body
                level = HttpLoggingInterceptor.Level.BODY
            })
            // Thêm các interceptor khác nếu cần (ví dụ: cho Authentication)
            // .addInterceptor(AuthInterceptor())
            .build()
    }


    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient) // Gán OkHttpClient đã cấu hình vào Retrofit
            .build()
    }

    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}

`HttpLoggingInterceptor` là một Interceptor có sẵn từ thư viện OkHttp Logging (dependency chúng ta đã thêm ở trên). Nó rất hữu ích trong giai đoạn phát triển để xem chi tiết các yêu cầu và phản hồi HTTP trong Logcat.

Tóm tắt: OkHttp vs Retrofit

Để làm rõ hơn vai trò của từng thư viện, hãy xem bảng so sánh ngắn gọn này:

Tính năng OkHttp Retrofit
Mục đích chính Thực hiện các yêu cầu HTTP cấp thấp, quản lý kết nối, caching, retry Định nghĩa và gọi API REST/GraphQL bằng cách sử dụng Interface và Annotation, tích hợp Converter
Cấp độ làm việc Cấp thấp (request/response raw bytes, headers, status codes) Cấp cao (mapping API endpoints sang phương thức Kotlin/Java, tự động parsing dữ liệu)
Sử dụng độc lập? Có thể, là một HTTP client đầy đủ chức năng Không, cần một HTTP client bên dưới (thường là OkHttp)
Type-safety Ít hơn, bạn làm việc với Request/Response objects Rất cao, bạn làm việc trực tiếp với các đối tượng dữ liệu Kotlin/Java
Tích hợp Converter (JSON, XML…) Không trực tiếp, bạn phải tự xử lý body phản hồi Tích hợp chặt chẽ qua `addConverterFactory`
Hỗ trợ Asynchronous/Coroutine Có (với `enqueue` Callback, hoặc qua các extension/thư viện khác) Hỗ trợ mạnh mẽ với Callbacks, RxJava, và Suspend functions (Coroutine)
Interceptors Tính năng cốt lõi của OkHttp Không có Interceptors riêng, sử dụng Interceptors của OkHttp thông qua `client()`

Như bạn thấy, Retrofit và OkHttp không cạnh tranh mà bổ sung cho nhau. Retrofit cung cấp giao diện làm việc dễ dàng và type-safe, trong khi OkHttp cung cấp hiệu suất và các tính năng mạng mạnh mẽ ở tầng dưới.

Lời khuyên và Các Bước Tiếp Theo

  • Luôn thực hiện yêu cầu mạng trên luồng nền. Nếu dùng Callback, `enqueue()` sẽ làm điều này cho bạn. Nếu dùng Coroutines, hãy đảm bảo bạn chuyển sang `Dispatchers.IO` khi thực hiện cuộc gọi Retrofit (như ví dụ trên ViewModel). Việc chặn luồng chính (Main Thread) sẽ gây ra lỗi `NetworkOnMainThreadException` và khiến ứng dụng bị đơ (ANR – Application Not Responding).
  • Xử lý trạng thái mạng: Luôn kiểm tra xem thiết bị có kết nối internet hay không trước khi thực hiện yêu cầu.
  • Triển khai Repository Pattern: Kết hợp Retrofit với Repository Pattern (đọc thêm về Repository) là cách tiếp cận kiến trúc rất tốt. Repository sẽ là nơi bạn gọi API Retrofit và xử lý logic về nguồn dữ liệu (cache, database, network). ViewModel sẽ gọi Repository thay vì gọi trực tiếp Retrofit.
  • Sử dụng Dependency Injection (đọc thêm về DI) để cung cấp các instance của ApiService và OkHttpClient. Điều này giúp mã code dễ kiểm tra và quản lý hơn.
  • Tìm hiểu thêm về các Interceptors khác của OkHttp cho các tác vụ nâng cao như thêm header Auth, xử lý cache, v.v.

Kết luận

Retrofit và OkHttp là bộ đôi không thể thiếu đối với bất kỳ lập trình viên Android hiện đại nào khi làm việc với API. Chúng giúp đơn giản hóa công việc phức tạp của kết nối mạng, tăng hiệu suất, và làm cho mã code của bạn sạch sẽ, dễ đọc và bảo trì hơn rất nhiều. Nắm vững cách sử dụng chúng là một cột mốc quan trọng trên con đường trở thành Lập trình viên Android chuyên nghiệp.

Trong bài viết tiếp theo của series Android Developer Roadmap, chúng ta có thể sẽ đi sâu hơn vào cách kết hợp Retrofit với các thành phần kiến trúc khác như Repository Pattern và ViewModel, hoặc khám phá cách kiểm thử (testing) các tầng mạng này. Hãy đón chờ nhé!

Chúc các bạn học tốt và hẹn gặp lại!

Chỉ mục