Jetpack Compose היא ערכת כלים מודרנית ודקלרטיבית ליצירת ממשקי משתמש ל-Android. Compose מפשט את הכתיבה והתחזוקה של ממשק המשתמש של האפליקציה באמצעות API הצהרתי שמאפשר לכם לרנדר את ממשק המשתמש של האפליקציה בלי לשנות באופן אימפרטיבי את תצוגות הקצה הקדמי. המינוח הזה דורש הסבר, אבל ההשלכות שלו חשובות לעיצוב האפליקציה.
פרדיגמת התכנות הדקלרטיבי
בעבר, היררכיית התצוגה ב-Android הייתה ניתנת לייצוג כעץ של ווידג'טים בממשק המשתמש. כשהמצב של האפליקציה משתנה בגלל פעולות כמו אינטראקציות של משתמשים, צריך לעדכן את ההיררכיה של ממשק המשתמש כדי להציג את הנתונים הנוכחיים.
הדרך הנפוצה ביותר לעדכן את ממשק המשתמש היא לעבור על העץ באמצעות פונקציות כמו findViewById(), ולשנות צמתים באמצעות קריאה לפונקציות כמו button.setText(String), container.addChild(View) או img.setImageBitmap(Bitmap). השיטות האלה משנות את המצב הפנימי של הווידג'ט.
שינוי התצוגות באופן ידני מגדיל את הסיכוי לשגיאות. אם חלק מהנתונים מוצג בכמה מקומות, יכול להיות שתשכחו לעדכן את אחת התצוגות שבהן הוא מופיע. זה יכול גם להוביל למצבים לא חוקיים, כששני עדכונים מתנגשים בצורה לא צפויה. לדוגמה, יכול להיות שעדכון ינסה להגדיר ערך של צומת שהוסר ממשק המשתמש. באופן כללי, ככל שמספר התצוגות שצריך לעדכן גדול יותר, כך מורכבות תחזוקת התוכנה גדולה יותר.
במהלך השנים האחרונות, כל התעשייה החלה לעבור למודל של ממשק משתמש הצהרתי, שמפשט את תהליך הבנייה והעדכון של ממשקי משתמש.
הטכניקה פועלת על ידי יצירה מחדש של כל המסך מאפס, ואז החלת השינויים הנדרשים בלבד. הגישה הזו מונעת את המורכבות של עדכון ידני של היררכיית תצוגה עם מצב. Compose היא מסגרת הצהרתית לממשק משתמש.
אחד האתגרים בחידוש של כל המסך הוא שזה עלול להיות יקר מבחינת זמן, כוח מחשוב ושימוש בסוללה. כדי לצמצם את העלות הזו, התכונה 'יצירה' בוחרת בצורה חכמה אילו חלקים בממשק המשתמש צריך לצייר מחדש בכל רגע נתון. יש לכך השלכות על האופן שבו מעצבים את רכיבי ממשק המשתמש, כפי שמוסבר במאמר בנושא רה-קומפוזיציה.
דוגמה לפונקציה קומפוזבילית
באמצעות Compose, אפשר ליצור את ממשק המשתמש על ידי הגדרה של קבוצה של פונקציות ניתנות להרכבה שמקבלות נתונים ומפיקות רכיבי ממשק משתמש. דוגמה: הווידג'ט Greeting מקבל את הווידג'ט String ופולט את הווידג'ט Text שמציג הודעת ברכה.
כמה דברים חשובים לגבי הפונקציה הזו:
- הערה: הפונקציה מסומנת בהערה
@Composable. כל הפונקציות שניתנות להרכבה חייבות לכלול את ההערה הזו. ההערה הזו מודיעה לקומפיילר של Compose שהפונקציה הזו נועדה להמיר נתונים לממשק משתמש. - קלט נתונים: הפונקציה מקבלת נתונים. פונקציות קומפוזביליות יכולות לקבל פרמטרים, שמאפשרים ללוגיקה של האפליקציה לתאר את ממשק המשתמש. במקרה הזה, הווידג'ט שלנו מקבל
Stringכדי שהוא יוכל לברך את המשתמש בשמו. - תצוגה בממשק המשתמש: הפונקציה מציגה טקסט בממשק המשתמש. הוא עושה זאת על ידי קריאה לפונקציה קומפוזבילית
Text(), שיוצרת בפועל את רכיב ממשק המשתמש של הטקסט. פונקציות קומפוזביליות פולטות היררכיית ממשק משתמש על ידי קריאה לפונקציות קומפוזביליות אחרות. - אין ערך מוחזר: הפונקציה לא מחזירה כלום. פונקציות Compose שיוצרות ממשק משתמש לא צריכות להחזיר ערך כלשהו, כי הן מתארות את מצב המסך הרצוי במקום ליצור רכיבי UI.
מאפיינים: הפונקציה הזו מהירה, אידמפוטנטית ואין לה תופעות לוואי.
- הפונקציה מתנהגת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
random(). - הפונקציה מתארת את ממשק המשתמש בלי תופעות לוואי, כמו שינוי מאפיינים או משתנים גלובליים.
באופן כללי, כל הפונקציות שניתנות להרכבה חייבות להיכתב עם המאפיינים האלה, מהסיבות שמפורטות במאמר בנושא הרכבה מחדש.
- הפונקציה מתנהגת באותו אופן כשקוראים לה כמה פעמים עם אותו ארגומנט, והיא לא משתמשת בערכים אחרים כמו משתנים גלובליים או קריאות ל-
שינוי הפרדיגמה של הצהרה
במערכות רבות של כלים ליצירת ממשקי משתמש מונחי-אובייקטים, מאתחלים את ממשק המשתמש על ידי יצירת מופע של עץ ווידג'טים. לרוב עושים את זה על ידי ניפוח של קובץ פריסת XML. לכל ווידג'ט יש מצב פנימי משלו, והוא חושף שיטות getter ו-setter שמאפשרות ללוגיקה של האפליקציה ליצור אינטראקציה עם הווידג'ט.
בגישה הדקלרטיבית של Compose, הווידג'טים הם יחסית חסרי מצב ולא חושפים פונקציות של setter או getter. למעשה, ווידג'טים לא נחשפים כאובייקטים.
כדי לעדכן את ממשק המשתמש, קוראים לאותה פונקציה קומפוזבילית עם ארגומנטים שונים. הגישה הזו מפשטת את העברת המצב לדפוסי ארכיטקטורה כמו ViewModel, כפי שמתואר במדריך לארכיטקטורת אפליקציות. לאחר מכן, הרכיבים הקומפוזביליים אחראים להמרת המצב הנוכחי של האפליקציה לממשק משתמש בכל פעם שהנתונים שניתנים לצפייה מתעדכנים.
כשהמשתמש מקיים אינטראקציה עם ממשק המשתמש, ממשק המשתמש מעלה אירועים כמו onClick.
האירועים האלה אמורים לשלוח התראה ללוגיקה של האפליקציה, ואז אפשר לשנות את המצב של האפליקציה.
כשהמצב משתנה, הפונקציות הקומפוזביליות נקראות שוב עם הנתונים החדשים. כתוצאה מכך, רכיבי ממשק המשתמש מצוירים מחדש – התהליך הזה נקרא קומפוזיציה מחדש.
תוכן דינמי
מכיוון שפונקציות שאפשר להרכיב נכתבות ב-Kotlin ולא ב-XML, הן יכולות להיות דינמיות כמו כל קוד Kotlin אחר. לדוגמה, נניח שרוצים ליצור ממשק משתמש שמברך רשימה של משתמשים:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
הפונקציה הזו מקבלת רשימה של שמות ומפיקה ברכה לכל משתמש.
פונקציות הניתנות להגדרה יכולות להיות מורכבות למדי. אפשר להשתמש בהצהרות if כדי להחליט אם רוצים להציג רכיב בממשק המשתמש. אפשר להשתמש בלולאות. אפשר לקרוא לפונקציות עזר. יש לכם את הגמישות המלאה של השפה הבסיסית.
העוצמה והגמישות האלה הן אחד היתרונות המרכזיים של Jetpack Compose.
רה-קומפוזיציה
במודל UI אימפרטיבי, כדי לשנות ווידג'ט, קוראים לפונקציית setter בווידג'ט כדי לשנות את המצב הפנימי שלו. ב-Compose, קוראים שוב לפונקציה קומפוזבילית עם נתונים חדשים. הפעולה הזו גורמת לקומפוזיציה מחדש של הפונקציה – הווידג'טים שמוחזרים על ידי הפונקציה מצוירים מחדש, אם צריך, עם נתונים חדשים. ה-framework של Compose יכול להרכיב מחדש בצורה חכמה רק את הרכיבים שהשתנו.
לדוגמה, הפונקציה הקומפוזבילית הזו מציגה לחצן:
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
בכל פעם שלוחצים על הכפתור, המתקשר מעדכן את הערך של clicks.
הפונקציה Compose קוראת שוב לפונקציית ה-lambda עם הפונקציה Text כדי להציג את הערך החדש. התהליך הזה נקרא רה-קומפוזיציה. פונקציות אחרות שלא תלויות בערך לא מורכבות מחדש.
כמו שציינו, הרכבה מחדש של כל עץ ממשק המשתמש יכולה להיות יקרה מבחינת משאבי מחשוב, ולצרוך כוח מחשוב וחיי סוללה. כדי לפתור את הבעיה הזו, יצרנו את התכונה החכמה 'שינוי ניסוח'.
רה-קומפוזיציה היא התהליך של קריאה חוזרת לפונקציות קומפוזביליות כשקלט משתנה. כשכלי הניסוח מחדש מנסח מחדש על סמך קלט חדש, הוא קורא רק לפונקציות או לביטויי ה-lambda שאולי השתנו, ומדלג על השאר. אם מדלגים על פונקציות או על ביטויי למדה עם פרמטרים שלא השתנו, Compose מבצע קומפוזיציה מחדש בצורה יעילה.
אל תסתמכו על תופעות לוואי מהרצה של פונקציות קומפוזביליות, כי יכול להיות שהרה-קומפוזיציה של פונקציה תדלג. אם תעשו את זה, המשתמשים עלולים להיתקל בהתנהגות מוזרה ובלתי צפויה באפליקציה. תופעת לוואי היא כל שינוי שגלוי לשאר האפליקציה. לדוגמה, הפעולות הבאות הן תופעות לוואי מסוכנות:
- כתיבה למאפיין של אובייקט משותף
- עדכון של נתון שאפשר לצפות בו ב-
ViewModel - עדכון העדפות משותפות
יכול להיות שפונקציות שאפשר להרכיב יופעלו מחדש בתדירות גבוהה כמו כל פריים, למשל כשמעבדים אנימציה. פונקציות שאפשר להרכיב צריכות להיות מהירות כדי למנוע תנודות לא רצויות במהלך אנימציות. אם אתם צריכים לבצע פעולות יקרות, כמו קריאה מהעדפות משותפות, עשו זאת בקורוטינה ברקע והעבירו את התוצאה של הערך לפונקציה הניתנת להרכבה כפרמטר.
לדוגמה, הקוד הזה יוצר קומפוזיציה לעדכון ערך ב-SharedPreferences. רכיב ה-Composable לא אמור לקרוא או לכתוב מההעדפות המשותפות בעצמו. במקום זאת, הקוד הזה מעביר את פעולות הקריאה והכתיבה אל ViewModel
קורוטינה ברקע. הלוגיקה של האפליקציה מעבירה את הערך הנוכחי עם קריאה חוזרת (callback) כדי להפעיל עדכון.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
במסמך הזה מפורטים כמה דברים שחשוב לדעת כשמשתמשים בתכונה 'יצירת מוזיקה':
- רה-קומפוזיציה מדלגת על כמה שיותר פונקציות קומפוזביליות וביטויי למדא.
- ההרכבה מחדש היא אופטימית ועשויה להתבטל.
- יכול להיות שהרצה של פונקציה קומפוזבילית תהיה די תכופה, אפילו בכל פריים של אנימציה.
- אפשר להריץ פונקציות הניתנות להגדרה במקביל.
- פונקציות שאפשר להרכיב יכולות לפעול בכל סדר.
בסעיפים הבאים נסביר איך ליצור פונקציות קומפוזביליות כדי לתמוך ברה-קומפוזיציה. בכל מקרה, השיטה המומלצת היא לשמור על מהירות הפונקציות המורכבות, על אידמפוטנטיות ועל היעדר תופעות לוואי.
רה-קומפוזיציה מדלגת על כמה שיותר חלקים
כשחלקים בממשק המשתמש לא תקינים, Compose מנסה לבצע קומפוזיציה מחדש רק של החלקים שצריך לעדכן. כלומר, יכול להיות שהמערכת תדלג על הפעלה חוזרת של רכיב אחד של Button בלי להפעיל אף אחד מהרכיבים שמעליו או מתחתיו בעץ ממשק המשתמש.
כל פונקציה קומפוזבילית וכל lambda עשויים לעבור רה-קומפוזיציה בעצמם. בדוגמה הבאה אפשר לראות איך רה-קומפוזיציה יכולה לדלג על חלק מהרכיבים כשמרנדרים רשימה:
/** * Display a list of names the user can click with a header */ @Composable fun NamePicker( header: String, names: List<String>, onNameClicked: (String) -> Unit ) { Column { // this will recompose when [header] changes, but not when [names] changes Text(header, style = MaterialTheme.typography.bodyLarge) HorizontalDivider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. LazyColumn { items(names) { name -> // When an item's [name] updates, the adapter for that item // will recompose. This will not recompose when [header] changes NamePickerItem(name, onNameClicked) } } } } /** * Display a single name the user can click. */ @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable(onClick = { onClicked(name) })) }
יכול להיות שכל אחד מההיקפים האלה יהיה הדבר היחיד שיופעל במהלך הרה-קומפוזיציה.
יכול להיות שהרכיב Compose ידלג אל Column lambda בלי להפעיל אף אחד מהרכיבים ההורים שלו
כשהערך של header משתנה. ובזמן ההרצה של Column, יכול להיות ש-Compose ידלג על הפריטים של LazyColumn אם names לא השתנה.
שוב, כל הפונקציות או הלמדות שניתנות להרכבה לא צריכות להכיל תופעות לוואי. כשצריך לבצע תופעת לוואי, מפעילים אותה מתוך קריאה חוזרת (callback).
ההרכבה מחדש היא אופטימית
רה-קומפוזיציה מתחילה בכל פעם ש-Compose חושב שהפרמטרים של פונקציה קומפוזבילית השתנו. הרה-קומפוזיציה היא אופטימית,כלומר Compose מצפה לסיים את הרה-קומפוזיציה לפני שהפרמטרים ישתנו שוב. אם פרמטר משתנה לפני שהרה-קומפוזיציה מסתיימת, יכול להיות ש-Compose יבטל את הרה-קומפוזיציה ויתחיל אותה מחדש עם הפרמטר החדש.
כשמבטלים את הרה-קומפוזיציה, Compose מבטל את עץ ממשק המשתמש מהרה-קומפוזיציה. אם יש לכם תופעות לוואי שתלויות בהצגת ממשק המשתמש, תופעת הלוואי תופעל גם אם הביצוע יבוטל. הדבר עלול לגרום למצב לא עקבי באפליקציה.
כדי לטפל ברה-קומפוזיציה אופטימית, צריך לוודא שכל הפונקציות הקומפוזביליות והביטויים של lambda אידמפוטנטיים וללא תופעות לוואי.
יכול להיות שפונקציות הניתנות להגדרה יפעלו בתדירות גבוהה
במקרים מסוימים, פונקציה קומפוזבילית עשויה לפעול בכל פריים של אנימציה בממשק המשתמש. אם הפונקציה מבצעת פעולות שצורכות משאבים רבים, כמו קריאה מאחסון המכשיר, היא עלולה לגרום לבעיות בממשק המשתמש.
לדוגמה, אם הווידג'ט ינסה לקרוא את הגדרות המכשיר, הוא עלול לקרוא את ההגדרות האלה מאות פעמים בשנייה, עם השלכות הרסניות על הביצועים של האפליקציה.
אם פונקציה קומפוזבילית צריכה נתונים, צריך להגדיר פרמטרים לנתונים האלה. אחר כך אפשר להעביר עבודה שדורשת הרבה משאבים ל-Thread אחר, מחוץ לקומפוזיציה, ולהעביר את הערך שמתקבל לפונקציה קומפוזבילית כפרמטר באמצעות mutableStateOf או LiveData.
פונקציות הניתנות להגדרה יכולות לפעול במקביל
Compose יכולה לבצע אופטימיזציה של רה-קומפוזיציה על ידי הפעלת פונקציות קומפוזביליות במקביל. כך, Compose יוכל לנצל את היתרונות של ליבות מרובות, ולהריץ פונקציות שניתנות להרכבה שלא מוצגות במסך בעדיפות נמוכה יותר.
האופטימיזציה הזו תאפשר לפונקציה קומפוזבילית לפעול במאגר של שרשורים ברקע.
אם פונקציה קומפוזבילית קוראת לפונקציה ב-ViewModel, יכול להיות ש-Compose יקרא לפונקציה הזו מכמה שרשורים בו-זמנית.
כדי לוודא שהאפליקציה פועלת בצורה תקינה, לכל הפונקציות שניתנות להרכבה לא צריכות להיות תופעות לוואי. במקום זאת, מפעילים תופעות לוואי מתוך קריאות חוזרות (callback) כמו onClick שתמיד מופעלות בשרשור UI.
כשמפעילים פונקציה קומפוזבילית, ההפעלה עשויה להתרחש ב-Thread אחר מהמתקשר. כלומר, צריך להימנע מקוד שמשנה משתנים בלמבדא שניתן להרכבה – גם כי קוד כזה לא בטוח לשימוש עם שרשורים, וגם כי הוא מהווה תופעת לוואי אסורה של הלמבדא שניתן להרכבה.
דוגמה לקומפוזיציה שמציגה רשימה ואת מספר הפריטים בה:
@Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
הקוד הזה לא גורם לתופעות לוואי, והוא משנה את רשימת הקלט לממשק משתמש. זהו קוד מצוין להצגת רשימה קטנה. עם זאת, אם הפונקציה כותבת למשתנה מקומי, הקוד הזה לא יהיה בטוח לשימוש בשרשור ולא יהיה נכון:
@Composable fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Card { Text("Item: $item") items++ // Avoid! Side-effect of the column recomposing. } } } Text("Count: $items") } }
בדוגמה הזו, items משתנה בכל פעם שהפריסה מחדש מתבצעת. זה יכול להיות כל פריים באנימציה, או כשמתבצע עדכון ברשימה. בכל מקרה, בממשק המשתמש תוצג ספירה שגויה. לכן, פעולות כתיבה כאלה לא נתמכות ב-Compose. האיסור על פעולות הכתיבה האלה מאפשר למסגרת לשנות את השרשורים כדי להפעיל ביטויי למדא שניתנים להרכבה.
פונקציות הניתנות להגדרה יכולות לפעול בכל סדר
אם תסתכלו על הקוד של פונקציה קומפוזבילית, יכול להיות שתניחו שהקוד מורץ לפי הסדר שבו הוא מופיע. אבל זה לא בהכרח נכון. אם פונקציה קומפוזבילית מכילה קריאות לפונקציות קומפוזביליות אחרות, יכול להיות שהפונקציות האלה יפעלו בכל סדר. ב-Compose יש אפשרות לזהות שחלק מרכיבי ממשק המשתמש חשובים יותר מאחרים, ולצייר אותם קודם.
לדוגמה, נניח שיש לכם קוד כזה כדי לצייר שלושה מסכים בפריסת כרטיסיות:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
השיחות אל StartScreen, אל MiddleScreen ואל EndScreen עשויות להתבצע בכל סדר. המשמעות היא שאי אפשר, לדוגמה, להגדיר ש-StartScreen() יגדיר משתנה גלובלי מסוים (תופעת לוואי) ו-MiddleScreen() ינצל את השינוי הזה. במקום זאת, כל אחת מהפונקציות האלה צריכה להיות עצמאית.
מידע נוסף
כדי לקבל מידע נוסף על חשיבה ב-Compose ועל פונקציות שאפשר להוסיף, אפשר לעיין במקורות המידע הנוספים הבאים.
סרטונים
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Kotlin ל-Jetpack Compose
- מצב ו-Jetpack פיתוח נייטיב
- שכבות ארכיטקטוניות ב-Jetpack Compose