אפליקציות ל-Android נבנות בדרך כלל באמצעות מערכת ה-build של Gradle. לפני שנתעמק בפרטים של הגדרת ה-build, נסביר את המושגים שמאחורי ה-build כדי שתוכלו לראות את המערכת כמכלול.
מהי גרסת Build?
מערכת build הופכת את קוד המקור שלכם לאפליקציה שניתן להפעיל. תהליכי בנייה כוללים בדרך כלל שימוש בכמה כלים לניתוח, לקומפילציה, לקישור ולחבילה של האפליקציה או הספרייה. Gradle משתמש בגישה מבוססת-משימות כדי לארגן ולהריץ את הפקודות האלה.
משימות מכילות פקודות שמתרגמות את הקלט לפלט. תוספים מגדירים משימות ואת ההגדרות שלהן. החלת פלאגין על ה-build רושמת את המשימות שלו ומקשרת ביניהן באמצעות הקלט והפלט שלהן. לדוגמה, אם מפעילים את Android Gradle Plugin (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 והתוספים שהוחלו. בשלב הזה מתמקדים בקובץ הגדרות שבו מצהירים על הפרויקטים שרוצים ליצור ועל המיקומים שמהם רוצים לאחזר פלאגינים וספריות.
- Configuration (הגדרה) רושם משימות לכל פרויקט ומבצע את קובץ ה-build כדי להחיל את מפרט ה-build של המשתמש. חשוב להבין שלקוד ההגדרה לא תהיה גישה לנתונים או לקבצים שנוצרו במהלך ההפעלה.
- ההרצה מבצעת את ה'בנייה' בפועל של האפליקציה. הפלט של ההגדרה הוא גרף מכוון ללא מעגלים (DAG) של משימות, שמייצג את כל שלבי ה-build הנדרשים שהמשתמש ביקש (המשימות שסופקו בשורת הפקודה או כברירות מחדל בקובצי ה-build). הגרף הזה מייצג את הקשר בין המשימות, בין אם הוא מפורש בהצהרה של המשימה או מבוסס על הקלט והפלט שלה. אם למשימה יש קלט שהוא הפלט של משימה אחרת, היא חייבת לפעול אחרי המשימה האחרת. בשלב הזה, משימות לא עדכניות מופעלות לפי הסדר שמוגדר בתרשים. אם לא חל שינוי בקלט של משימה מאז ההפעלה האחרונה שלה, Gradle ידלג עליה.
מידע נוסף זמין במאמר בנושא מחזור החיים של ה-build ב-Gradle.
שפות תצורה ספציפיות לתחום (DSL)
Gradle משתמש בשפה ספציפית לדומיין (DSL) כדי להגדיר את ה-build. הגישה הזו מתמקדת בהגדרת הנתונים ולא בכתיבת הוראות מפורטות (גישת ציווי). אפשר לכתוב את קובצי ה-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) } // ... } }
Groovy
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 מיוצג על ידי פונקציה שמקבלת lambda כדי להגדיר אותו, ומאפיין עם אותו שם כדי לגשת אליו. כך הקוד בקובצי ה-build נראה יותר כמו מפרט נתונים.
תלות חיצונית
מערכת ה-build של Maven הציגה מפרט של תלות, מערכת אחסון וניהול. הספריות מאוחסנות במאגרים (שרתים או ספריות), עם מטא-נתונים שכוללים את הגרסה שלהן ויחסי תלות בספריות אחרות. אתם מציינים אילו מאגרי מידע לחפש, אילו גרסאות של התלות רוצים להשתמש, ומערכת הבנייה מורידה אותן במהלך הבנייה.
פריטי Maven מזוהים לפי שם הקבוצה (חברה, מפתח וכו'), שם הפריט (שם הספרייה) והגרסה של הפריט. בדרך כלל זה מיוצג כ-group:artifact:version.
הגישה הזו משפרת באופן משמעותי את ניהול הגרסאות. מאגרי מידע כאלה נקראים לעיתים קרובות 'מאגרי Maven', אבל הכול קשור לאופן שבו הארטיפקטים נארזים ומתפרסמים. המאגרים והמטא-נתונים האלה נעשה בהם שימוש חוזר בכמה מערכות build, כולל Gradle (ו-Gradle יכולה לפרסם במאגרים האלה). מאגרים ציבוריים מאפשרים שיתוף לשימוש של כולם, ומאגרים של חברות שומרים על תלות פנימית בתוך החברה.
אתם יכולים גם לפרק את הפרויקט לפרויקטים משניים (שנקראים גם 'מודולים' ב-Android Studio), שאפשר להשתמש בהם גם כתלות. כל פרויקט משנה יוצר פלטים (כמו קובצי JAR) שאפשר להשתמש בהם בפרויקטים משניים אחרים או בפרויקט ברמה העליונה. השימוש ב-Bazel יכול לקצר את זמן הבנייה, כי הוא מאפשר לבודד את החלקים שצריך לבנות מחדש, וגם להפריד טוב יותר בין האחריות השונות באפליקציה.
במאמר הוספת יחסי תלות של build מוסבר בפירוט איך מציינים יחסי תלות.
גרסאות Build שונות
כשיוצרים אפליקציה ל-Android, בדרך כלל רוצים ליצור כמה וריאציות. וריאציות מכילות קוד שונה או בנויות עם אפשרויות שונות, והן מורכבות מסוגי build ומטעמי מוצר.
סוגי build משתנים בהתאם לאפשרויות ה-build המוצהרות. כברירת מחדל, AGP מגדיר סוגי build של 'release' ו-'debug', אבל אפשר לשנות אותם ולהוסיף עוד (למשל, לבדיקות פנימיות או לבדיקות לפני פרסום).
גרסת debug לא מצמצמת את האפליקציה או מסתירה את הקוד שלה, ולכן היא נבנית מהר יותר ושומרת על כל הסמלים כמו שהם. היא גם מסמנת את האפליקציה כ'ניתנת לניפוי באגים', חותמת עליה באמצעות מפתח debug כללי ומאפשרת גישה לקבצי האפליקציה המותקנים במכשיר. כך אפשר לבדוק נתונים שנשמרו בקבצים ובמסדי נתונים בזמן שהאפליקציה פועלת.
בגרסת build של אפליקציה לפרסום מתבצעת אופטימיזציה של האפליקציה, היא נחתמת באמצעות מפתח הפרסום וקבצי האפליקציה המותקנים מוגנים.
באמצעות גרסאות מוצר, אפשר לשנות את המקור הכלול ווריאציות התלות של האפליקציה. לדוגמה, יכול להיות שתרצו ליצור גרסאות 'הדגמה' ו'מלאה' לאפליקציה שלכם, או אולי גרסאות 'חינמית' ו'בתשלום'. כותבים את המקור המשותף בספרייה של ערכת מקורות 'ראשית', ומבטלים או מוסיפים מקור בערכת מקורות שנקראת על שם הטעם.
AGP יוצר וריאציות לכל שילוב של סוג build וטעם מוצר. אם לא מגדירים טעמים, הווריאציות נקראות על שם סוגי ה-build. אם מגדירים את שניהם, הווריאנט נקרא <flavor><Buildtype>. לדוגמה, אם יש לכם סוגי build release ו-debug, וטעמים demo ו-full, AGP ייצור וריאציות:
demoReleasedemoDebugfullReleasefullDebug
השלבים הבאים
אחרי שהכרתם את מושגי ה-build, כדאי לעיין במבנה ה-build של Android בפרויקט.