تحليل بيانات JSON
لديك الآن نص استجابة HTTP — سلسلة نصية بتنسيق JSON. تحتاج إلى تحويلها إلى كائنات Java. إجراء ذلك يدويًا باستخدام JSONObject مرهق ومعرّض للأخطاء؛ فكل وصول إلى حقل يستلزم استدعاء getString() واحتمال رمي JSONException. تستخدم تطبيقات Android الاحترافية مكتبة ربط تُعيّن مفاتيح JSON إلى حقول Java تلقائيًا. الخياران السائدان هما Gson (من Google) وMoshi (من Square). يتناول هذا الدرس كليهما لتتمكن من اتخاذ قرار مستنير وقراءة أي قاعدة كود تصادفها.
لماذا نستخدم مكتبة ربط؟
يقترن التحليل اليدوي بهيكل JSON مباشرةً. إذا تغيّر اسم مفتاح في الـ API، يجب عليك تتبّع كل استدعاء getString("old_key") وتعديله. تمركز مكتبة الربط هذا التعيين في فئة نموذج واحدة. تستخدم المكتبة الانعكاس (Gson) أو توليد الكود (Moshi) لملء كائناتك — تكتب فئة وتُضيف التوصيفات (annotations)، وتترك الباقي للمكتبة.
التسلسل مقابل إلغاء التسلسل: إلغاء التسلسل هو تحويل JSON إلى كائن Java (ما تفعله في الغالب مع استجابات الـ API). التسلسل هو تحويل كائن Java إلى سلسلة JSON (ما تفعله عند إرسال جسم الطلب). تتعامل كلتا المكتبتين مع الاتجاهين.
إضافة التبعيات
في ملف build.gradle الخاص بالوحدة:
dependencies {
// Gson — يعتمد على الانعكاس، لا يحتاج معالج تعليقات
implementation 'com.google.code.gson:gson:2.10.1'
// Moshi — يعتمد على توليد الكود (أسرع وأكثر أمانًا من حيث القيم الفارغة)
implementation 'com.squareup.moshi:moshi:1.15.1'
implementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
// للمشاريع Java الصرفة استخدم annotationProcessor بدلًا من kapt
}
مشاريع Android بلغة Java الصرفة: يعمل توليد كود Moshi عبر معالجة التعليقات. إذا لم تكن تستخدم Kotlin، استبدل kapt بـ annotationProcessor أو استخدم Gson فقط — فهو لا يحتاج أي معالج تعليقات على الإطلاق.
تمثيل بنية JSON في نموذج Java
لنفترض أن الـ REST API يُعيد ملف مستخدم:
{
"id": 42,
"full_name": "Layla Hassan",
"email": "layla@example.com",
"is_active": true,
"score": 98.5
}
أنشئ فئة Java بسيطة (POJO) تعكس هذا الهيكل:
// User.java
public class User {
public int id;
public String fullName; // سيُعيَّن عبر @SerializedName / @Json
public String email;
public boolean isActive;
public double score;
}
التحليل باستخدام Gson
يُعيّن Gson مفاتيح JSON تلقائيًا إلى أسماء حقول Java عند تطابقها. لمفاتيح snake_case المختلفة عن أسماء حقول camelCase، استخدم @SerializedName:
import com.google.gson.annotations.SerializedName;
public class User {
public int id;
@SerializedName("full_name")
public String fullName;
public String email;
@SerializedName("is_active")
public boolean isActive;
public double score;
}
لإلغاء تسلسل كائن واحد من سلسلة JSON:
import com.google.gson.Gson;
String json = "{ \"id\": 42, \"full_name\": \"Layla Hassan\", "
+ "\"email\": \"layla@example.com\", \"is_active\": true, \"score\": 98.5 }";
Gson gson = new Gson();
User user = gson.fromJson(json, User.class);
System.out.println(user.fullName); // Layla Hassan
System.out.println(user.isActive); // true
لمصفوفة JSON في المستوى الأعلى، استخدم TypeToken لحمل معلومات النوع العام التي يمحوها Java في وقت التشغيل:
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
String jsonArray = "[ {\"id\":1, ...}, {\"id\":2, ...} ]";
Type listType = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(jsonArray, listType);
System.out.println(users.size()); // 2
والتسلسل (تحويل الكائن إلى JSON) بسهولة تامة:
String output = gson.toJson(user);
// {"id":42,"fullName":"Layla Hassan","email":"layla@example.com","isActive":true,"score":98.5}
Gson يتجاهل الحقول المفقودة بصمت. إذا لم يحتوِ الـ JSON على مفتاح معين، يبقى الحقل المقابل في Java عند قيمته الافتراضية (0 أو false أو null). قد يُخفي ذلك تغييرات في عقد الـ API. تحقق دائمًا من الحقول الحرجة القابلة للقيمة null قبل الاستخدام.
التحليل باستخدام Moshi
واجهة برمجة Moshi مشابهة لكنها تستخدم @Json لتعيين الأسماء وتحتاج إلى محوّل (adapter) لكل نوع:
import com.squareup.moshi.Json;
public class User {
public int id;
@Json(name = "full_name")
public String fullName;
public String email;
@Json(name = "is_active")
public boolean isActive;
public double score;
}
import com.squareup.moshi.Moshi;
import com.squareup.moshi.JsonAdapter;
import java.io.IOException;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<User> adapter = moshi.adapter(User.class);
User user = adapter.fromJson(json);
System.out.println(user.fullName); // Layla Hassan
// التسلسل
String output = adapter.toJson(user);
لقائمة من الكائنات:
import com.squareup.moshi.Types;
import java.lang.reflect.ParameterizedType;
import java.util.List;
ParameterizedType type = Types.newParameterizedType(List.class, User.class);
JsonAdapter<List<User>> listAdapter = moshi.adapter(type);
List<User> users = listAdapter.fromJson(jsonArray);
Moshi يرمي استثناءً عند انتهاكات القيمة null. إذا استقبل حقل غير قابل للقيمة null في المحوّل قيمة null من الـ JSON، يرمي Moshi استثناء JsonDataException. هذا السلوك الصارم يكشف أخطاء الـ API مبكرًا — وهو سبب تفضيل المطورين لـ Moshi في المشاريع الجديدة.
الكائنات المتداخلة والمصفوفات
تعتمد الـ APIs الحقيقية على الكائنات المتداخلة. يتعامل Gson وMoshi مع التداخل تلقائيًا طالما أن نموذج Java يعكس هيكل JSON:
// JSON
// { "id": 1, "author": { "id": 10, "full_name": "Sara Ali" }, "tags": ["android","java"] }
public class Post {
public int id;
public User author; // كائن متداخل
public List<String> tags; // مصفوفة من القيم البدائية
}
// التحليل متطابق — Gson وMoshi يتكرران تلقائيًا
Post post = gson.fromJson(json, Post.class);
System.out.println(post.author.fullName); // Sara Ali
System.out.println(post.tags.get(0)); // android
أين يتم التحليل في تطبيق Android؟
تحليل JSON عمل يعتمد على المعالج — ويجب أن لا يعمل أبدًا على الخيط الرئيسي (UI thread). في الدرس الرابع تعلّمت عن خيوط العمل في الخلفية. عند استخدام HttpURLConnection (الدرس الخامس) أو Retrofit (الدرس السادس)، ينتمي استدعاء التحليل إلى داخل AsyncTask أو Callable أو رد النداء enqueue — وكلها تعمل بالفعل خارج خيط الـ UI. إذا استخدمت Retrofit مع GsonConverterFactory، يحدث التحليل تلقائيًا داخل مجموعة خيوط OkHttp التابعة لـ Retrofit وتستقبل كائن Java جاهزًا في رد النداء.
// تكامل Retrofit + Gson (build.gradle)
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// RetrofitClient.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// واجهة الـ API
public interface UserApi {
@GET("users/{id}")
Call<User> getUser(@Path("id") int id);
}
// الاستخدام — يُلغي Retrofit تسلسل User تلقائيًا
UserApi api = retrofit.create(UserApi.class);
api.getUser(42).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
if (response.isSuccessful()) {
User user = response.body(); // جاهز للاستخدام بعد التحليل
runOnUiThread(() -> textView.setText(user.fullName));
}
}
@Override
public void onFailure(Call<User> call, Throwable t) {
Log.e("API", "فشل الطلب", t);
}
});
Gson مقابل Moshi — أيهما تختار؟
- Gson: إعداد أبسط، لا يحتاج معالج تعليقات، موثّق على نطاق واسع، يعمل بالانعكاس حتى بدون تعليقات. خيار افتراضي جيد للمشاريع البسيطة.
- Moshi: أسرع (محوّل مُولَّد بالكود)، آمن من القيمة null افتراضيًا، وقت تشغيل أخف، يتكامل جيدًا مع Kotlin. مفضّل للمشاريع الجديدة وقواعد الكود الكبيرة حيث الصحة أهم من البساطة.
- كلاهما: يمتلك مصانع محوّل Retrofit من الدرجة الأولى، ويتعامل مع الكائنات المتداخلة والقوائم، ويدعم محوّلات أنواع مخصصة للأنواع الخاصة كالتواريخ.
الخلاصة
تلغي مكتبات الربط تحليل JSON المكتوب يدويًا. أنشئ POJO يعكس هيكل JSON الخاص بالـ API، ضع تعليقات على أسماء الحقول غير المتطابقة باستخدام @SerializedName (Gson) أو @Json (Moshi)، ثم دع المكتبة تتولى إلغاء التسلسل والتسلسل. أبقِ التحليل بعيدًا عن خيط الـ UI — وعمليًا، دع مصنع محوّل Retrofit يتولى ذلك حتى يستقبل رد نداؤك كائنات Java نظيفة وجاهزة للاستخدام. في الدرس القادم ستجمع كل ما تعلمته لاستهلاك REST API كامل من البداية إلى النهاية.