019c - View Binding ב-Fragments וב-MenuActivity


מחזור החיים של Fragment וההבדל בין Views, פריטי תפריט ומזהי משאבים

בשלבים הקודמים הפעלנו View Binding והמרנו Activities. בשלב הזה נמיר Fragment אחד ואת MenuActivity, שמארח את ה-Fragments ואת מגירת הניווט.

בדרך נפגוש מגבלה חשובה: View Binding יוצר שדות עבור Views בקובץ layout, אבל הוא לא מחליף כל שימוש ב-R.id.

האם MenuActivity הוא Fragment או Activity?

MenuActivity הוא Activity. אפשר לראות זאת לפי ההצהרה:

public class MenuActivity extends AppCompatActivity

הוא מציג את activity_menu.xml, מנהל את ה-NavigationView, ומחליף Fragments בתוך content_container.

HomeFragment, ProfileFragment ו-SettingsFragment הם Fragments. הם מוצגים בתוך ה-Activity, ולכל אחד מהם layout ומחזור חיים משלו.

שלב 1 - View Binding ב-HomeFragment

פתחו את:

  • app/src/main/java/com/example/tictacmenu/shell/HomeFragment.java

החליפו את ה-import והוסיפו שדה binding:

-import com.example.tictacmenu.R;
+import com.example.tictacmenu.databinding.FragmentHomeBinding;

 public class HomeFragment extends Fragment {
+    private FragmentHomeBinding binding;

ב-onCreateView, ה-binding מנפח את ה-layout ומחזיר את ה-root שלו:

 public View onCreateView(LayoutInflater inflater, ViewGroup container,
                          Bundle savedInstanceState) {
-    return inflater.inflate(R.layout.fragment_home, container, false);
+    binding = FragmentHomeBinding.inflate(inflater, container, false);
+    return binding.getRoot();
 }

לבסוף, נקו את ה-binding כאשר ה-View של ה-Fragment נהרס:

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

ב-Fragment חובה לנקות את ה-binding ב-onDestroyView. אובייקט ה-Fragment עשוי להישאר קיים גם לאחר שה-View שלו נהרס. שמירת binding ישן עלולה להחזיק Views שכבר אינם מוצגים ולגרום לדליפת זיכרון.

שלב 2 - התחלת ההמרה של MenuActivity

פתחו את:

  • app/src/main/java/com/example/tictacmenu/shell/MenuActivity.java

MenuActivity משתמש גם ב-binding וגם ב-R, ולכן צריכים את שני ה-imports:

import com.example.tictacmenu.R;
import com.example.tictacmenu.databinding.ActivityMenuBinding;

הוסיפו את שדה ה-binding:

private ActivityMenuBinding binding;

והחליפו את טעינת ה-layout:

-setContentView(R.layout.activity_menu);
+binding = ActivityMenuBinding.inflate(getLayoutInflater());
+setContentView(binding.getRoot());

שלב 3 - ה-Views מתוך activity_menu.xml

שלושת הרכיבים הבאים מוגדרים בתוך activity_menu.xml, ולכן ActivityMenuBinding מחזיר את האובייקטים שלהם ישירות:

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

ובהמשך:

-NavigationView navigationView = findViewById(R.id.navigation_view);
+NavigationView navigationView = binding.navigationView;

אלה שדות binding חוקיים מפני שב-activity_menu.xml קיימים Views עם המזהים:

  • @+id/drawer_layout
  • @+id/toolbar
  • @+id/navigation_view
  • @+id/content_container

שלב 4 - מדוע nav_home אינו קיים ב-binding?

ב-activity_menu.xml, ה-NavigationView מפנה לקובץ תפריט:

app:menu="@menu/menu_drawer"

אבל הפריטים nav_home, nav_local_game, nav_rtdb_prep ו-nav_profile מוגדרים בתוך res/menu/menu_drawer.xml. הם אובייקטי MenuItem, לא Views בתוך activity_menu.xml.

לכן הקוד הבא שגוי:

item.getItemId() == binding.navHome // לא קיים שדה כזה

גם השוואה בין שני האובייקטים אינה נכונה:

item == binding.navigationView // MenuItem אינו NavigationView

ה-listener מקבל MenuItem, והמתודה getItemId() מחזירה מזהה מסוג int. לכן כאן ממשיכים להשתמש ב-R.id:

navigationView.setNavigationItemSelectedListener(item -> {
    if (item.getItemId() == R.id.nav_home) {
        showFragment(new HomeFragment());
    } else if (item.getItemId() == R.id.nav_local_game) {
        startActivity(new Intent(this, MainActivity.class));
    } else if (item.getItemId() == R.id.nav_rtdb_prep) {
        startActivity(new Intent(this, Main2Activity.class));
    } else if (item.getItemId() == R.id.nav_profile) {
        showFragment(new ProfileFragment());
    }

    drawerLayout.closeDrawer(GravityCompat.START);
    return true;
});

גם setCheckedItem מצפה למזהה של פריט תפריט:

navigationView.setCheckedItem(R.id.nav_home);

שלב 5 - החלפת Fragment דורשת container ID

binding.contentContainer הוא אובייקט FrameLayout. אולם הגרסה שבה אנו משתמשים של FragmentTransaction.replace מצפה למזהה container מסוג int:

private void showFragment(Fragment fragment) {
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.content_container, fragment)
            .commit();
}

לכן הקוד הבא לא מתקמפל:

.replace(binding.contentContainer, fragment)

אפשר טכנית לכתוב binding.contentContainer.getId(), אבל במקרה הזה R.id.content_container מבטא ישירות את מה שה-API דורש: מזהה משאב, ולא אובייקט View.

מה binding מחליף ומה לא?

המקרה סוג הערך הדרוש הקוד המתאים
שינוי toolbar או NavigationView אובייקט View binding.toolbar, binding.navigationView
זיהוי MenuItem שנלחץ מזהה int item.getItemId() == R.id.nav_home
בחירת פריט תפריט מזהה int setCheckedItem(R.id.nav_home)
בחירת container לעסקת Fragment מזהה int replace(R.id.content_container, fragment)
גישה ל-View של Fragment אובייקט View binding.someView של אותו Fragment

View Binding אינו תחליף גורף למחלקה R. הוא מחליף בעיקר את פעולת החיפוש findViewById ומחזיר Views מטופסים. משאבים אחרים — תפריטים, מחרוזות, תמונות, צבעים, ולעיתים גם IDs ש-API דורש — עדיין נגישים דרך R.

בדיקה

  1. הריצו Build > Make Project.
  2. פתחו את MenuActivity וודאו שה-toolbar ומגירת הניווט מוצגים.
  3. לחצו על Home ועל Profile וודאו שה-Fragment המתאים מוצג.
  4. לחצו על Local Game ועל RTDB Prep וודאו שה-Activities המתאימים נפתחים.
  5. חזרו בין Fragments מספר פעמים וודאו שאין קריסה.