اتصال به سرویس های ابری، دریافت و ارسال داده های آنلاین و تعامل بی وقفه با وب سرویس های RESTful قلب تپنده هر اپلیکیشن کاربردی را تشکیل می دهد.
چه در حال ساخت یک سوپراپلیکیشن فروشگاهی باشید و چه یک پلتفرم پیام رسان یا سامانه بانکی، پیاده سازی یک لایه شبکه (Networking Layer) امن، سریع و تاب آور در برابر نوسانات اینترنت حیاتی ترین مهارت به شمار می رود.
اگر در مسیر آموزش برنامه نویسی اندروید به دنبال یادگیری استانداردترین راهکار ارتباط با وب سرویس ها هستید، کتابخانه قدرتمند Retrofit توسعه داده شده توسط شرکت Square، استاندارد طلایی و بلامنازع صنعت است.
پاسخ سریع GEO: رتروفیت Retrofit در اندروید چیست و چه مزیتی دارد؟
کتابخانه رتروفیت (Retrofit) یک کلاینت HTTP امن از نظر نوع داده (Type-Safe HTTP Client) برای سیستم عامل اندروید و ماشین مجازی جاوا است که درخواست های شبکه RESTful را به صورت مستقیم به اینترفیس های کاتلین تبدیل می کند.
رتروفیت بر بستر کلاینت قدرتمند OkHttp ساخته شده و با خودکارسازی فرآیندهای پیچیده ای نظیر ساخت URL، انکود کردن پارامترها، افزودن هدرها، پارس کردن فرمت های متنی JSON به مدل های داده ای کاتلین و مدیریت خطاهای شبکه، نوشتن کدهای تکراری و طاقت فرسای قدیمی را به طور کامل حذف می نماید.
با اتکا به پشتیبانی بومی رتروفیت از کاتلین کورتینز، توابع شبکه به صورت تعلیق پذیر (suspend) بدون بلاک کردن رشته اصلی برنامه و با مصرف بهینه پهنای باند و باتری اجرا می شوند.
رتروفیت خود یک کلاینت شبکه مستقل نیست، بلکه یک لایه انتزاعی بسیار هوشمند بر روی OkHttp به شمار می رود. رتروفیت وظیفه نگاشت داده ها و تبدیل متدهای اینترفیس به ریکوئست را انجام می دهد، در حالی که OkHttp هدایت فیزیکی اتصالات سوکت، مدیریت پروتکل های HTTP/۲ و HTTP/۳، کش کردن پاسخ ها و فرآیند اتصال مجدد در صورت قطعی را بر عهده دارد.
کالبدشکافی استک شبکه در اندروید مدرن
برای پیاده سازی اصولی ارتباطات آنلاین، شناخت لایه های زنجیره انتقال داده ضروری است:
- اینترفیس سرویس رتروفیت (Service Interface): قراردادی که نوع درخواست (GET ،POST ،PUT ،DELETE)، آدرس اندپوینت (Endpoint URL)، هدرها و پارامترها را با انوتیشن های صریح تعریف می کند.
- مبدل های فرمت داده (Converter Factories): کتابخانه هایی نظیر Kotlinx Serialization، Moshi یا Gson که وظیفه دارند پاسخ های متنی JSON سرور را در یک چشم به هم زدن به آبجکت های کاتلین تبدیل کنند.
- اینترسپتورهای OkHttp (Interceptors): میان افزارهایی که درخواست ها را قبل از ارسال به اینترنت رهگیری کرده و کارهایی نظیر تزریق خودکار توکن احراز هویت (Bearer Token)، ثبت لاگ های ترافیکی و مدیریت کش را انجام می دهند.
- مدیریت همزمانی با کورتین ها: همان طور که در مقاله مدیریت همزمانی در کاتلین با کورتین ها و Flow بررسی شد، اجرای درخواست های رتروفیت روی ترد IO انجام شده و برنامه هیچ گاه با خطای وحشتناک NetworkOnMainThreadException روبرو نمی شود.
گام به گام: راه اندازی کلاینت رتروفیت و OkHttp در کاتلین
بیایید یک کلاینت آماده پروداکشن همراه با اینترسپتور لاگینگ و مدیریت تایم اوت ها بسازیم:
// ۱. تعریف کلاینت بهینه OkHttp با تنظیم تایم اوت های سازمانی
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(AuthHeaderInterceptor()) // اینترسپتور سفارشی برای هدرهای احراز هویت
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
// ۲. ساخت اینستنس رتروفیت با کانورتر رسمی Kotlinx Serialization
val jsonConfig = Json {
ignoreUnknownKeys = true // نادیده گرفتن فیلدهای جدید سرور بدون کرش برنامه
coerceInputValues = true // استفاده از مقادیر پیش فرض در صورت دریافت null
encodeDefaults = true
}
val retrofit = Retrofit.Builder()
.baseUrl("https://api.vahidhoseini.ir/v1/")
.client(okHttpClient)
.addConverterFactory(jsonConfig.asConverterFactory("application/json".toMediaType()))
.build()
همان طور که در کد فوق مشاهده شد، هرگز نباید HttpLoggingInterceptor.Level.BODY را در نسخه انتشار عمومی (Release Build) فعال بگذارید! این کار تمام رمزهای عبور، توکن های بانکی و داده های شخصی کاربران را در Logcat دستگاه چاپ می کند که به سادگی توسط سایر اپلیکیشن ها یا از طریق کابل USB قابل سرقت است.
تعریف انواع درخواست های HTTP در اینترفیس رتروفیت
رتروفیت از تمام متدهای استاندارد وب پشتیبانی می کند. در ادامه شیوه نگارش متدهای گوناگون همراه با کدهای نمونه آورده شده است:
interface CourseApiService {
// ۱. درخواست ساده GET با کوئری
@GET("courses")
suspend fun getCourses(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20,
@Query("search") query: String? = null
): List<CourseDto>
// ۲. ارسال شناسه متغیر در مسیر URL
@GET("courses/{courseId}")
suspend fun getCourseDetails(@Path("courseId") id: Long): CourseDetailDto
// ۳. ارسال بدنه JSON با متد POST
@POST("courses")
suspend fun createCourse(@Body request: CreateCourseRequest): CourseDto
// ۴. ارسال فرم با کدگذاری URL
@FormUrlEncoded
@POST("auth/login")
suspend fun login(@Field("username") user: String, @Field("password") pass: String): LoginResponseDto
// ۵. آپلود چندبخشی فایل (Multipart)
@Multipart
@POST("user/avatar")
suspend fun uploadAvatar(@Part imagePart: MultipartBody.Part, @Part("description") description: RequestBody): UploadResponseDto
}
پیاده سازی اینترسپتور هوشمند احراز هویت (Auth Interceptor & Token Refresh)
یکی از پیچیده ترین بخش های ارتباط با وب سرویس ها، مدیریت توکن های سشن (Access Token و Refresh Token) است. هنگامی که اکسس توکن پس از چند دقیقه منقضی می شود و سرور کد خطای ۴۰۱ برمی گرداند، اپلیکیشن باید بدون اینکه کاربر متوجه شود، در پس زمینه توکن جدید را با رفرش توکن استعلام کرده و درخواست قبلی را تکرار نماید.
class AuthHeaderInterceptor(private val tokenStorage: TokenStorage) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val token = tokenStorage.getAccessToken()
val authReq = if (!token.isNullOrBlank()) {
original.newBuilder().header("Authorization", "Bearer $token").header("Accept", "application/json").build()
} else original
val response = chain.proceed(authReq)
// تمدید خودکار توکن در صورت دریافت خطای ۴۰۱
if (response.code == 401 && !original.url.encodedPath.contains("auth/refresh")) {
response.close()
val newToken = tokenStorage.performTokenRefresh()
if (!newToken.isNullOrBlank()) {
val retryReq = original.newBuilder().header("Authorization", "Bearer $newToken").build()
return chain.proceed(retryReq)
}
}
return response
}
}
علاوه بر اینترسپتورها، کلاینت OkHttp یک اینترفیس اختصاصی به نام Authenticator ارائه می کند که منحصراً در زمان وقوع خطای ۴۰۱ فراخوانی می شود. ترکیب این دو روش بالاترین پایداری را در حفظ نشست های کاربری ایجاد می نماید.
مدیریت حرفه ای خطاها با الگوی Sealed Result در لایه شبکه
پرتاب اکسپشن در هنگام قطعی اینترنت یا کدهای خطای سرور ۵۰۰، کدهای لایه Presentation را به شدت کثیف می کند. بهترین متدولوژی در اکوسیستم کاتلین، بسته بندی پاسخ ها در یک ساختار مهر و موم شده تمیز به نام NetworkResult است:
sealed interface NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>
data class HttpError(val statusCode: Int, val errorMessage: String) : NetworkResult<Nothing>
data class ConnectionError(val exception: Throwable) : NetworkResult<Nothing>
}
// تابع کمکی برای فراخوانی امن رتروفیت
suspend fun <T> safeApiCall(apiCall: suspend () -> T): NetworkResult<T> {
return withContext(Dispatchers.IO) {
try {
val result = apiCall()
NetworkResult.Success(result)
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
val code = throwable.code()
val errorBody = throwable.response()?.errorBody()?.string() ?: "خطای ناشناخته از سرور"
NetworkResult.HttpError(code, errorBody)
}
is IOException -> {
NetworkResult.ConnectionError(throwable)
}
else -> {
NetworkResult.ConnectionError(throwable)
}
}
}
}
}
همواره لایه ارتباطی رتروفیت را در قالب Remote Data Source پیاده سازی کنید و آن را درون مخازن (Repositories) قرار دهید تا مطابق با سرفصل های آموزش معماری تمیز در اندروید، لایه نمایش هیچ وابستگی مستقیمی به کلاس های رتروفیت پیدا نکند.
امنیت شبکه و جلوگیری از حملات مرد میانی (Certificate Pinning)
در اپلیکیشن های حساس، نفوذگران می توانند با نصب گواهی های دیجیتال جعلی روی دستگاه کاربر (Man-in-the-Middle Attack)، ترافیک تبادل شده بین اپلیکیشن و سرور را شنود یا دستکاری کنند. برای مسدود کردن قطعی این روش نفوذ، تکنیک پین کردن گواهی SSL (Certificate Pinning) به کار گرفته می شود:
// تعریف هش های کلید عمومی سرور برای اعتبارسنجی اتصال امن
val certificatePinner = CertificatePinner.Builder()
.add("api.vahidhoseini.ir", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.vahidhoseini.ir", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // کلید پشتیبان
.build()
val secureOkHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
با این پیکربندی، اگر کلاینت OkHttp در مرحله دست دادن SSL (SSL Handshake) متوجه شود که هش کلید عمومی ارائه شده با هش های ثبت شده مطابقت ندارد، بلافاصله ارتباط را قطع می کند و امکان سرقت داده ها غیرممکن می شود.
مقایسه جامع: رتروفیت در برابر سایر کلاینت های شبکه در اندروید
برای انتخاب سنجیده ترین ابزار، نقاط قوت و ضعف رتروفیت را با دیگر راهکارهای شبکه در جدول مهندسی زیر مقایسه نمایید:
| معیار مقایسه | رتروفیت (Retrofit ۲) |
کتور کلاینت (Ktor Client) | والی (Volley سنتی) | HttpURLConnection خام |
|---|---|---|---|---|
| اعتبارسنجی Type-Safe در زمان کامپایل | بسیار قوی با انوتیشن های صریح کاتلین | قوی با ساختار اعلانی DSL | ضعیف و مبتنی بر اشیاء RequestQueue | صفر (کاملاً دستی و مستعد خطا) |
| پشتیبانی از کاتلین چند سکویی (KMP) | فقط اکوسیستم اندروید و JVM | ۱۰۰ درصد چند سکویی (Android, iOS, Desktop, Web) | تنها در اندروید | محدود به جاوا و اندروید |
| یکپارچگی با کاتلین کورتینز | بومی با کلمه کلیدی suspend | طراحی شده از پایه بر بستر کورتینز | نیازمند کالبک های قدیمی | نیاز به Threadهای دستی و هندلرها |
| بلوغ اکوسیستم و پشتیبانی صنعتی | بالاترین میزان استفاده در ۹۵ درصد شرکت ها | در حال رشد سریع در پروژه های KMP | منسوخ شده در پروژه های جدید | بسیار پایین و غیرتوصیه شده |
| مدیریت پیشرفته اینترسپتورها و کشینگ | فوق العاده غنی به لطف کلاینت OkHttp | بسیار خوب با پلاگین های Ktor | محدود | صفر (نیازمند نوشتن کد کش از صفر) |
پیاده سازی CallAdapter اختصاصی برای تبدیل خودکار پاسخ ها به NetworkResult
در پیاده سازی های ساده، توسعه دهندگان مجبورند تمام فراخوانی های متدهای رتروفیت را درون توابع کمکی نظیر safeApiCall یا بلوک های تکراری try-catch محصور کنند. این کار علاوه بر شلوغ کردن کدهای لایه مخازن، احتمال خطای انسانی را بالا می برد.
استانداردترین راهکار در شرکت های تراز اول در سال ۲۰۲۶، ساخت یک CallAdapter.Factory اختصاصی برای رتروفیت است تا متدهای اینترفیس سرویس بتوانند مستقیماً خروجی NetworkResult<T> را به صورت بومی برگردانند.
class NetworkResultCall<T : Any>(private val proxyCall: Call<T>) : Call<NetworkResult<T>> {
override fun enqueue(callback: Callback<NetworkResult<T>>) {
proxyCall.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val networkResult = if (response.isSuccessful) {
val body = response.body()
if (body != null) NetworkResult.Success(body)
else NetworkResult.HttpError(response.code(), "پاسخ سرور خالی است.")
} else {
NetworkResult.HttpError(response.code(), response.errorBody()?.string() ?: "خطای سرور")
}
callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@NetworkResultCall, Response.success(NetworkResult.ConnectionError(t)))
}
})
}
override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxyCall.clone())
override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError()
override fun isExecuted(): Boolean = proxyCall.isExecuted
override fun cancel() = proxyCall.cancel()
override fun isCanceled(): Boolean = proxyCall.isCanceled
override fun request(): Request = proxyCall.request()
override fun timeout(): Timeout = proxyCall.timeout()
}
برای تزریق این کلاس واسط به سیستم تطبیق رتروفیت، یک CallAdapter.Factory اختصاصی ایجاد و در پیکربندی کلاینت ثبت میکنیم:
class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) return null
check(returnType is ParameterizedType) { "نوع بازگشتی باید پارامتری باشد" }
val responseType = getParameterUpperBound(0, returnType)
if (getRawType(responseType) != NetworkResult::class.java) return null
check(responseType is ParameterizedType) { "نوع پاسخ باید پارامتری باشد" }
val successType = getParameterUpperBound(0, responseType)
return object : CallAdapter<Any, Call<NetworkResult<Any>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<Any>): Call<NetworkResult<Any>> = NetworkResultCall(call)
}
}
}
با ثبت این فکتوری با متد addCallAdapterFactory(NetworkResultCallAdapterFactory())، متدهای اینترفیس به صورت مستقیم تعریف می شوند:
@GET("courses")
suspend fun getCourses(): NetworkResult<List<CourseDto>>
استراتژی کشینگ پیشرفته HTTP با کلاینت OkHttp و سناریوی آفلاین
استفاده هوشمندانه از کشینگ در لایه HTTP باعث می شود بدون نیاز به درگیر کردن دیتابیس محلی برای درخواست های ساده، پاسخ های تکراری سرور از دیسک محلی واکشی شوند. این قابلیت به خصوص برای فهرست های کندتغییر نظیر لیست شهرها، تعرفه ها یا مقالات استاتیک بی نظیر است.
برای استقرار این سیستم، یک کش با حجم ۵۰ مگابایت در پوشه کش اختصاصی اپلیکیشن ایجاد کرده و با دو اینترسپتور آنلاین و آفلاین هدرهای کنترل کش را تزریق می نماییم:
// ۱. ایجاد دایرکتوری و تخصیص حجم به کش OkHttp
val cacheSize = 50L * 1024L * 1024L // ۵۰ مگابایت کش دیسک
val httpCache = Cache(File(context.cacheDir, "http_cache_dir"), cacheSize)
// ۲. اینترسپتور حالت آنلاین: تعیین مدت زمان اعتبار داده های کش شده
val onlineCacheInterceptor = Interceptor { chain ->
val response = chain.proceed(chain.request())
val maxAge = 60 * 5 // نگه داری در کش به مدت ۵ دقیقه
response.newBuilder()
.header("Cache-Control", "public, max-age=$maxAge")
.removeHeader("Pragma")
.build()
}
// ۳. اینترسپتور حالت آفلاین: استفاده اجباری از کش در زمان قطعی اینترنت
val offlineCacheInterceptor = Interceptor { chain ->
var request = chain.request()
if (!isNetworkAvailable(context)) {
val maxStale = 60 * 60 * 24 * 7 // استفاده از کش های تا سقف ۷ روز قبل
request = request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
.removeHeader("Pragma")
.build()
}
chain.proceed(request)
}
val clientWithCache = OkHttpClient.Builder()
.cache(httpCache)
.addInterceptor(offlineCacheInterceptor)
.addNetworkInterceptor(onlineCacheInterceptor)
.build()
مدیریت آپلود فایل های حجیم و رهگیری پیشرفت با ProgressRequestBody
در سناریوهای آپلود فایل، ویدیو یا اسناد اداری، کاربران انتظار دارند نوار پیشرفت درصد بارگذاری (Upload Progress Bar) را به صورت لحظه ای مشاهده کنند. از آنجا که رتروفیت به صورت پیش فرض این قابلیت را در کلاس های ساده RequestBody قرار نداده است، با ساخت یک کلاس سفارشی که بایت های خروجی به BufferedSink را رهگیری می کند، این نیاز را برآورده می سازیم:
class ProgressRequestBody(
private val file: File,
private val contentType: MediaType?,
private val onProgressUpdate: (percentage: Int) -> Unit
) : RequestBody() {
override fun contentType(): MediaType? = contentType
override fun contentLength(): Long = file.length()
override fun writeTo(sink: BufferedSink) {
val fileLength = file.length()
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
val inStream = FileInputStream(file)
var uploadedBytes = 0L
inStream.use { input ->
var readBytes: Int
var lastReportedPercent = -1
while (input.read(buffer).also { readBytes = it } != -1) {
sink.write(buffer, 0, readBytes)
uploadedBytes += readBytes
val currentPercent = ((uploadedBytes * 100) / fileLength).toInt()
if (currentPercent != lastReportedPercent) {
lastReportedPercent = currentPercent
onProgressUpdate(currentPercent)
}
}
}
}
}
بهینه سازی عملکرد و کاهش تاخیر شبکه با پروتکل مدرن HTTP/۲
یکی از برگ های برنده کلاینت OkHttp در زیرساخت رتروفیت، پشتیبانی خودکار از مالتی پلکسینگ (Multiplexing) در پروتکل HTTP/۲ است. در پروتکل های سنتی HTTP/۱.۱، برای هر درخواست شبکه باید یک اتصال سوکت TCP مستقل با فرآیند ۳ مرحله ای دست دادن (۳-Way Handshake) برقرار می شد که باعث ایجاد سربار تاخیر (Latency) شدید و معضل مسدودسازی ابتدای خط (Head-of-Line Blocking) می شد.
در پروتکل HTTP/۲، تمامی درخواست های همزمان اپلیکیشن شما روی یک اتصال TCP یگانه به سرور فرستاده می شوند. هدرهای تکراری به وسیله الگوریتم فشرده سازی HPACK فشرده شده و تاخیر انتقال اطلاعات در شبکه های تلفن همراه تا بیش از ۴۵ درصد کاهش می یابد.
استراتژی تست های یکپارچگی شبکه با ابزار MockWebServer
یکی از شاخص ترین مزایای کتابخانه رتروفیت و OkHttp، امکان آزمودن کدهای لایه شبکه بدون نیاز به اتصال به سرور واقعی است. با استفاده از ابزار رسمی MockWebServer، یک وب سرور مجازی درون رم دستگاه اجرا می شود و رفتارهای گوناگون سرور را شبیه سازی می کند:
@RunWith(JUnit4::class)
class CourseApiServiceTest {
private lateinit var mockServer: MockWebServer
private lateinit var apiService: CourseApiService
@Before
fun startServer() {
mockServer = MockWebServer()
mockServer.start()
apiService = Retrofit.Builder()
.baseUrl(mockServer.url("/"))
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(CourseApiService::class.java)
}
@After
fun tearDown() = mockServer.shutdown()
@Test
fun getCourseDetails_returnsSuccess() = runTest {
val mockJson = """{"id": 1, "title": "آموزش کاتلین", "price": 150000.0}"""
mockServer.enqueue(MockResponse().setResponseCode(200).setBody(mockJson))
val result = apiService.getCourseDetails(1L)
assertEquals("آموزش کاتلین", result.title)
assertEquals(150000.0, result.price, 0.01)
}
}
الگوی تلاش مجدد خودکار با فاصله نمایی و جیتر (Exponential Backoff & Jitter)
در شرایط ناپایداری اینترنت یا بروز ترافیک های سنگین لحظه ای، ممکن است سرور با خطای ۵۰۳ یا تاخیر مواجه شود. اگر کلاینت های موبایل بلافاصله و با فواصل زمانی ثابت درخواست خود را تکرار کنند، پدیده ازدحام گله ای (Thundering Herd Problem) رخ می دهد و سرور کاملاً از کار می افتد.
راهکار مهندسی، پیاده سازی الگوی فاصله زمانی نمایی همراه با تصادفی سازی (Jitter) در یک اینترسپتور OkHttp است:
class RetryWithJitterInterceptor(
private val maxRetries: Int = 3,
private val baseDelayMs: Long = 1000L
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var attempt = 0
var response: Response? = null
while (attempt < maxRetries) {
try {
response?.close()
response = chain.proceed(chain.request())
if (response.isSuccessful || response.code in 400..499) return response
} catch (e: IOException) {
if (attempt == maxRetries - 1) throw e
}
attempt++
val delayMs = (baseDelayMs * (1 shl attempt)) + Random.nextLong(0, 500)
Thread.sleep(delayMs)
}
return response ?: throw IOException("تمام تلاش های مجدد با شکست مواجه شدند")
}
}
مانیتورینگ ترافیک شبکه در دستگاه با کتابخانه Chucker
در حین توسعه و تست اپلیکیشن های موبایل، دیباگ درخواست های رتروفیت با اتصال به Logcat محیط اندروید استودیو همیشه در دسترس نیست؛ به خصوص زمانی که تیم تست نرم افزار در حال بررسی اپلیکیشن روی دستگاه های واقعی است.
کتابخانه قدرتمند Chucker یک اینترسپتور بسیار سبک به کلاینت OkHttp اضافه می کند که با ارسال هر درخواست شبکه، یک اعلان نوتیفیکیشن در بالای صفحه گوشی ایجاد می نماید. با کلیک بر روی این اعلان، صفحه اختصاصی دیباگ باز شده و هدرها، بادی درخواست، زمان تاخیر میلی ثانیه ای و پاسخ های سرور به صورت تفکیک شده و با رنگ بندی زیبا به نمایش درمی آیند.
// اضافه کردن اینترسپتور Chucker فقط در بیلد دیباگ
val chuckerInterceptor = ChuckerInterceptor.Builder(context)
.collector(ChuckerCollector(context))
.maxContentLength(250_000L)
.redactHeaders("Authorization", "Bearer") // محافظت از لو رفتن توکن های امنیتی
.alwaysReadResponseBody(true)
.build()
val debugOkHttpClient = OkHttpClient.Builder()
.addInterceptor(chuckerInterceptor)
.build()
۷ اشتباه مهندسی رایج در پیاده سازی لایه شبکه با رتروفیت
در بررسی پروژه های اندرویدی، خطاهای تکراری متعددی در لایه ارتباطات شبکه ای دیده می شود که باید از آنها اجتناب کرد:
- ساخت مجدد اینستنس رتروفیت در هر درخواست: ساخت
Retrofitو OkHttpClient فرآیندی هزینه بر است. این اشیاء باید به صورت Singleton با Dagger Hilt ساخته شده و در کل طول عمر اپلیکیشن به اشتراک گذاشته شوند. - تنظیم نکردن تایم اوت های مناسب برای اتصالات ضعیف: تایم اوت های پیش فرض ممکن است باعث شوند کاربر در اینترنت های ناپایدار تا ۲ دقیقه منتظر بماند. تعیین تایم اوت های منطقی (۱۵ الی ۳۰ ثانیه) پایداری اپلیکیشن را تضمین می کند.
- عدم رسیدگی به استثناهای قطعی اینترنت (SocketTimeoutException و UnknownHostException): اگر این اکسپشن ها در بلوک try-catch شکار نشوند، اپلیکیشن فوراً کرش می کند.
- استفاده از کتابخانه های قدیمی Gson به جای ابزار های مدرن: کتابخانه Gson مبتنی بر رفلکشن (Reflection) کار می کند و در کاتلین ممکن است مقادیر غیر تهی (Non-nullable) را به صورت مخفیانه null مقداردهی کند. استفاده از Kotlinx Serialization یا Moshi اکیداً توصیه می شود.
- پراکندن آدرس های پایه (Base URLs) در بخش های مختلف پروژه: تمامی آدرس های پایه باید در فایل های کانفیگ سازمانی نظیر BuildConfig یا کلاس های تنظیمات متمرکز شوند.
- تلاش برای آپلود فایل های بزرگ با درخواست های حافظه محور: در آپلود فایل های سنگین نظیر ویدیو، باید از شیوه جریان داده (Streaming RequestBody) استفاده شود تا تمام فایل به یکباره وارد رم نشود و خطای OutOfMemoryError رخ ندهد.
- نادیده گرفتن پروتکل های کشینگ HTTP با هدرهای Cache-Control: بهره گیری از قابلیت کش بومی OkHttp می تواند حجم مصرف اینترنت و زمان بارگذاری اطلاعات تکراری را تا ۶۰ درصد کاهش دهد.
- پیکربندی نکردن استخر اتصالات (ConnectionPool) در شرایط بار کاری بالا: کلاینت OkHttp به صورت پیش فرض تا ۵ اتصال بیکار را به مدت ۵ دقیقه زنده نگه می دارد. در اپلیکیشن های بزرگ با ترافیک بالا، تنظیم پارامترهای ConnectionPool متناسب با معماری سرور، از هدررفت منابع سوکت و تاخیر Handshake های مکرر جلوگیری به عمل می آورد.
- غفلت از پین کردن گواهی امنیتی (Certificate Pinning) در تراکنش های مالی: عدم استفاده از کلاس CertificatePinner در سناریوهای پرداخت و اطلاعات حساس، ریسک حملات مرد میانی (Man-In-The-Middle) را به شدت افزایش می دهد.
۱۸. مقایسه تخصصی کانوورترها: Moshi در برابر Kotlinx Serialization در رتروفیت
انتخاب مبدل مناسب برای سریالایز و دیسریالایز کردن کدهای JSON در لایه شبکه رتروفیت تاثیر بسزایی بر عملکرد زمان اجرا، حجم باینری APK و امنیت حافظه برنامه دارد. دو گزینه استاندارد و مدرن اکوسیستم کاتلین عبارتند از Moshi و Kotlinx Serialization.
| معیار مقایسه | Kotlinx Serialization | Moshi (با کدهای تولیدی) |
|---|---|---|
| نیاز به بازتاب (Reflection) | صفر (کاملاً مبتنی بر پلاگین کامپایلر) | بدون رفلکشن در حالت کدهای جنریتشده (KSP) |
| سازگاری با KMP (Multiplatform) | پشتیبانی ۱۰۰٪ نیتیو از اندروید، iOS و دسکتاپ | محدود به JVM و پلتفرم اندروید |
| سرعت دیسریالایز JSON | بسیار سریع (تا ۳۰ درصد بهینهتر در حجم داده بالا) | بسیار بهینه و پایدار با JsonReader |
| پشتیبانی از مقادیر پیشفرض | پشتیبانی کامل نیتیو از مقادیر پیشفرض کاتلین | نیازمند تنظیمات خاص در صورت عدم استفاده از KSP |
برای پیاده سازی Kotlinx Serialization در رتروفیت، کافی است کتابخانه retrofit2-kotlinx-serialization-converter را به وابستگی های گریدل افزوده و کلاینت را به شکل زیر پیکربندی نمایید:
val jsonConfig = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
}
val retrofit = Retrofit.Builder()
.baseUrl("https://api.vahidhoseini.ir/v1/")
.client(okHttpClient)
.addConverterFactory(jsonConfig.asConverterFactory("application/json".toMediaType()))
.build()
۱۹. معماری تمیز و یکپارچه سازی رتروفیت با کاتلین Flow و کش محلی Room
در معماری نرم افزارهای مدرن اندروید، لایه شبکه هرگز نباید داده های خام را مستقیماً به لایه Presentation ارائه دهد. بهترین الگوی پروداکشن، پیاده سازی مخزن آفلاین محور (Offline-First Repository) است که با استفاده از کاتلین Flow وضعیت های سهگانه Loading، Success و Error را منتشر میکند.
در این الگو، داده های واکشیشده از رتروفیت بلافاصله در پایگاه داده محلی Room ذخیره میشوند و رابط کاربری (UI) صرفاً به منبع واحد حقیقت (Single Source of Truth) در دیتابیس گوش فرا میدهد:
class CourseRepositoryImpl @Inject constructor(
private val api: CourseApiService,
private val courseDao: CourseDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : CourseRepository {
override fun getCoursesStream(): Flow<Resource<List<Course>>> = flow {
emit(Resource.Loading)
// ۱. ابتدا داده های کش موجود در دیتابیس را نمایش می دهیم
val cached = courseDao.getAllCourses().first()
if (cached.isNotEmpty()) emit(Resource.Success(cached))
// ۲. ارسال درخواست به سرور در پس زمینه و به روزرسانی کش
try {
val networkCourses = api.getCourses()
courseDao.insertCourses(networkCourses.map { it.toEntity() })
val updated = courseDao.getAllCourses().first()
emit(Resource.Success(updated))
} catch (e: Exception) {
emit(Resource.Error("عدم دسترسی به سرور: " + (e.localizedMessage ?: "خطای ناشناخته")))
}
}.flowOn(ioDispatcher)
}
پرسش های متداول در خصوص کتابخانه رتروفیت در اندروید
آیا رتروفیت قابلیت ارتباط با وب سوکت (WebSocket) را دارد؟
خیر؛ رتروفیت منحصراً برای پروتکل های درخواست-پاسخ HTTP طراحی شده است. برای برقراری ارتباطات بلادرنگ مبتنی بر وب سوکت، از متد newWebSocket در کلاینت OkHttp استفاده می شود که به صورت ذاتی از اتصالات پایدار دوطرفه پشتیبانی می کند.
چرا استفاده از Kotlinx Serialization بر کتابخانه قدیمی Gson برتری دارد؟
کتابخانه Kotlinx Serialization در زمان کامپایل کدهای پارس کردن را تولید می کند، از سیستم ایمنی تهی بودن (Null-Safety) کاتلین به طور کامل پاسداری می نماید و بر خلاف Gson نیازی به پردازش های سنگین رفلکشن در زمان اجرا ندارد.
چگونه می توان هدرهای متغیر را به درخواست های رتروفیت اضافه کرد؟
با استفاده از انوتیشن Header@ در آرگومان های متد سرویس، می توانید هدرهای داینامیک مانند شناسه دستگاه یا زبان برنامه را برای هر فراخوانی به صورت مستقل ارسال فرمایید.
تفاوت متدهای Response و توابع مستقیم در رتروفیت چیست؟
اگر نوع خروجی متد را به صورت Response<T> تعریف کنید، به هدرهای پاسخ سرور، کدهای وضعیت HTTP و بادی خطاها دسترسی خواهید داشت؛ اما اگر مستقیماً T را برگردانید، رتروفیت در صورت دریافت کدهای خطای غیراز ۲۰۰ تا ۲۹۹، یک HttpException پرتاب خواهد کرد.
چگونه می توان پیشرفت آپلود و دانلود فایل را در رتروفیت ردیابی کرد؟
با ساخت یک کلاس سفارشی که از RequestBody یا ResponseBody ارث بری کرده و فرآیند انتقال بایت ها را در جریان متد writeTo به یک کالبک یا Flow گزارش می دهد، می توان نوار پیشرفت (Progress Bar) را در رابط کاربری به صورت نرم به نمایش گذاشت.
آیا رتروفیت از پروتکل مدرن HTTP/۳ و QUIC پشتیبانی می کند؟
بله؛ از آنجا که رتروفیت بر روی کلاینت OkHttp اجرا می شود، با فعال سازی ماژول okhttp-tls و اتصال به سرور هاییی که از پروتکل QUIC پشتیبانی می کنند، تبادل داده ها با سرعت خیره کننده پروتکل HTTP/۳ انجام می پذیرد.
چگونه خطای Cleartext HTTP traffic not permitted را برطرف کنیم؟
سیستم عامل اندروید از نسخه ۹ به بعد ارتباطات ناامن HTTP بدون رمزنگاری را مسدود کرده است. استانداردترین راه، ارتقای وب سرویس به پروتکل امن HTTPS است. در موارد اضطراری می توان با تنظیم فایل network_security_config.xml در منیفست، دامنه های مشخصی را مستثنی کرد.
بهترین روش برای تزریق سرویس های رتروفیت با Dagger Hilt چیست؟
با ایجاد یک NetworkModule و نوشتن متدهای Provides@ با حوزه Singleton@ برای ایجاد اینستنس OkHttpClient، Retrofit و متد create روی اینترفیس سرویس، می توان سرویس شبکه را به سادگی در کلاس های لایه دیتا تزریق نمود.
امتیاز و نظرات
هنوز نظری ثبت نشده است. اولین نفر باشید.