الشبكات مع Retrofit
في الدرس السابق أجريتَ طلبات HTTP بفتح HttpURLConnection يدويًا، وقراءة InputStream، وبناء الاستجابة بنفسك. هذا الأسلوب يعمل، لكنه ينتج كمّية كبيرة من الكود التمهيدي يسهل فيها الوقوع في الأخطاء. مكتبة Retrofit من Square تحوّل REST API إلى واجهة Java. تصف شكل كل نقطة نهاية (endpoint)، وتتولّى Retrofit بقية العمل — إدارة الاتصال، وتسلسل الطلبات، وتحليل الاستجابات، وإدارة الخيوط، ومعالجة الأخطاء.
Retrofit هو المعيار الفعلي لاستدعاءات REST في Android. كل قاعدة كود Android احترافية ستصادفها تستخدمه أو شيئًا مبنيًا فوقه. إتقانه العميق يؤتي ثماره على الفور.
إضافة Retrofit إلى مشروعك
افتح ملف build.gradle الخاص بالوحدة وأضف هذه التبعيات:
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}
retrofit هي المكتبة الأساسية. converter-gson تدمج Gson لتتمكّن Retrofit من تحويل JSON تلقائيًا إلى فئات نماذج Java. logging-interceptor ليست إلزامية لكنها لا تُقدَّر بثمن أثناء التطوير — فهي تسجّل كل طلب واستجابة في Logcat.
تحتاج أيضًا إلى إذن الإنترنت في AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
الخطوة 1 — تعريف فئات النموذج
تُعيّن Retrofit مع Gson حقول JSON على حقول Java بالاسم. عرّف فئة Java عادية (POJO) تتطابق حقولها مع مفاتيح JSON المتوقعة. مثلًا إذا كانت الـ API ترجع كائنات مستخدمين:
// models/User.java
public class User {
private int id;
private String name;
private String email;
// تستخدم Gson الانعكاس؛ الـ getters اختيارية لكن ممارسة جيدة
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
تسمية الحقول: إذا كان مفتاح JSON يستخدم snake_case (مثل first_name) لكنك تريد حقل Java بـ camelCase (مثل firstName)، استخدم التوضيح @SerializedName("first_name"). أو يمكنك ضبط Gson باستخدام setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) عند بناء GsonConverterFactory.
الخطوة 2 — تعريف واجهة API
أنشئ واجهة Java. كل دالة تمثّل نقطة نهاية واحدة. توضيحات Retrofit تصف دالة HTTP ومسار URL وبارامترات الاستعلام وجسم الطلب والترويسات.
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface UserApiService {
// GET /users — يعيد قائمة مستخدمين
@GET("users")
Call<List<User>> getUsers();
// GET /users/{id} — بارامتر مسار
@GET("users/{id}")
Call<User> getUser(@Path("id") int userId);
// GET /users?page=2 — بارامتر استعلام
@GET("users")
Call<List<User>> getUsersPage(@Query("page") int page);
// POST /users — جسم الطلب مسلسَل إلى JSON
@POST("users")
Call<User> createUser(@Body User newUser);
// PUT /users/{id}
@PUT("users/{id}")
Call<User> updateUser(@Path("id") int userId, @Body User updated);
// DELETE /users/{id}
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") int userId);
}
نوع الإرجاع دائمًا Call<T> حيث T هو النوع الذي يجب أن تُفكّك Retrofit ترميز جسم الاستجابة إليه. استخدم Call<Void> عندما لا تحتوي الاستجابة على جسم (مثل 204 No Content).
الخطوة 3 — بناء كائن Retrofit
كائن Retrofit مكلف الإنشاء. ابنه مرة واحدة وأعِد استخدامه عبر نمط singleton — عادةً في فئة ApiClient مخصصة:
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ApiClient {
private static final String BASE_URL = "https://jsonplaceholder.typicode.com/";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
public static UserApiService getUserService() {
return getClient().create(UserApiService.class);
}
}
يجب أن ينتهي Base URL بشرطة مائلة. إذا كتبتَ "https://api.example.com/v1" (بدون شرطة) وفي واجهتك @GET("users")، تحلّ Retrofit عنوان URL بشكل خاطئ. احرص دائمًا على إنهاء base URL بـ /.
الخطوة 4 — إجراء استدعاءات غير متزامنة
يمكن تنفيذ كائن Call بشكل متزامن (call.execute()، لكن هذا يُعيق الخيط ويجب ألا يعمل أبدًا على الخيط الرئيسي) أو بشكل غير متزامن (call.enqueue()، الذي يُعيد النتيجة على الخيط الرئيسي). استخدم دائمًا enqueue من Activity أو Fragment:
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "RetrofitDemo";
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
loadUsers();
}
private void loadUsers() {
Call<List<User>> call = ApiClient.getUserService().getUsers();
call.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call,
Response<List<User>> response) {
if (response.isSuccessful() && response.body() != null) {
List<User> users = response.body();
StringBuilder sb = new StringBuilder();
for (User u : users) {
sb.append(u.getName()).append("\n");
}
textView.setText(sb.toString());
} else {
// خطأ HTTP (4xx / 5xx)
Log.e(TAG, "خطأ HTTP: " + response.code());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
// فشل شبكي أو انتهاء مهلة أو استجابة مشوهة
Log.e(TAG, "فشل الشبكة: " + t.getMessage());
}
});
}
}
يتوفر نوعان من callbacks. يُطلَق onResponse عند أي استجابة HTTP من الخادم — بما فيها 404 أو 500. تحقق دائمًا من response.isSuccessful() (الأكواد 200–299) قبل الوثوق بـ response.body(). يُطلَق onFailure فقط عند الأعطال على مستوى الشبكة: انعدام الاتصال، فشل DNS، انتهاء مهلة الاتصال، أو خطأ في تحليل JSON.
إرسال البيانات مع @Body
لإنشاء مورد، أضِف توضيح @Body إلى البارامتر. تُسلسل Retrofit كائن Java إلى JSON تلقائيًا:
private void createUser() {
User newUser = new User();
// تقرأ Gson قيم الحقول مباشرةً عبر الانعكاس
// لذا لا حاجة لـ setters منفصلة
Call<User> call = ApiClient.getUserService().createUser(newUser);
call.enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
if (response.isSuccessful()) {
User created = response.body();
Log.d(TAG, "تم إنشاء مستخدم بمعرّف: " + created.getId());
}
}
@Override
public void onFailure(Call<User> call, Throwable t) {
Log.e(TAG, "فشل: " + t.getMessage());
}
});
}
إضافة ترويسات المصادقة
تتطلب معظم الـ APIs الحقيقية ترويسة تفويض. أنظف أسلوب هو استخدام interceptor من OkHttp يُرفق الترويسة بكل طلب تلقائيًا:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
okhttp3.Request original = chain.request();
okhttp3.Request authenticated = original.newBuilder()
.header("Authorization", "Bearer " + getAuthToken())
.build();
return chain.proceed(authenticated);
})
.build();
هكذا لا يحتاج أي استدعاء API فردي إلى معرفة الرمز المميز — فصل نظيف للاهتمامات.
لا تترك الاستدعاءات معلّقة بعد تدمير الـ Activity. إذا أُطلق onResponse أو onFailure بعد اختفاء الـ Activity، ستحاول تحديث views مدمّرة وتتعطل بـ NullPointerException أو IllegalStateException. ألغِ أي استدعاءات قيد التشغيل في onDestroy باحتفاظك بمرجع: call.cancel(). عمليًا، تحلّ ViewModels مع LiveData هذه المشكلة بتحديد نطاق استدعاءات الشبكة لدورة حياة ViewModel.
الخلاصة
تختزل Retrofit شبكات REST في أربع خطوات: إضافة التبعية، وتعريف نماذج POJO، والإعلان عن واجهة بالتوضيحات، وبناء كائن Retrofit singleton. تُجرى الاستدعاءات بـ enqueue الذي يُسلّم النتائج على الخيط الرئيسي عبر onResponse وonFailure. في الدرس القادم ستتعلم كيفية تحليل هياكل JSON متداخلة أكثر تعقيدًا يدويًا ومع Gson.