019a - מעבר ל־View Binding


מחליפים findViewById בגישה דרך binding אחרי Gradle Sync

חזרה ל־018d: התחברות Google והגדרת SHA1

המטרה בשלב הזה היא לא לכתוב מחדש את כל האפליקציה. מפעילים View Binding, מסנכרנים Gradle, ואז מחליפים בהדרגה קריאות findViewById בגישה ישירה דרך אובייקט binding.

בסוף השלב הזה נרצה לראות:

  1. viewBinding = true מוגדר במודול app.
  2. Android Studio מזהה מחלקות כמו ActivityLoginBinding.
  3. לפחות מסך אחד, למשל LoginActivity, כבר עובד בלי findViewById.
  4. שאר המסכים נשארים תקינים ואפשר להמיר אותם בהמשך בקצב מבוקר.

שלב 1 - הפעלת View Binding ב־Gradle

פתחו את קובץ המודול:

  • app/build.gradle.kts

בתוך הבלוק android, הוסיפו:

 android {
     namespace = "com.example.tictacmenu"
     compileSdk {
         version = release(36)
     }
 
+    buildFeatures {
+        viewBinding = true
+    }
+
     defaultConfig {
         applicationId = "com.example.tictacmenu"
         ...
     }
 }

אם כבר קיים אצלכם בלוק buildFeatures, אל תיצרו בלוק שני. הוסיפו רק את השורה:

viewBinding = true

אחרי העריכה חובה לבצע Sync Now / Sync Project with Gradle Files. רק אחרי הסנכרון Android Studio יוצר את מחלקות ה־Binding.

אם ActivityLoginBinding מופיע באדום מיד אחרי העריכה, זה בדרך כלל לא אומר שהקוד שגוי. קודם מריצים Gradle Sync. אם זה עדיין אדום, בודקים ששם קובץ ה־XML באמת activity_login.xml ושאתם מייבאים את ה־package הנכון.


שלב 2 - להבין את השמות שנוצרים

View Binding יוצר מחלקת Java עבור כל קובץ layout תחת res/layout.

דוגמאות נפוצות:

קובץ XML מחלקת Binding
activity_login.xml ActivityLoginBinding
activity_menu.xml ActivityMenuBinding
fragment_home.xml FragmentHomeBinding

גם מזהי ה־Views הופכים לשדות על binding.

id ב־XML גישה בקוד
@+id/main binding.main
@+id/cBstayconnect binding.cBstayconnect
@+id/drawer_layout binding.drawerLayout
@+id/navigation_view binding.navigationView

ב־Java השמות נוצרים לפי ה־id. אם משנים id ב־XML, הקוד שמשתמש בשם הישן יישבר בזמן קומפילציה. זה טוב: השגיאה מתגלה מוקדם במקום להפוך לקריסה בזמן ריצה.


שלב 3 - המרה ראשונה: LoginActivity

פתחו ב־Android Studio את app > kotlin+java > com.example.tictacmenu > activities > LoginActivity.

נקודת המוצא כאן היא מסך ההתחברות אחרי 018c ו־018d: דוא״ל וסיסמה, Google ושמירת הבחירה ״הישאר מחובר״. אם כבר השתמשתם בגרסת 018c עם binding, בצעו רק את ההחלפות שעדיין חסרות. אין להחזיר את המעבר הזמני עם Handler שהוסר בשיעור ההתחברות.

א. שדה binding ואתחול המסך

הוסיפו את הייבוא (התאימו את ה־package לפרויקט שלכם):

import com.example.tictacmenu.databinding.ActivityLoginBinding;

בגוף המחלקה, מחוץ לכל מתודה, החליפו את שדות רכיבי המסך בשדה binding אחד. שדות שאינם רכיבי מסך, כמו settings ו־loginInProgress, נשארים:

 public class LoginActivity extends AppCompatActivity {
     ⁞
-    private CheckBox cBstayconnect;
-    private EditText eTemail;
-    private EditText eTpass;
-    private Button btnLogin;
-    private Button btnGoogleSignIn;
+    private ActivityLoginBinding binding;
     ⁞
    

בתחילת onCreate, מיד אחרי EdgeToEdge.enable(this):

         super.onCreate(savedInstanceState);
         EdgeToEdge.enable(this);
-        setContentView(R.layout.activity_login);
-        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
+        binding = ActivityLoginBinding.inflate(getLayoutInflater());
+        setContentView(binding.getRoot());
+        ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
             Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
             return insets;
         });
    

