بيانات أندرويد والشبكات والواجهات

استهلاك REST API

18 دقيقة الدرس 8 من 12

استهلاك REST API

في الدرسين السابقين أعددت HttpURLConnection وهيّأت Retrofit. الآن ستجمع كل الأجزاء معًا من البداية إلى النهاية: أرسل طلب شبكة حقيقيًا، وحلّل استجابة JSON، واعرض البيانات داخل RecyclerView — مع إدارة الخيوط بشكل صحيح حتى لا تتجمّد واجهة المستخدم. هذا النمط هو ما يُشغّل تقريبًا كل تطبيق Android في الإنتاج يتصل بخادم خلفي.

الهدف: قائمة بمستودعات GitHub

سنستدعي نقطة النهاية العامة في GitHub API: GET https://api.github.com/users/{username}/repos، التي تعيد مصفوفة JSON من كائنات المستودعات. كل كائن يحتوي (من بين حقول أخرى) على name وdescription وstargazers_count. لا يُشترط المصادقة للبيانات العامة، مما يجعلها هدفًا مثاليًا للتعلم.

الخطوة 1 — إضافة فئة النموذج

أنشئ فئة Java عادية تعكس حقول JSON التي تهتم بها. سيملأها Retrofit (عبر Gson) تلقائيًا.

// GitHubRepo.java public class GitHubRepo { private String name; private String description; private int stargazers_count; public String getName() { return name; } public String getDescription() { return description; } public int getStars() { return stargazers_count; } }
يجب أن تتطابق أسماء الحقول مع مفاتيح JSON تمامًا عند استخدام التعيين الافتراضي لـ Gson. إن أردت اسم Java مختلفًا، استخدم التعليق التوضيحي @SerializedName("stargazers_count"). هنا الحقل مُسمَّى بشكل متطابق فلا حاجة إلى تعليق توضيحي.

الخطوة 2 — تعريف واجهة Retrofit

طريقة واحدة لكل نقطة نهاية. يستبدل تعليق @Path العنصر النائب {username} في عنوان URL وقت التشغيل.

import java.util.List; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; public interface GitHubService { @GET("users/{username}/repos?per_page=30&sort=stars") Call<List<GitHubRepo>> getRepos(@Path("username") String username); }

الخطوة 3 — بناء Singleton لـ Retrofit

أنشئ نسخة واحدة تُعيد استخدامها في أنحاء التطبيق. عادةً تكون في فئة ApiClient أو مزوّد حقن تبعيات.

import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ApiClient { private static final String BASE_URL = "https://api.github.com/"; private static Retrofit instance; public static Retrofit getInstance() { if (instance == null) { instance = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } return instance; } }

الخطوة 4 — ربط RecyclerView

أنشئ محوّلًا بسيطًا يربط قائمة كائنات GitHubRepo بعناصر العرض.

