Jetpack Compose הוא ערכת כלים מודרנית ודקלרטיבית לבניית ממשקי משתמש ל-Android. Compose מפשטת את הכתיבה והתחזוקה של ממשק המשתמש של האפליקציה באמצעות API הצהרתי שמאפשר לכם לעבד את ממשק המשתמש של האפליקציה בלי לשנות באופן אימפרטיבי את תצוגות החזית. המונחים האלה דורשים הסבר, אבל ההשלכות שלהם חשובות לעיצוב האפליקציה.
פרדיגמת התכנות הדקלרטיבית
בעבר, היררכיית תצוגות ב-Android הייתה ניתנת לייצוג כעץ של ווידג'טים של ממשק משתמש. כשהמצב של האפליקציה משתנה בגלל פעולות כמו אינטראקציות של משתמשים, צריך לעדכן את ההיררכיה של ממשק המשתמש כדי להציג את הנתונים הנוכחיים.
הדרך הנפוצה ביותר לעדכן את ממשק המשתמש היא לעבור על העץ באמצעות פונקציות כמו findViewById(), ולשנות צמתים באמצעות קריאה לפונקציות כמו button.setText(String), container.addChild(View) או img.setImageBitmap(Bitmap). השיטות האלה משנות את המצב הפנימי של הווידג'ט.
כשמבצעים שינויים בתצוגות באופן ידני, גדל הסיכוי לשגיאות. אם חלק מהנתונים מוצג בכמה מקומות, יכול להיות שתשכחו לעדכן את אחד מהתצוגות שבהן הוא מופיע. זה יכול גם להוביל למצבים לא חוקיים, כששני עדכונים מתנגשים בצורה לא צפויה. לדוגמה, יכול להיות שעדכון ינסה להגדיר ערך של צומת שהוסר ממשק המשתמש. באופן כללי, מורכבות תחזוקת התוכנה גדלה עם מספר התצוגות שנדרש לעדכן.
בשנים האחרונות, כל התעשייה החלה לעבור למודל של ממשק משתמש הצהרתי. המודל הזה מפשט את ההנדסה שקשורה ליצירה ולעדכון של ממשקי משתמש. הטכניקה הזו פועלת על ידי יצירה מחדש של כל המסך מאפס, ואז החלת השינויים הנדרשים בלבד. הגישה הזו מונעת את המורכבות של עדכון ידני של היררכיית תצוגה עם שמירת מצב. Compose הוא framework הצהרתי לממשקי משתמש.
אחד האתגרים בתהליך של יצירה מחדש של כל המסך הוא שזה עלול להיות תהליך יקר מבחינת זמן, כוח מחשוב ושימוש בסוללה. כדי לצמצם את העלות הזו, התכונה 'יצירה' בוחרת בצורה חכמה אילו חלקים בממשק המשתמש צריך לצייר מחדש בכל רגע נתון. יש לכך השלכות על האופן שבו מעצבים את רכיבי ממשק המשתמש, כמו שמוסבר במאמר בנושא קומפוזיציה מחדש.
פונקציה הניתנת להגדרה לדוגמה
באמצעות Compose, אתם יכולים ליצור את ממשק המשתמש על ידי הגדרה של קבוצה של פונקציות ניתנות להרכבה שמקבלות נתונים ומפיקות רכיבי ממשק משתמש. דוגמה: Greeting ווידג'ט שמקבל String ופולט ווידג'ט Text שמציג הודעת ברכה.
כמה דברים חשובים לגבי הפונקציה הזו:
- הערה: הפונקציה מסומנת בהערה
@Composable. כל הפונקציות הניתנות להרכבה חייבות לכלול את ההערה הזו. ההערה הזו מודיעה לקומפיילר של Compose שהפונקציה הזו נועדה להמיר נתונים לממשק משתמש. - קלט נתונים: הפונקציה מקבלת נתונים. פונקציות קומפוזביליות יכולות לקבל פרמטרים, שמאפשרים ללוגיקה של האפליקציה לתאר את ממשק המשתמש. במקרה הזה, הווידג'ט שלנו מקבל
Stringכדי שהוא יוכל לפנות למשתמש בשם. - תצוגה בממשק המשתמש: הפונקציה מציגה טקסט בממשק המשתמש. היא עושה זאת על ידי קריאה לפונקציה
Text()composable, שיוצרת בפועל את רכיב ממשק המשתמש של הטקסט. פונקציות קומפוזביליות יוצרות היררכיית ממשק משתמש על ידי קריאה לפונקציות קומפוזביליות אחרות. - אין ערך מוחזר: הפונקציה לא מחזירה כלום. פונקציות 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 פיתוח נייטיב.
הלחנה מחדש
במודל 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 כדי להציג את הערך החדש. התהליך הזה נקרא הרכבה מחדש. פונקציות אחרות שלא תלויות בערך לא מורכבות מחדש.
כמו שציינו, הרכבה מחדש של כל עץ ממשק המשתמש יכולה להיות יקרה מבחינת משאבי מחשוב, ולצרוך כוח מחשוב וחיי סוללה. התכונה 'יצירה' פותרת את הבעיה הזו באמצעות שינוי חכם של הטקסט.
קומפוזיציה מחדש היא התהליך של קריאה חוזרת לפונקציות קומפוזביליות כשקלט משתנה. כש-Compose יוצר מחדש על סמך קלט חדש, הוא קורא רק לפונקציות או ל-lambdas שאולי השתנו, ומדלג על השאר. אם מדלגים על פונקציות או על ביטויי למבדה עם פרמטרים שלא השתנו, Compose מבצע קומפוזיציה מחדש בצורה יעילה.
אל תסתמכו על תופעות לוואי מהרצה של פונקציות קומפוזביליות, כי יכול להיות שהקומפוזיציה מחדש של פונקציה תדלג על תופעות הלוואי. אם תעשו את זה, המשתמשים עלולים להיתקל בהתנהגות מוזרה ולא צפויה באפליקציה. תופעת לוואי היא כל שינוי שגלוי לשאר האפליקציה. לדוגמה, הפעולות הבאות הן תופעות לוואי מסוכנות:
- כתיבה למאפיין של אובייקט משותף
- עדכון של תצפית ב-
ViewModel - עדכון העדפות משותפות
יכול להיות שפונקציות שאפשר להרכיב יופעלו מחדש בתדירות גבוהה כמו בכל פריים, למשל כשמבצעים עיבוד של אנימציה. הפונקציות שניתנות להרכבה צריכות להיות מהירות כדי למנוע קפיצות במהלך אנימציות. אם אתם צריכים לבצע פעולות יקרות, כמו קריאה מהעדפות משותפות, עשו זאת בקורוטינה ברקע והעבירו את ערך התוצאה לפונקציה הניתנת להרכבה כפרמטר.
לדוגמה, הקוד הזה יוצר קומפוזיציה לעדכון ערך ב-SharedPreferences. הקומפוזיציה לא צריכה לקרוא או לכתוב מההעדפות המשותפות בעצמה. במקום זאת, הקוד הזה מעביר את פעולות הקריאה והכתיבה אל ViewModel
בקורוטינה ברקע. הלוגיקה של האפליקציה מעבירה את הערך הנוכחי עם קריאה חוזרת (callback) כדי להפעיל עדכון.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
במסמך הזה מפורטים כמה דברים שחשוב לדעת כשמשתמשים בכתיבה בעזרת AI:
- הרכבה מחדש מדלגת על כמה שיותר פונקציות וביטויי למדא שניתנים להרכבה.
- ההרכבה מחדש היא אופטימית ועשויה להתבטל.
- יכול להיות שהרצת פונקציה שאפשר להרכיב ממנה ממשקים תהיה תכופה מאוד, עד כדי הרצה בכל פריים של אנימציה.
- פונקציות הניתנות להגדרה יכולות לפעול במקביל.
- פונקציות שאפשר להרכיב יכולות לפעול בכל סדר.
בסעיפים הבאים נסביר איך ליצור פונקציות שאפשר להרכיב כדי לתמוך בהרכבה מחדש. בכל מקרה, השיטה המומלצת היא להקפיד שהפונקציות המורכבות יהיו מהירות, אידמפוטנטיות וללא תופעות לוואי.
התאמה מחדש מדלגת על כמה שיותר חלקים
כשחלקים בממשק המשתמש לא תקינים, Compose מנסה לבצע קומפוזיציה מחדש רק של החלקים שצריך לעדכן. כלומר, יכול להיות שהמערכת תדלג על הפעלה מחדש של רכיב אחד מסוג ButtonComposable בלי להפעיל רכיבים מסוג Composable שנמצאים מעליו או מתחתיו בעץ ממשק המשתמש.
כל פונקציה וכל 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 ידלג אל lambda Column בלי להפעיל אף אחד מהרכיבים ההורים שלו, אם header משתנה. כשמריצים את Column, יכול להיות ש-Compose ידלג על הפריטים של LazyColumn אם names לא השתנה.
שוב, כל הפונקציות או הלמדות שניתנות להרכבה צריכות להיות ללא תופעות לוואי. כשצריך לבצע תופעת לוואי, מפעילים אותה מתוך קריאה חוזרת (callback).
הקומפוזיציה מחדש היא אופטימית
קומפוזיציה מחדש מתחילה בכל פעם ש-Compose חושב שהפרמטרים של רכיב קומפוזבילי השתנו. הקומפוזיציה מחדש היא אופטימית,כלומר Compose מצפה לסיים את הקומפוזיציה מחדש לפני שהפרמטרים ישתנו שוב. אם פרמטר משתנה לפני שהקומפוזיציה מסתיימת, יכול להיות ש-Compose יבטל את הקומפוזיציה ויפעיל אותה מחדש עם הפרמטר החדש.
כשמבטלים את ההרכבה מחדש, Compose מבטל את עץ ממשק המשתמש מההרכבה מחדש. אם יש לכם תופעות לוואי שתלויות בהצגת ממשק המשתמש, תופעת הלוואי תופעל גם אם ההרכבה תבוטל. הדבר עלול לגרום למצב לא עקבי של האפליקציה.
כדי לטפל בהרכבה אופטימית, צריך לוודא שכל הפונקציות וה-lambdas שניתנים להרכבה הם אידמפוטנטיים ולא גורמים לתופעות לוואי.
יכול להיות שפונקציות הניתנות להגדרה יפעלו בתדירות גבוהה
במקרים מסוימים, פונקציה קומפוזבילית עשויה לפעול בכל פריים של אנימציה בממשק המשתמש. אם הפונקציה מבצעת פעולות שצורכות משאבים רבים, כמו קריאה מאחסון המכשיר, היא עלולה לגרום לבעיות בממשק המשתמש.
לדוגמה, אם הווידג'ט ינסה לקרוא את הגדרות המכשיר, הוא עלול לקרוא את ההגדרות האלה מאות פעמים בשנייה, עם השפעות הרסניות על הביצועים של האפליקציה.
אם פונקציה שאפשר להרכיב ממנה פונקציות אחרות צריכה נתונים, צריך להגדיר פרמטרים לנתונים האלה. אחר כך אפשר להעביר עבודה יקרה לשרשור אחר, מחוץ לקומפוזיציה, ולהעביר את הערך שמתקבל לפונקציה הניתנת להרכבה כפרמטר באמצעות mutableStateOf או LiveData.
פונקציות הניתנות להגדרה יכולות לפעול במקביל
Compose יכול לבצע אופטימיזציה של ההרכבה מחדש על ידי הפעלה של פונקציות שניתנות להרכבה במקביל. כך Compose יוכל לנצל את היתרונות של ליבות מרובות, ולהריץ פונקציות שניתנות להרכבה שלא מוצגות במסך בעדיפות נמוכה יותר.
האופטימיזציה הזו תאפשר לפונקציה קומפוזבילית לפעול במאגר של תהליכי רקע.
אם פונקציה קומפוזבילית קוראת לפונקציה ב-ViewModel, יכול להיות ש-Compose יקרא לפונקציה הזו מכמה שרשורים בו-זמנית.
כדי לוודא שהאפליקציה פועלת בצורה תקינה, לכל הפונקציות שניתנות להרכבה לא צריכות להיות תופעות לוואי. במקום זאת, מפעילים תופעות לוואי מתוך קריאות חוזרות (callback) כמו onClick שתמיד מופעלות בשרשור של ממשק המשתמש.
כשמפעילים פונקציה שאפשר להרכיב, ההפעלה עשויה להתרחש בשרשור אחר מהמתקשר. כלומר, צריך להימנע מקוד שמשנה משתנים ב-lambda שאפשר להרכיב – גם כי קוד כזה לא בטוח לשימוש עם שרשורים, וגם כי הוא מהווה תופעת לוואי אסורה של ה-lambda שאפשר להרכיב.
בדוגמה הבאה מוצג קומפוזבל שמציג רשימה ואת מספר הפריטים שבה:
@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 פיתוח נייטיב
- מצב ו-Jetpack פיתוח נייטיב
- שכבות ארכיטקטוניות ב-Jetpack פיתוח נייטיב