מחקו את ההשמות הבאות מהמקום שבו הן נמצאות אצלכם — initViews() או onCreate:

-        cBstayconnect = findViewById(R.id.cBstayconnect);
-        eTemail = findViewById(R.id.eTemail);
-        eTpass = findViewById(R.id.eTpass);
-        btnLogin = findViewById(R.id.btnLogin);
-        btnGoogleSignIn = findViewById(R.id.btnGoogleSignIn);
    

אם initViews() התרוקנה, מחקו גם אותה ואת הקריאה אליה מתוך onCreate. השאירו כל פעולה אחרת, ובפרט את FBRef.initializeGoogleSignIn(this).

ב. מחליפים כל שימוש בשדות שמחקנו

מחיקת ההצהרות ו־findViewById היא רק חצי מההמרה. בכל המחלקה, גם במתודות אחרות ובתוך callbacks, כל שימוש בשדה UI שמחקנו צריך לעבור דרך binding.. אחרת יישארו שמות לא מוכרים כמו eTemail או btnLogin.

binding.eTemail הוא אותו רכיב קלט שאליו ניגשנו קודם דרך המשתנה eTemail. הפעולות getText(), setError() ו־requestFocus() נשארות; משתנה רק הדרך להגיע לרכיב. שם השדה נוצר לפי ה־id ב־XML ואפשר לבחור אותו בהשלמה האוטומטית אחרי binding..

בהשוואות הבאות הקוד לפני מופיע משמאל והקוד אחרי מימין. התצוגה מיועדת למסך רחב ושומרת על השורות זו מול זו. רק התוספת binding. מסומנת בירוק ובקו תחתון. שאר השורה נשארת ללא הדגשה, כי אין בה שינוי.

ב־onLoginClick: קריאת הקלט, הודעות השגיאה והעברת המיקוד. החליפו את תחילת המתודה והשאירו את המשך ההתחברות במקומו:

לפני

