معالجة مدخلات المستخدم والأحداث
كل تطبيق أندرويد مفيد يتفاعل مع ما يفعله المستخدم — يضغط على زر، يكتب في حقل، يُحدّد خانة اختيار. يُعبّر الأندرويد عن جميع هذه التفاعلات من خلال مفهوم واحد: مستمع الأحداث (event listener). المستمع هو كائن ينفّذ واجهة رد نداء (callback interface)؛ تُسجّله على عنصر واجهة (view)، ويستدعيه الإطار حين يُطلق المستخدم ذلك الحدث. يُريك هذا الدرس كيفية إرفاق المستمعين وقراءة ما كتبه المستخدم، والجمع بين الاثنين لبناء شاشة تفاعلية حقيقية — بلغة Java كاملةً.
مشكلة الحصول على مرجع العنصر
قبل أن تتمكن من الاستماع إلى عنصر ما، تحتاج إلى مرجع Java له. تُعلَن العناصر في XML وتُهيَّأ بواسطة setContentView(). الجسر بين XML وJava هو findViewById():
// داخل Activity.onCreate()، بعد setContentView(R.layout.activity_main)
Button submitBtn = findViewById(R.id.btn_submit);
EditText nameField = findViewById(R.id.et_name);
TextView resultText = findViewById(R.id.tv_result);
تُولَّد ثوابت R.id.* تلقائيًا من سمات android:id في XML الخاص بالتخطيط. تعيد الدالة نوع العنصر المحدد، لذا عيّنها إلى الفئة المناسبة (Button، EditText، TextView، إلخ).
استدع setContentView() دائمًا قبل findViewById(). لا يُهيَّأ التخطيط حتى يتمّ ذلك الاستدعاء؛ واستدعاء findViewById() قبله يُعيد دائمًا null، ما يتسبب في NullPointerException في اللحظة التي تحاول فيها استخدام المرجع.
مستمعو النقر (Click Listeners)
أكثر الأحداث شيوعًا في أي تطبيق هو النقر على زر. الواجهة هي View.OnClickListener؛ ولها طريقة واحدة بالضبط: onClick(View v). هناك ثلاث طرق اصطلاحية لإرفاقها في Java:
الخيار الأول — فئة داخلية مجهولة
أسلوب Java الكلاسيكي، مطوّل لكنه واضح:
submitBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// ينفّذ هذا الكود على الخيط الرئيسي عند النقر على الزر
resultText.setText("Button tapped!");
}
});
الخيار الثاني — Lambda (Java 8+، موصى به)
لأن OnClickListener واجهة دالية (ذات طريقة مجردة واحدة)، تُعدّ lambda بديلًا أنيقًا:
submitBtn.setOnClickListener(v -> {
resultText.setText("Button tapped!");
});
استخدم lambda مع المستمعين ذوي الطريقة الواحدة. تُقلّص الكود المتكرر بشكل ملحوظ وتُبقي المنطق بجانب التسجيل مباشرةً. احتفظ بالفئات المجهولة أو الفئات الداخلية المسمّاة للحالات التي تحتاج فيها إلى الإشارة إلى كائن المستمع ذاته (مثل استدعاء removeCallbacks).
الخيار الثالث — النشاط ينفّذ الواجهة
حين يدير نشاط (Activity) أزرارًا كثيرة، يمكنه تنفيذ View.OnClickListener بنفسه والتوزيع بحسب معرّف العنصر:
public class MainActivity extends AppCompatActivity
implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnSave = findViewById(R.id.btn_save);
Button btnCancel = findViewById(R.id.btn_cancel);
btnSave.setOnClickListener(this);
btnCancel.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_save) {
// معالجة الحفظ
} else if (id == R.id.btn_cancel) {
// معالجة الإلغاء
}
}
}
قراءة النص من EditText
يحمل EditText النص الذي كتبه المستخدم. تسترجعه كـ CharSequence عبر getText()، ثم تحوّله إلى String عادي باستخدام toString(). احرص دائمًا على قطع المسافات قبل التحقق من الصحة:
String name = nameField.getText().toString().trim();
if (name.isEmpty()) {
nameField.setError("Name is required"); // يعرض نافذة منبثقة حمراء مدمجة
return;
}
resultText.setText("Hello, " + name + "!");
تُعدّ setError() الطريقة المعيارية للإبلاغ عن فشل التحقق على مستوى الحقل؛ فهي تعرض أيقونة تعجب حمراء وتلميح أدوات دون الحاجة إلى أي كود تخطيط إضافي.
عناصر الإدخال الشائعة الأخرى
إلى جانب الأزرار وحقول النص، ستتعامل بشكل متكرر مع:
- CheckBox — تعيد
isChecked() قيمة boolean. استمع إلى التغييرات بـ setOnCheckedChangeListener().
- RadioGroup / RadioButton — استدع
radioGroup.getCheckedRadioButtonId() لمعرفة معرّف الخيار المحدد، ثم findViewById(id) للحصول على نص الزر.
- Switch — نفس
setOnCheckedChangeListener() المستخدم مع CheckBox.
- Spinner — دالة رد النداء
setOnItemSelectedListener()؛ تعطيك getSelectedItem().toString() القيمة الحالية.
مثال واقعي: نموذج الملف الشخصي
إليك مثالًا كاملًا ومكتفيًا بذاته يجمع كل شيء — XML التخطيط يليه كود Activity بلغة Java:
res/layout/activity_profile.xml (مقتطف ذو صلة):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="textPersonName" />
<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
<CheckBox
android:id="@+id/cb_newsletter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subscribe to newsletter" />
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Profile" />
<TextView
android:id="@+id/tv_output"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp" />
</LinearLayout>
ProfileActivity.java:
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class ProfileActivity extends AppCompatActivity {
private EditText etUsername;
private EditText etEmail;
private CheckBox cbNewsletter;
private TextView tvOutput;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
// 1. الحصول على المراجع
etUsername = findViewById(R.id.et_username);
etEmail = findViewById(R.id.et_email);
cbNewsletter = findViewById(R.id.cb_newsletter);
tvOutput = findViewById(R.id.tv_output);
Button btnSave = findViewById(R.id.btn_save);
// 2. إرفاق المستمع
btnSave.setOnClickListener(v -> onSaveClicked());
}
private void onSaveClicked() {
String username = etUsername.getText().toString().trim();
String email = etEmail.getText().toString().trim();
// 3. التحقق من الصحة
if (username.isEmpty()) {
etUsername.setError("Username is required");
etUsername.requestFocus();
return;
}
if (email.isEmpty() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
etEmail.setError("Valid email is required");
etEmail.requestFocus();
return;
}
// 4. التفاعل مع النتيجة
String summary = "Saved: " + username + " <" + email + ">";
if (cbNewsletter.isChecked()) {
summary += "\nNewsletter: subscribed";
}
tvOutput.setText(summary);
}
}
فوّض منطق الأحداث إلى طريقة خاصة مسمّاة (مثل onSaveClicked() أعلاه) بدلًا من كتابة كل المنطق داخل lambda. تبقى lambda سطرًا واحدًا، ويصبح المنطق الحقيقي سهل القراءة والاختبار.
TextWatcher — التفاعل أثناء الكتابة
أحيانًا تحتاج إلى التفاعل مع كل ضغطة مفتاح — مثلًا لتفعيل زر فقط حين يكون النموذج صالحًا، أو لعرض عداد الأحرف. استخدم TextWatcher:
etUsername.addTextChangedListener(new android.text.TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(android.text.Editable s) {
// يُستدعى بعد كل تغيير؛ s.toString() هو النص الحالي
btnSave.setEnabled(s.toString().trim().length() > 0);
}
});
يجب تنفيذ الطرق الثلاث للواجهة؛ تلك التي لا تحتاجها يمكن تركها فارغة.
قاعدة الخيط الرئيسي
كل دالة رد نداء للمستمعين — onClick، afterTextChanged، onCheckedChanged — تُنفَّذ على الخيط الرئيسي (UI thread). يمكنك قراءة العناصر والكتابة فيها بحرية، لكن يجب ألا تُجري عملًا محظورًا هنا (طلبات شبكة، قراءة القرص، عمليات حسابية طويلة). يُطلق حظر الخيط الرئيسي لأكثر من ~5 ثوانٍ مربع حوار ANR (التطبيق لا يستجيب). انقل العمل الثقيل إلى خيط خلفي أو استخدم AsyncTask / ExecutorService / LiveData.
لا تُجرِ أبدًا استدعاءات شبكة أو قاعدة بيانات مباشرةً داخل دالة رد نداء للمستمع. إن فعلت، تجمّدت واجهة المستخدم وقد يُنهي النظام التطبيق. دائمًا أرسل العمل إلى خيط خلفي، ثم ابعث تحديثات UI إلى الخيط الرئيسي عبر runOnUiThread() أو من خلال Handler.
الخلاصة
يتدفق تفاعل المستخدم في الأندرويد عبر واجهات المستمعين المسجّلة على العناصر. سير العمل الأساسي هو: هيّئ التخطيط بـ setContentView()، احصل على مراجع العناصر بـ findViewById()، أرفق مستمعًا (يفضّل أن يكون lambda)، اقرأ المدخلات بـ getText().toString().trim()، تحقق من الصحة، وحدّث واجهة المستخدم. أبقِ دوال رد النداء خفيفة — فوّض المنطق الحقيقي إلى طرق خاصة، ولا تعيق الخيط الرئيسي أبدًا. هذه الأنماط هي الأساس الذي تبني عليه كل شاشة تفاعلية ستصنعها.