واجهة أندرويد والأنشطة والتنقّل

مشروع: تطبيق أندرويد بقائمة وتفاصيل

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

مشروع: تطبيق أندرويد بقائمة وتفاصيل

نمط القائمة-التفاصيل هو أحد أكثر الأنماط شيوعًا في تطوير تطبيقات الهاتف المحمول. تجلس قائمة قابلة للتمرير على شاشة واحدة؛ والنقر على أي صف يؤدي إلى الانتقال إلى شاشة ثانية تعرض التفاصيل الكاملة لذلك العنصر. لقد رأيت هذا النمط في عملاء البريد الإلكتروني وقرّاء الأخبار والكتالوجات التجارية وكل تطبيق CRUD تقريبًا. في هذا الدرس الختامي ستقوم بربط كل ما تعلمته في البرنامج التعليمي — التخطيطات، وRecyclerView، والمحوّلات، والتنقل القائم على Intent، ونقل البيانات — في تطبيق واحد متماسك وذي شكل احترافي.

ما الذي نبنيه

تطبيق بسيط لـكتالوج الكتب بشاشتين:

  • BookListActivity — تعرض RecyclerView من بطاقات الكتب (العنوان + المؤلف). كل بطاقة قابلة للنقر.
  • BookDetailActivity — تستقبل الكتاب المحدد عبر إضافة Intent وتعرض تفاصيله الكاملة (العنوان، المؤلف، السنة، الملخص).

طبقة البيانات عبارة عن قائمة Java عادية — لا قاعدة بيانات بعد — حتى تتمكن من التركيز تمامًا على واجهة المستخدم وربط التنقل.

الخطوة الأولى — نموذج البيانات

أنشئ فئة Java عادية. اجعلها Serializable حتى يمكن تضمين الكائن كاملًا في إضافة Intent دون استخراج يدوي للحقول.

// Book.java package com.example.booklist.model; import java.io.Serializable; public class Book implements Serializable { private final String title; private final String author; private final int year; private final String synopsis; public Book(String title, String author, int year, String synopsis) { this.title = title; this.author = author; this.year = year; this.synopsis = synopsis; } public String getTitle() { return title; } public String getAuthor() { return author; } public int getYear() { return year; } public String getSynopsis() { return synopsis; } }
Serializable مقابل Parcelable: تُعدّ Serializable أبطأ من Parcelable الخاصة بأندرويد، لكنها لا تتطلب أي كود إضافي للفئات الصغيرة. في تطبيق حقيقي بقوائم كبيرة تُمرَّر بين الأنشطة، نفّذ Parcelable. لهذا المشروع، Serializable مناسبة تمامًا.

الخطوة الثانية — تخطيط عنصر القائمة

كل صف في RecyclerView يحتاج إلى ملف تخطيط خاص به. أنشئ res/layout/item_book.xml:

<!-- res/layout/item_book.xml --> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginVertical="6dp" app:cardCornerRadius="8dp" app:cardElevation="3dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/tvTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp" android:textStyle="bold" android:textColor="@color/black" /> <TextView android:id="@+id/tvAuthor" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textSize="14sp" android:textColor="#666666" /> </LinearLayout> </androidx.cardview.widget.CardView>

الخطوة الثالثة — محوّل RecyclerView

يعمل المحوّل كجسر بين List<Book> وRecyclerView. يُحقن استدعاء النقر عبر المُنشئ حتى يبقى المحوّل قابلًا لإعادة الاستخدام ويحتفظ النشاط بالتحكم في التنقل.

// BookAdapter.java package com.example.booklist.adapter; 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 com.example.booklist.R; import com.example.booklist.model.Book; import java.util.List; public class BookAdapter extends RecyclerView.Adapter<BookAdapter.BookViewHolder> { public interface OnBookClickListener { void onBookClick(Book book); } private final List<Book> books; private final OnBookClickListener listener; public BookAdapter(List<Book> books, OnBookClickListener listener) { this.books = books; this.listener = listener; } @NonNull @Override public BookViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_book, parent, false); return new BookViewHolder(view); } @Override public void onBindViewHolder(@NonNull BookViewHolder holder, int position) { Book book = books.get(position); holder.tvTitle.setText(book.getTitle()); holder.tvAuthor.setText(book.getAuthor()); holder.itemView.setOnClickListener(v -> listener.onBookClick(book)); } @Override public int getItemCount() { return books.size(); } static class BookViewHolder extends RecyclerView.ViewHolder { final TextView tvTitle; final TextView tvAuthor; BookViewHolder(@NonNull View itemView) { super(itemView); tvTitle = itemView.findViewById(R.id.tvTitle); tvAuthor = itemView.findViewById(R.id.tvAuthor); } } }

الخطوة الرابعة — BookListActivity

تقوم شاشة القائمة بتضخيم RecyclerView، وتملؤها ببيانات نموذجية، وتربط المحوّل، وعند النقر على عنصر تبني Intent صريحًا يحمل كائن Book المحدد كإضافة قابلة للتسلسل.