import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.ViewHolder> { private List<GitHubRepo> repos = new ArrayList<>(); public void setRepos(List<GitHubRepo> repos) { this.repos = repos; notifyDataSetChanged(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_2, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { GitHubRepo repo = repos.get(position); holder.name.setText(repo.getName()); String desc = repo.getDescription() != null ? repo.getDescription() : "No description"; holder.detail.setText(desc + " ★ " + repo.getStars()); } @Override public int getItemCount() { return repos.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView name, detail; ViewHolder(View v) { super(v); name = v.findViewById(android.R.id.text1); detail = v.findViewById(android.R.id.text2); } } }

الخطوة 5 — إطلاق الطلب من النشاط

هنا تلتقي الأجزاء معًا. تُرسل Call.enqueue() طلب HTTP على خيط خلفي تديره Retrofit، ثم تُسلّم النتيجة على الخيط الرئيسي — لا حاجة إلى AsyncTask أو Handler يدوي.

import android.os.Bundle; import android.util.Log; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class RepoListActivity extends AppCompatActivity { private static final String TAG = "RepoListActivity"; private RepoAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_repo_list); RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new RepoAdapter(); recyclerView.setAdapter(adapter); fetchRepos("torvalds"); // استبدل بأي اسم مستخدم على GitHub } private void fetchRepos(String username) { GitHubService service = ApiClient.getInstance() .create(GitHubService.class); Call<List<GitHubRepo>> call = service.getRepos(username); call.enqueue(new Callback<List<GitHubRepo>>() { @Override public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) { if (response.isSuccessful() && response.body() != null) { adapter.setRepos(response.body()); } else { Log.e(TAG, "HTTP " + response.code()); Toast.makeText(RepoListActivity.this, "Error: " + response.code(), Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call<List<GitHubRepo>> call, Throwable t) { Log.e(TAG, "Network failure", t); Toast.makeText(RepoListActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } }
تحقق دائمًا من response.isSuccessful() قبل الوصول إلى الجسم. استجابة 404 أو 403 هي معاملة HTTP ناجحة تقنيًا — تستدعي Retrofit الدالة onResponse لأي طلب مكتمل. تُشغَّل onFailure فقط عند رمي استثناء (لا اتصال، انتهاء مهلة، خطأ SSL). تجاهل رمز الحالة يعني ابتلاع أخطاء API بصمت.

فهم عقد خيط الاستدعاء الراجع

يعمل عميل OkHttp الخاص بـ Retrofit على تجمّع خيوط خاص به. عند وصول الاستجابة، ترسل Retrofit الاستدعاء الراجع إلى الخيط الرئيسي (خيط UI) تلقائيًا — يمكنك تحديث Views مباشرةً داخل onResponse وonFailure دون أي تبديل إضافي للخيوط. لهذا السبب يعمل استدعاء المحوّل في المثال أعلاه دون حاجة إلى runOnUiThread().

إلغاء الطلبات الجارية

إذا انتقل المستخدم بعيدًا قبل وصول الاستجابة، يجب ألا يُحدّث الاستدعاء الراجع نشاطًا مُدمَّرًا. احتفظ بمرجع لكائن Call وألغِه في onDestroy.

private Call<List<GitHubRepo>> currentCall; private void fetchRepos(String username) { GitHubService service = ApiClient.getInstance().create(GitHubService.class); currentCall = service.getRepos(username); currentCall.enqueue(/* ... نفس الاستدعاء الراجع ... */); } @Override protected void onDestroy() { super.onDestroy(); if (currentCall != null) { currentCall.cancel(); } }
تسريب الذاكرة والأعطال. يحمل Callback المجهول مرجعًا ضمنيًا للنشاط المُحيط. إذا دُمِّر النشاط (دوران، ضغط رجوع) أثناء جريان الطلب، سيُطلَق الاستدعاء الراجع على كائن ميت. يمنع الإلغاء في onDestroy هذا. في التطبيق الإنتاجي تنقل الطلب إلى ViewModel ليبقى حيًا أثناء الدوران — لكن الإلغاء يبقى شبكة الأمان الأساسية.

إذن الإشعار المطلوب في الملف التعريفي

بدون هذا السطر في AndroidManifest.xml تفشل كل طلبات الشبكة بصمت مع SecurityException أو تعيد استجابة فارغة:

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

نظرًا لأن INTERNET إذن عادي (غير خطير)، لا يُطلَب من المستخدم الموافقة — تُصرّح به فحسب ويمنحه نظام التشغيل عند التثبيت.

الخلاصة

يتلخّص استهلاك REST API في Android في خمس خطوات قابلة للتكرار: نمذجة JSON كـ POJO، وصف نقطة النهاية في واجهة Retrofit، بناء العميل مرة واحدة، تهيئة محوّل RecyclerView، واستدعاء enqueue() مع Callback. تتولى Retrofit إدارة الخيوط وإلغاء التسلسل؛ مهمتك التحقق من رمز الحالة وتحديث UI عند النجاح وإلغاء الطلب حين اختفاء الشاشة. يتوسّع هذا النمط من نقطة نهاية واحدة إلى تطبيق كامل متعدد الخدمات دون تغيير البنية الأساسية.