public void onLoginClick(View view) {
    if (loginInProgress) return;

    String email = eTemail.getText().toString().trim();
    String password = eTpass.getText().toString();

    if (TextUtils.isEmpty(email)) {
        eTemail.setError("Enter e-mail");
        eTemail.requestFocus();
        return;
    }

    if (TextUtils.isEmpty(password)) {
        eTpass.setError("Enter password");
        eTpass.requestFocus();
        return;
    }

    setLoginInProgress(true);
    ⁞

אחרי

public void onLoginClick(View view) {
    if (loginInProgress) return;

    String email = binding.eTemail.getText().toString().trim();
    String password = binding.eTpass.getText().toString();

    if (TextUtils.isEmpty(email)) {
        binding.eTemail.setError("Enter e-mail");
        binding.eTemail.requestFocus();
        return;
    }

    if (TextUtils.isEmpty(password)) {
        binding.eTpass.setError("Enter password");
        binding.eTpass.requestFocus();
        return;
    }

    setLoginInProgress(true);
    ⁞

email ו־password נשארים משתני String מקומיים, ו־loginInProgress נשאר שדה boolean. לא מוסיפים להם binding.. גם הפרמטר view של מתודת הלחיצה נשאר כפי שהוא.

ב־onCreate: שחזור הבחירה והאזנה לשינוי ב־checkbox.

לפני

cBstayconnect.setChecked(settings.getBoolean(KEY_STAY_CONNECT, false));
cBstayconnect.setOnCheckedChangeListener((buttonView, isChecked) ->
        settings.edit().putBoolean(KEY_STAY_CONNECT, isChecked).apply()
);

אחרי

binding.cBstayconnect.setChecked(settings.getBoolean(KEY_STAY_CONNECT, false));
binding.cBstayconnect.setOnCheckedChangeListener((buttonView, isChecked) ->
        settings.edit().putBoolean(KEY_STAY_CONNECT, isChecked).apply()
);

בתוך callback של הצלחת ההתחברות ב־onLoginClick:

לפני

FBRef.getUser(user);
settings.edit().putBoolean(KEY_STAY_CONNECT, cBstayconnect.isChecked()).apply();
Toast.makeText(this, "Login success", Toast.LENGTH_SHORT).show();

אחרי

FBRef.getUser(user);
settings.edit().putBoolean(KEY_STAY_CONNECT, binding.cBstayconnect.isChecked()).apply();
Toast.makeText(this, "Login success", Toast.LENGTH_SHORT).show();

גם בתוך callback של הצלחת ההתחברות ב־firebaseAuthWithGoogle:

לפני

FBRef.getUser(currentUser);
settings.edit().putBoolean(KEY_STAY_CONNECT, cBstayconnect.isChecked()).apply();
Toast.makeText(this, "Google login success", Toast.LENGTH_SHORT).show();

אחרי

FBRef.getUser(currentUser);
settings.edit().putBoolean(KEY_STAY_CONNECT, binding.cBstayconnect.isChecked()).apply();
Toast.makeText(this, "Google login success", Toast.LENGTH_SHORT).show();

ב־setLoginInProgress: הפעלת הכפתורים ועדכון הכיתוב.

לפני

private void setLoginInProgress(boolean inProgress) {
    loginInProgress = inProgress;
    btnLogin.setEnabled(!inProgress);
    btnGoogleSignIn.setEnabled(!inProgress);
    btnLogin.setText(inProgress ? "Logging in..." : "Login");
}

אחרי

private void setLoginInProgress(boolean inProgress) {
    loginInProgress = inProgress;
    binding.btnLogin.setEnabled(!inProgress);
    binding.btnGoogleSignIn.setEnabled(!inProgress);
    binding.btnLogin.setText(inProgress ? "Logging in..." : "Login");
}

אם טרם הוספתם את התחברות Google, המירו את השורות שכבר קיימות אצלכם; אין צורך להוסיף כאן את מנגנון ההתחברות שלה.

ג. בדיקת השלמת ההמרה

  1. חפשו בקובץ את cBstayconnect, eTemail, eTpass, btnLogin ו־btnGoogleSignIn. בכל שימוש ברכיבים שהשדות שלהם נמחקו צריכה להיות גישה דרך binding., גם מחוץ ל־onCreate. אל תבצעו החלפת טקסט גורפת שעלולה ליצור binding.binding.eTemail.
  2. ודאו שאין עוד findViewById במסך הזה או קריאה למתודת initViews שנמחקה. הסירו imports של CheckBox, EditText ו־Button רק אם אין בהם עוד שימוש. הסירו את import של R רק אם אין בקובץ שימוש אחר במשאבים דרך R.
  3. הריצו Build. אם שם UI ישן מופיע באדום, השלימו את ההמרה באותו מקום.
  4. הריצו את האפליקציה ולחצו Login עם דוא״ל ריק, ואחר כך עם דוא״ל וסיסמה ריקה. ודאו שהשגיאה והמיקוד מופיעים בשדה הנכון.
  5. בדקו התחברות בדוא״ל וב־Google אם הוגדרה בפרויקט. ודאו ששני הכפתורים מושבתים בזמן ההתחברות וחוזרים לפעול בסיומה, ושהכיתוב של Login מתעדכן.
  6. בדקו שהבחירה ״הישאר מחובר״ נשמרת לאחר סגירה ופתיחה מחדש, ושהתחברות מוצלחת עדיין פותחת את התפריט.
מקור ההשוואה בהיסטוריית TicTacMenu

הקומיט 1358935 מציג את המרת מסך ה־checkbox המוקדם ל־binding. בקומיט e14c7c2 נוסף קוד ההתחברות בדוא״ל כשהוא כבר משתמש ב־binding, וב־fd354b7 נוספה התחברות Google. לכן הדיפים כאן משווים את הקוד ללא binding שנלמד ב־018c/018d לצורת הגישה שבפרויקט הייחוס; הם אינם העתק של קומיט יחיד שממיר את כל מסך ההתחברות.

המרת מסך ה־checkbox המוקדם ל־View Binding


שלב 4 - איך ממשיכים למסכים אחרים

אל תנסו להמיר את כל הפרויקט במכה אחת. בחרו Activity אחד, בנו והריצו, ואז עברו לבא.

הדפוס הקבוע בכל Activity הוא:

-setContentView(R.layout.activity_something);
+binding = ActivitySomethingBinding.inflate(getLayoutInflater());
+setContentView(binding.getRoot());

ואז מחליפים:

-drawerLayout = findViewById(R.id.drawer_layout);
-MaterialToolbar toolbar = findViewById(R.id.toolbar);
-NavigationView navigationView = findViewById(R.id.navigation_view);
+drawerLayout = binding.drawerLayout;
+MaterialToolbar toolbar = binding.toolbar;
+NavigationView navigationView = binding.navigationView;

במסך המשחק אפשר להמיר בהדרגה. לדוגמה, במקום:

-EditText ipEdit = findViewById(R.id.editServerIP);
+EditText ipEdit = binding.editServerIP;

אם יש לולאות שמקבלות id ומבצעות findViewById(id), מותר להשאיר אותן לשלב מאוחר יותר. המטרה כאן היא מעבר מדורג, לא ריפקטור גדול.


נוחות שנרוויח בהמשך

כשנתחיל להוסיף יותר UI, Firebase, תפריטים, ו־Fragments, binding יחסוך הרבה עבודה שחוזרת על עצמה:

  1. Autocomplete אמיתי: כותבים binding. ורואים את כל ה־Views של המסך.
  2. פחות casting: אין צורך להמיר ידנית View ל־Button, EditText, NavigationView וכדומה.
  3. שגיאות מוקדמות: אם מחקתם או שיניתם id ב־XML, הקוד נשבר בקומפילציה ולא בזמן ריצה.
  4. מסכי Login נוחים יותר: כפי שראינו בהמרת מסך ההתחברות, אפשר לקרוא binding.eTemail, binding.eTpass, binding.btnGoogleSignIn בלי לחפש אותם ידנית.
  5. Drawer נקי יותר: MenuActivity נהיה קריא יותר עם binding.drawerLayout, binding.toolbar, ו־binding.navigationView.
  6. Fragments בטוחים יותר: גם בפרגמנטים משתמשים ב־binding, אבל שם חייבים לנקות אותו ב־onDestroyView.
דפוס קצר ל־Fragment

ב־Fragment ה־View יכול להיהרס לפני שה־Fragment עצמו נהרס. לכן לא משאירים binding חי אחרי onDestroyView.

private FragmentHomeBinding binding;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    binding = FragmentHomeBinding.inflate(inflater, container, false);
    return binding.getRoot();
}

@Override
public void onDestroyView() {
    super.onDestroyView();
    binding = null;
}

תקלות נפוצות

ActivityLoginBinding לא מזוהה:

  • ודאו שביצעתם Gradle Sync אחרי viewBinding = true.
  • ודאו שהקובץ נמצא תחת app/src/main/res/layout/activity_login.xml.
  • ודאו שהייבוא הוא com.example.tictacmenu.databinding.ActivityLoginBinding או ה־package שלכם.

שדה מסוים לא קיים על binding:

  • בדקו את ה־id המדויק ב־XML.
  • אם ה־id כולל underscore, השם בקוד יהיה בדרך כלל camelCase, למשל drawer_layout -> drawerLayout.
  • אם הרכיב נמצא רק בגרסת layout מסוימת, למשל רק ב־layout-land, ייתכן שהשדה יהיה nullable ותצטרכו לבדוק null.

אפשר לגרום ל־View Binding להתעלם מקובץ layout מסוים בעזרת tools:viewBindingIgnore="true" על ה־root של ה־XML, אבל בשלב הזה בדרך כלל אין צורך בזה.


המשך שיעורי View Binding

מורה־עזר ב־Gemini

פתחו את Gem: מעבדת TicTacToe: Model, View Binding ועיצוב כדי לקבל רמזים, שאלות אבחון והסברים המתאימים לשלב שבו אתם נמצאים.