// BookListActivity.java package com.example.booklist; import android.content.Intent; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.booklist.adapter.BookAdapter; import com.example.booklist.model.Book; import java.util.ArrayList; import java.util.List; public class BookListActivity extends AppCompatActivity { // ثابت مشترك بين النشاطين — يُعرَّف مرة واحدة لتفادي الأخطاء المطبعية public static final String EXTRA_BOOK = "extra_book"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_book_list); List<Book> books = buildSampleBooks(); RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new BookAdapter(books, this::onBookSelected)); } private void onBookSelected(Book book) { Intent intent = new Intent(this, BookDetailActivity.class); intent.putExtra(EXTRA_BOOK, book); // Book قابلة للتسلسل — تعمل مباشرةً startActivity(intent); } private List<Book> buildSampleBooks() { List<Book> list = new ArrayList<>(); list.add(new Book("Clean Code", "Robert C. Martin", 2008, "دليل حرفية البرمجة الرشيقة يتناول التسمية والدوال والتعليقات والمزيد.")); list.add(new Book("Effective Java", "Joshua Bloch", 2018, "أفضل الممارسات لمنصة Java بما يشمل محاور اللغة وتصميم الواجهات البرمجية.")); list.add(new Book("The Pragmatic Programmer", "Andrew Hunt & David Thomas", 1999, "نصائح خالدة حول فلسفة تطوير البرمجيات والعادات المهنية.")); list.add(new Book("Design Patterns", "Gang of Four", 1994, "المرجع الأساسي لأنماط التصميم الكائنية الـ 23 الأساسية.")); list.add(new Book("Refactoring", "Martin Fowler", 2018, "تحسين تصميم الكود الموجود عبر تحويلات منضبطة خطوة بخطوة.")); return list; } }
عرّف مفتاح الإضافة كثابت عام في النشاط المُرسِل. تستورد BookDetailActivity وتستخدم BookListActivity.EXTRA_BOOK لقراءة الإضافة. هذا الأسلوب ذو المصدر الواحد يُزيل خطأ "سلسلة نصية مختلفة، لا شيء يظهر" الكلاسيكي.

الخطوة الخامسة — تخطيط التفاصيل

<!-- res/layout/activity_book_detail.xml --> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tvDetailTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:textStyle="bold" android:textColor="@color/black" /> <TextView android:id="@+id/tvDetailAuthorYear" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:textSize="16sp" android:textColor="#777777" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginVertical="16dp" android:background="#DDDDDD" /> <TextView android:id="@+id/tvDetailSynopsis" android:layout_width="match_parent" android:layout_height="wrap_content" android:lineSpacingMultiplier="1.5" android:textSize="16sp" /> </LinearLayout> </ScrollView>

الخطوة السادسة — BookDetailActivity

// BookDetailActivity.java package com.example.booklist; import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import com.example.booklist.model.Book; public class BookDetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_book_detail); // استرداد كائن Book الممرَّر من BookListActivity Book book = (Book) getIntent().getSerializableExtra(BookListActivity.EXTRA_BOOK); if (book == null) { // دفاعي: يجب ألا يحدث من واجهتنا الخاصة، لكن نحميها finish(); return; } // تفعيل زر الرجوع في شريط الإجراءات if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(book.getTitle()); } TextView tvTitle = findViewById(R.id.tvDetailTitle); TextView tvAuthorYear = findViewById(R.id.tvDetailAuthorYear); TextView tvSynopsis = findViewById(R.id.tvDetailSynopsis); tvTitle.setText(book.getTitle()); tvAuthorYear.setText(book.getAuthor() + " · " + book.getYear()); tvSynopsis.setText(book.getSynopsis()); } @Override public boolean onSupportNavigateUp() { // سهم الرجوع في شريط الأدوات يعود إلى القائمة finish(); return true; } }

الخطوة السابعة — تسجيل النشاطين في AndroidManifest.xml

يجب أن يعلم أندرويد بكل نشاط قبل أن تتمكن من تشغيله. أعلن عن BookDetailActivity مع parentActivityName حتى يعرف النظام كيفية التعامل مع زر الرجوع بشكل صحيح:

<!-- AndroidManifest.xml (داخل <application>) --> <activity android:name=".BookListActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".BookDetailActivity" android:exported="false" android:parentActivityName=".BookListActivity" />
نسيان الإعلان عن نشاط في الملف البياني يُلقي ActivityNotFoundException أثناء التشغيل. سيتعطّل التطبيق فور استدعاء startActivity(intent). هذا من أكثر الأخطاء شيوعًا لدى المبتدئين في تطوير أندرويد — سجّل دائمًا كل نشاط.

كيف تتكامل جميع الأجزاء

إليك تدفق البيانات الكامل:

  1. يبني BookListActivity.onCreate() القائمة ويمررها إلى BookAdapter مع لامدا لأحداث النقر.
  2. ينقر المستخدم على صف. يستدعي المحوّل listener.onBookClick(book)، الذي يُعيد توجيهه إلى onBookSelected() في النشاط.
  3. يُنشأ Intent صريح يستهدف BookDetailActivity، مع إرفاق كائن Book كإضافة قابلة للتسلسل.
  4. يُشغّل أندرويد BookDetailActivity ويستدعي onCreate() الخاص به. يقرأ النشاط الإضافة، ويحمي من القيم الفارغة، ويملأ مشاهداته، ويضبط عنوان شريط الأدوات وزر الرجوع.
  5. الضغط على زر الرجوع أو سهم الرجوع ينهي BookDetailActivity ويعود المكدس الخلفي بالمستخدم إلى القائمة.

الخلاصة

لقد بنيت تطبيق أندرويد كامل بشاشتين بلغة Java. الخيارات التصميمية المُستخدمة هنا — واجهة استدعاء في المحوّل، وثابت مفتاح مشترك، ونموذج Serializable، وIntent صريح، وparentActivityName في الملف البياني — هي نفس الخيارات التي ستراها في قواعد الأكواد الاحترافية والمشاريع مفتوحة المصدر. من هنا، الخطوات الطبيعية التالية هي استبدال القائمة الثابتة بقاعدة بيانات Room، وإضافة منطق البحث والتصفية، ومشاركة نموذج البيانات بين الشاشات عبر ViewModel. لكن الهيكل البنيوي يبقى كما كتبته اليوم تمامًا.