אפליקציות ל-Android נבנות בדרך כלל באמצעות מערכת ה-build של Gradle. לפני שנתעמק בפרטים של הגדרת הבנייה, נסביר את המושגים שמאחורי הבנייה כדי שתוכלו לראות את המערכת כמכלול.
מהי גרסת Build?
מערכת build הופכת את קוד המקור שלכם לאפליקציה שניתן להפעיל. תהליכי בנייה כוללים בדרך כלל כמה כלים לניתוח, לקומפילציה, לקישור ולאריזה של האפליקציה או הספרייה. Gradle משתמש בגישה מבוססת-משימות כדי לארגן ולהריץ את הפקודות האלה.
משימות כוללות פקודות שמתרגמות את הקלט לפלט. תוספים מגדירים משימות ואת ההגדרות שלהן. החלת פלאגין על ה-build רושמת את המשימות שלו ומקשרת אותן באמצעות הקלט והפלט שלהן. לדוגמה, אם תפעילו את פלאגין של Android Gradle (AGP) בקובץ ה-build, כל המשימות שנדרשות ליצירת APK או ספריית Android יירשמו. הפלאגין java-library מאפשר ליצור קובץ jar מקוד מקור של Java. יש תוספים דומים ל-Kotlin ולשפות אחרות, אבל תוספים אחרים נועדו להרחיב את התוספים. לדוגמה, הפלאגין protobuf נועד להוסיף תמיכה ב-protobuf לפלאגינים קיימים כמו AGP או java-library.
ב-Gradle, המוסכמות מקבלות עדיפות על פני ההגדרות, ולכן הפלאגינים מגיעים עם ערכי ברירת מחדל טובים מראש, אבל אפשר להגדיר את הגרסה באמצעות שפה ספציפית לתחום (DSL) הצהרתית. שפת ה-DSL מתוכננת כך שאפשר לציין מה לבנות, ולא איך לבנות את זה. הלוגיקה בתוספים מנהלת את האופן שבו זה קורה. ההגדרה הזו מצוינת בכמה קבצים לבנייה בפרויקט (ובפרויקטים משניים).
קלט של משימות יכול להיות קבצים וספריות, וגם מידע אחר שמקודד כסוגי Java (מספרים שלמים, מחרוזות או מחלקות בהתאמה אישית). הפלט יכול להיות רק ספרייה או קבצים, כי הוא צריך להיכתב בדיסק. חיבור פלט של משימה לקלט של משימה אחרת מקשר בין המשימות, כך שמשימה אחת צריכה לפעול לפני השנייה.
למרות ש-Gradle תומך בכתיבת קוד שרירותי והצהרות על משימות בקובצי ה-build, זה יכול להקשות על כלי העבודה להבין את ה-build ועל התחזוקה שלו. לדוגמה, אפשר לכתוב בדיקות לקוד בתוך תוספים, אבל לא בקובצי build. במקום זאת, כדאי להגביל את הלוגיקה של ה-build ואת ההצהרות של המשימות לתוספים (שאתם או מישהו אחר מגדירים), ולהצהיר בקובצי ה-build איך אתם רוצים להשתמש בלוגיקה הזו.
מה קורה כשמריצים Gradle build?
הידור Gradle מתבצע בשלושה שלבים. בכל אחת מהפאזות האלה מופעלים חלקים שונים של קוד שהגדרתם בקובצי ה-build.
- האתחול קובע אילו פרויקטים ותתי-פרויקטים נכללים ב-build, ומגדיר את נתיבי המחלקות שמכילים את קובצי ה-build והתוספים שהוחלו. בשלב הזה מתמקדים בקובץ הגדרות שבו מצהירים על הפרויקטים שרוצים ליצור ועל המיקומים שמהם רוצים לאחזר פלאגינים וספריות.
- ההגדרה רושמת משימות לכל פרויקט ומבצעת את קובץ ה-build כדי להחיל את מפרט ה-build של המשתמש. חשוב להבין שלקוד ההגדרה לא תהיה גישה לנתונים או לקבצים שנוצרו במהלך ההפעלה.
- ההרצה מבצעת את ה'בנייה' בפועל של האפליקציה. הפלט של ההגדרה הוא גרף אציקלי מכוון (DAG) של משימות, שמייצג את כל שלבי הבנייה הנדרשים שהמשתמש ביקש (המשימות שסופקו בשורת הפקודה או כברירות מחדל בקובצי הבנייה). הגרף הזה מייצג את הקשר בין המשימות, בין אם הוא מפורש בהצהרה של המשימה או מבוסס על הקלט והפלט שלה. אם למשימה יש קלט שהוא הפלט של משימה אחרת, היא חייבת לפעול אחרי המשימה האחרת. בשלב הזה, משימות לא עדכניות מופעלות לפי הסדר שמוגדר בתרשים. אם נתוני הקלט של משימה לא השתנו מאז ההפעלה האחרונה שלה, Gradle ידלג עליה.
מידע נוסף זמין במאמר בנושא מחזור החיים של ה-build ב-Gradle.
DSLs להגדרות
Gradle משתמש בשפה ספציפית לדומיין (DSL) כדי להגדיר את ה-builds. הגישה הזו מתמקדת בציון הנתונים ולא בכתיבת הוראות מפורטות (גישת ציווי). אפשר לכתוב את קובצי ה-build באמצעות Kotlin או Groovy, אבל אנחנו ממליצים מאוד להשתמש ב-Kotlin.
שפות ספציפיות לתחום מנסות להקל על כולם, מומחים בתחום ומתכנתים, לתרום לפרויקט, ולהגדיר שפה קטנה שמייצגת נתונים בצורה טבעית יותר. תוספי Gradle יכולים להרחיב את ה-DSL כדי להגדיר את הנתונים שהם צריכים למשימות שלהם.
לדוגמה, הגדרת החלק של Android ב-build יכולה להיראות כך:
Kotlin
android { namespace = "com.example.app" compileSdk { version = release(36) { minorApiLevel = 1 } } // ... defaultConfig { applicationId = "com.example.app" minSdk { version = release(23) } targetSdk { version = release(36) } // ... } }
מגניב
android { namespace = 'com.example.app' compileSdk { version = release(36) { minorApiLevel = 1 } } // ... defaultConfig { applicationId = 'com.example.app' minSdk { version = release(23) } targetSdk { version = release(36) } // ... } }
מאחורי הקלעים, קוד ה-DSL דומה לזה:
fun Project.android(configure: ApplicationExtension.() -> Unit) {
...
}
interface ApplicationExtension {
var namespace: String?
fun compileSdk(configure: CompileSdkSpec.() -> Unit) {
...
}
val defaultConfig: DefaultConfig
fun defaultConfig(configure: DefaultConfig.() -> Unit) {
...
}
}
כל בלוק ב-DSL מיוצג על ידי פונקציה שמקבלת למדה כדי להגדיר אותו, ומאפיין עם אותו שם כדי לגשת אליו. כך הקוד בקובצי ה-build נראה יותר כמו מפרט נתונים.
יחסי תלות חיצוניים
מערכת ה-build של Maven הציגה מפרט של תלות, מערכת אחסון וניהול. הספריות מאוחסנות במאגרים (שרתים או ספריות), עם מטא-נתונים שכוללים את הגרסה שלהן ויחסי תלות בספריות אחרות. אתם מציינים אילו מאגרי מידע לחפש, אילו גרסאות של התלויות רוצים להשתמש, ומערכת build מורידה אותן במהלך ה-build.
פריטי Maven מזוהים לפי שם הקבוצה (חברה, מפתח וכו'), שם הפריט (שם הספרייה) והגרסה של הפריט. בדרך כלל זה הנתיב group:artifact:version.
הגישה הזו משפרת באופן משמעותי את ניהול הגרסאות. לרוב, מאגרי מידע כאלה נקראים 'מאגרי Maven', אבל הכול קשור לאופן שבו הארטיפקטים נארזים ומתפרסמים. המאגרים והמטא-נתונים האלה נעשה בהם שימוש חוזר בכמה מערכות build, כולל Gradle (ו-Gradle יכולה לפרסם במאגרים האלה). מאגרים ציבוריים מאפשרים שיתוף לשימוש של כולם, ומאגרים של חברות שומרים על תלות פנימית בתוך החברה.
אתם יכולים גם לפרק את הפרויקט לפרויקטים משניים (שנקראים גם 'מודולים' ב-Android Studio), שאפשר להשתמש בהם גם כתלות. כל פרויקט משנה יוצר פלטים (כמו קובצי JAR) שאפשר להשתמש בהם בפרויקטים משניים או בפרויקט ברמה העליונה. השימוש ב-Gradle יכול לשפר את משך זמן של תהליך build, כי הוא מאפשר לבודד את החלקים שצריך לבנות מחדש, וגם להפריד טוב יותר בין האחריות השונות באפליקציה.
במאמר הוספת יחסי תלות ב-build מוסבר איך מציינים יחסי תלות.
וריאנטים של build
כשיוצרים אפליקציה ל-Android, בדרך כלל כדאי ליצור כמה וריאציות. הווריאנטים מכילים קוד שונה או נוצרים עם אפשרויות שונות, והם מורכבים מסוגי build ומטעמי מוצר.
סוגי build הם אפשרויות build שונות שמוצהרות. כברירת מחדל, AGP מגדיר סוגי build של 'release' ו-'debug', אבל אפשר לשנות אותם ולהוסיף עוד (למשל, לבדיקות פנימיות או לבדיקות לפני השקת האפליקציה).
גרסת build לניפוי באגים לא מצמצמת את האפליקציה או מבצעת ערפול קוד (obfuscation), ולכן היא נבנית מהר יותר ושומרת על כל הסמלים כמו שהם. היא גם מסמנת את האפליקציה כ'ניתנת לבצע ניפוי באגים', חותמת עליה באמצעות מפתח debug כללי ומאפשרת גישה לקבצי האפליקציה המותקנים במכשיר. כך אפשר לבדוק נתונים שנשמרו בקבצים ובמסדי נתונים בזמן שהאפליקציה פועלת.
גרסת build להפצה מבצעת אופטימיזציה של האפליקציה, חותמת עליה באמצעות מפתח ההפצה ומגנה על קובצי האפליקציה המותקנים.
באמצעות product flavors, אפשר לשנות את המקור שכלול ואת הווריאציות של התלות באפליקציה. לדוגמה, יכול להיות שתרצו ליצור גרסאות 'הדגמה' ו'מלאה' לאפליקציה שלכם, או אולי גרסאות 'חינמית' ו'בתשלום'. כותבים את המקור המשותף בספרייה של ערכת מקורות 'ראשית', ומבטלים או מוסיפים מקור בערכת מקורות שנקראת על שם הטעם.
AGP יוצר וריאציות לכל שילוב של סוג build וגרסת מוצר. אם לא מגדירים flavors, הווריאנטים מקבלים שם לפי סוגי ה-build. אם מגדירים את שניהם, הווריאנט נקרא <flavor><Buildtype>. לדוגמה, אם יש לכם סוגי build release ו-debug, וטעמים demo ו-full, AGP ייצור וריאציות:
demoReleasedemoDebugfullReleasefullDebug
השלבים הבאים
אחרי שהכרתם את מושגי ה-build, כדאי לעיין במבנה ה-build של Android בפרויקט.