تمرير البيانات بين الشاشات
كل تطبيق أندرويد غير بسيط بحاجة إلى نقل المعلومات بين الأنشطة (Activities). يضغط المستخدم على عنصر في قائمة فتحتاج شاشة التفاصيل إلى معرفة أي عنصر جرى ضغطه. تنتهي شاشة نموذج وتحتاج الشاشة الاستدعائية إلى استلام النتيجة. يوفّر أندرويد آليةً مخصصةً لكل هذا: منظومة إضافات Intent، وفئة Bundle لتجميع القيم، وواجهة Activity Result API للتواصل ثنائي الاتجاه. يتناول هذا الدرس الثلاثة بعمق.
إضافات Intent — النمط الأساسي
يحمل كائن Intent حزمة إضافات (extras Bundle) — وهي في جوهرها خريطة مكتوبة بأزواج مفتاح-قيمة. تُلصق القيم باستخدام تحميلات putExtra() وتقرأها في الطرف المستقبِل بالأساليب المقابلة get*Extra().
// ---- النشاط المُرسِل ----
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("product_id", 42);
intent.putExtra("product_name", "Wireless Keyboard");
intent.putExtra("in_stock", true);
startActivity(intent);
// ---- النشاط المستقبِل (DetailActivity.java) ----
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
Intent intent = getIntent();
int productId = intent.getIntExtra("product_id", -1); // -1 = القيمة الافتراضية
String productName = intent.getStringExtra("product_name");
boolean inStock = intent.getBooleanExtra("in_stock", false);
TextView nameView = findViewById(R.id.textProductName);
nameView.setText(productName);
}
اصطلاح تسمية المفاتيح: عرّف مفاتيح الإضافات بوصفها ثوابت public static final String على النشاط المستقبِل. مثلًا: public static final String EXTRA_PRODUCT_ID = "com.example.shop.EXTRA_PRODUCT_ID";. يمنع استخدام البادئة الكاملة للحزمة تعارض المفاتيح إذا تلقّى تطبيقك يومًا ما نوايا من تطبيقات أخرى.
أنواع الإضافات المدعومة
تدعم عائلة putExtra() / get*Extra() جميع أنواع Java الأساسية ومصفوفاتها، وString، وCharSequence، وParcelable، وSerializable، وتنويعات ArrayList. الأكثر استخدامًا هي:
int، وlong، وdouble، وfloat، وboolean
String — الأكثر شيوعًا على الإطلاق
Parcelable — صيغة التسلسل الخاصة بأندرويد وهي الأكثر كفاءة
Serializable — التسلسل القياسي لـ Java؛ أبطأ من Parcelable، تجنّبه للكائنات الكبيرة
Bundles — تجميع القيم ذات الصلة
الـ Bundle حاوية مستقلة من أزواج مفتاح-قيمة. يمكنك بناء Bundle وملأه ثم تضمينه كإضافة واحدة. هذا هو النمط الذي تستخدمه الأجزاء (Fragments — درس 6) وهو أيضًا الأسلوب الذي يحفظ به النظام حالة النشاط ويستعيدها.
// بناء Bundle وإرفاقه
Bundle productBundle = new Bundle();
productBundle.putInt("id", 42);
productBundle.putString("name", "Wireless Keyboard");
productBundle.putDouble("price", 29.99);
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("product_data", productBundle);
startActivity(intent);
// فك تعبئته في DetailActivity
Bundle data = getIntent().getBundleExtra("product_data");
if (data != null) {
int id = data.getInt("id");
String name = data.getString("name");
double price = data.getDouble("price");
}
فضّل Parcelable على Serializable. حين تكون بياناتك كائنًا مخصصًا، نفّذ واجهة Parcelable. يستخدم Parcelable الصيغة الثنائية الخاصة بأندرويد — أسرع نحو 10 مرات من تسلسل Java وينتج مخلّفات أقل في الذاكرة. Serializable مقبول للتحويلات البسيطة قليلة التكرار لكنه يتدهور بسرعة مع الكائنات الكبيرة المتشعبة.
تمرير كائنات مخصصة باستخدام Parcelable
لتمرير كائن Product مباشرةً، نفّذ Parcelable:
import android.os.Parcel;
import android.os.Parcelable;
public class Product implements Parcelable {
private final int id;
private final String name;
private final double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// ---- كتابة Parcelable ----
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
dest.writeDouble(price);
}
// ---- قراءة Parcelable (الترتيب يجب أن يطابق الكتابة) ----
protected Product(Parcel in) {
id = in.readInt();
name = in.readString();
price = in.readDouble();
}
public static final Creator<Product> CREATOR = new Creator<Product>() {
@Override
public Product createFromParcel(Parcel in) { return new Product(in); }
@Override
public Product[] newArray(int size) { return new Product[size]; }
};
@Override
public int describeContents() { return 0; }
// دوال الوصول ...
public int getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
بمجرد تنفيذ الفئة لواجهة Parcelable، يصبح تمريرها مطابقًا لتمرير نوع أساسي:
// الإرسال
intent.putExtra(DetailActivity.EXTRA_PRODUCT, product); // product ينفّذ Parcelable
// الاستقبال (الإصدار 33+ يفضّل getParcelableExtra مع Class؛ استخدم compat للإصدارات الأقدم)
Product product = (Product) getIntent().getParcelableExtra(EXTRA_PRODUCT);
لا تمرّر مجموعات بيانات كبيرة عبر إضافات Intent. ثمة حد على مستوى نظام التشغيل لحجم مخزن IPC (نحو 1 ميجابايت للمعاملة بأكملها). إرسال Bitmaps كبيرة أو قوائم طويلة كإضافات سيرمي استثناء TransactionTooLargeException أثناء التشغيل. عوضًا عن ذلك، احفظ البيانات الكبيرة في ViewModel مشترك أو قاعدة بيانات أو ملف، ومرّر معرّفًا فحسب (ID أو URI) عبر Intent.
استرجاع النتائج — Activity Result API
الزوج القديم startActivityForResult() / onActivityResult() أصبح مهجورًا. البديل الحديث هو Activity Result API المُقدَّم في AndroidX Activity 1.2. يفصل هذا النهج التسجيل عن الإطلاق ويعمل بسلاسة مع دورة حياة النشاط.
يتكوّن النمط من ثلاثة أجزاء:
- تسجيل مُطلق (launcher) كحقل، قبل تشغيل
onCreate.
- الإطلاق باستخدام ذلك المُطلق من داخل معالج حدث.
- معالجة النتيجة داخل رد الاتصال (callback) الذي تُوفّره وقت التسجيل.
// ---- CallerActivity.java ----
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
public class CallerActivity extends AppCompatActivity {
// الخطوة 1: التسجيل قبل onCreate (كمُهيِّئ للحقل)
private final ActivityResultLauncher<Intent> editLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
// الخطوة 3: معالجة النتيجة
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
String updatedName = result.getData().getStringExtra("updated_name");
Toast.makeText(this, "تم التحديث: " + updatedName, Toast.LENGTH_SHORT).show();
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_caller);
Button editButton = findViewById(R.id.btnEdit);
editButton.setOnClickListener(v -> {
// الخطوة 2: الإطلاق
Intent intent = new Intent(this, EditActivity.class);
intent.putExtra("current_name", "Wireless Keyboard");
editLauncher.launch(intent);
});
}
}
// ---- EditActivity.java ----
public class EditActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
String currentName = getIntent().getStringExtra("current_name");
EditText nameField = findViewById(R.id.editName);
nameField.setText(currentName);
Button saveButton = findViewById(R.id.btnSave);
saveButton.setOnClickListener(v -> {
String newName = nameField.getText().toString().trim();
Intent resultIntent = new Intent();
resultIntent.putExtra("updated_name", newName);
setResult(RESULT_OK, resultIntent); // RESULT_OK أو RESULT_CANCELED
finish(); // يغلق EditActivity ويعود إلى المستدعي
});
Button cancelButton = findViewById(R.id.btnCancel);
cancelButton.setOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
}
}
لماذا التسجيل قبل onCreate؟ يجب استدعاء registerForActivityResult() قبل أن يصل النشاط إلى حالة STARTED. يضمن التسجيل كمُهيِّئ للحقل (أو في جسم الفئة لا داخل مستمع النقر) ذلك. استدعاؤه بشكل كسول داخل معالج onClick سيرمي IllegalStateException.
رموز النتائج
رمز النتيجة عدد صحيح int عادي. يوفّر أندرويد ثابتين: RESULT_OK (القيمة -1) وRESULT_CANCELED (القيمة 0). يمكنك أيضًا تعريف رموز مخصصة (أعداد صحيحة موجبة) للسيناريوهات التي يمكن فيها للشاشة الانتهاء بأكثر من نتيجتين ذات معنى، وإن كانت حالتان كافيتين عادةً إذا حملت إضافات Intent للنتيجة التفاصيل اللازمة.
الخلاصة
استخدم Intent.putExtra() لتسليم البيانات في اتجاه واحد — الأنواع الأساسية والسلاسل النصية للقيم البسيطة، وكائنات Parcelable للبيانات المعقدة، وBundle حين تريد تجميع قيم مترابطة. عرّف مفاتيح الإضافات دائمًا كثوابت. للتواصل ثنائي الاتجاه، سجّل ActivityResultLauncher قبل onCreate، وأطلقه من معالج الأحداث، واستدع setResult() + finish() في النشاط المستجيب. أبقِ جميع البيانات المُحوَّلة صغيرة؛ مرّر معرّفات لا كتل بيانات خام حين تتعامل مع حمولات كبيرة.