אפליקציות ל-Android נבנות בדרך כלל באמצעות מערכת ה-build של Gradle. לפני שנתעמק בפרטים של הגדרת ה-build, נסביר את המושגים שמאחורי ה-build כדי שתוכלו לראות את המערכת כמכלול.
מהי גרסת Build?
מערכת build הופכת את קוד המקור שלכם לאפליקציה שניתנת להפעלה. תהליכי בנייה כוללים בדרך כלל כמה כלים לניתוח, לקומפילציה, לקישור ולאריזה של האפליקציה או הספרייה. Gradle משתמש בגישה מבוססת-משימות כדי לארגן ולהריץ את הפקודות האלה.
משימות מכילות פקודות שמתרגמות את הקלט לפלט. תוספים מגדירים משימות ואת ההגדרות שלהן. החלת פלאגין על ה-build רושמת את המשימות שלו ומקשרת ביניהן באמצעות הקלט והפלט שלהן. לדוגמה, אם תפעילו את הפלאגין של Android Gradle (AGP) בקובץ ה-build, כל המשימות שנדרשות ליצירת APK או ספריית Android יירשמו. הפלאגין java-library מאפשר ליצור קובץ jar מקוד מקור של Java. יש תוספים דומים ל-Kotlin ולשפות אחרות, אבל תוספים אחרים נועדו להרחיב את התוספים. לדוגמה, הפלאגין protobuf נועד להוסיף תמיכה ב-protobuf לפלאגינים קיימים כמו AGP או java-library.
ב-Gradle, ההעדפה היא להשתמש במוסכמות במקום בהגדרות, ולכן הפלאגינים מגיעים עם ערכי ברירת מחדל טובים, אבל אפשר להגדיר את הגרסה באמצעות שפה ספציפית לתחום (DSL) הצהרתית. שפת ה-DSL מתוכננת כך שאפשר לציין מה לבנות, ולא איך לבנות את זה. הלוגיקה בתוספים מנהלת את ה'איך'. ההגדרה הזו מצוינת בכמה קבצים של build בפרויקט (ובפרויקטים משניים).
קלט של משימות יכול להיות קבצים וספריות, וגם מידע אחר שמקודד כסוגי Java (מספרים שלמים, מחרוזות או מחלקות בהתאמה אישית). הפלט יכול להיות רק ספרייה או קבצים, כי הוא צריך להיכתב בדיסק. חיבור של פלט של משימה לקלט של משימה אחרת מקשר בין המשימות, כך שאחת מהן צריכה לפעול לפני השנייה.
Gradle תומך בכתיבת קוד שרירותי והצהרות משימות בקובצי ה-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.
שפות ספציפיות לתחום (DSL) מנסות להקל על כולם, מומחים בתחום ומתכנתים, לתרום לפרויקט, על ידי הגדרת שפה קטנה שמייצגת נתונים בצורה טבעית יותר. תוספים של 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) שאפשר להשתמש בהם בפרויקטים משניים או בפרויקט ברמה העליונה. השימוש ב-Bazel יכול לשפר את משך זמן של תהליך build, כי הוא מאפשר לבודד את החלקים שצריך לבנות מחדש, וגם להפריד טוב יותר בין האחריות השונות באפליקציה.
במאמר הוספת יחסי תלות ב-build מוסבר איך מציינים יחסי תלות.
וריאציות build
כשיוצרים אפליקציה ל-Android, בדרך כלל כדאי ליצור כמה וריאציות. וריאנטים מכילים קוד שונה או נוצרים עם אפשרויות שונות, והם מורכבים מסוגי build ומטעמי מוצר.
סוגי build הם אפשרויות build שונות שמוצהרות. כברירת מחדל, AGP מגדיר סוגי build של 'release' ו-'debug', אבל אפשר לשנות אותם ולהוסיף עוד (למשל, לבדיקות פנימיות או לבדיקות לפני פרסום).
גרסת build לניפוי באגים לא מצמצמת את האפליקציה ולא מסתירה את הקוד שלה, ולכן היא נבנית מהר יותר ושומרת על כל הסמלים כמו שהם. היא גם מסמנת את האפליקציה כ'ניתן לבצע ניפוי באגים', חותמת עליה באמצעות מפתח debug כללי ומאפשרת גישה לקבצי האפליקציה שהותקנו במכשיר. כך אפשר לבדוק נתונים שנשמרו בקבצים ובמסדי נתונים בזמן שהאפליקציה פועלת.
גרסת build להפצה מבצעת אופטימיזציה של האפליקציה, חותמת אותה באמצעות מפתח ההפצה ומגנה על קבצי האפליקציה המותקנים.
באמצעות product flavors, אפשר לשנות את המקור שכלול ואת הווריאציות של התלות באפליקציה. לדוגמה, יכול להיות שתרצו ליצור גרסאות 'הדגמה' ו'מלאה' לאפליקציה שלכם, או אולי גרסאות 'חינמית' ו'בתשלום'. כותבים את המקור המשותף בספרייה של ערכת מקורות 'main', ומבטלים או מוסיפים מקור בערכת מקורות שנקראת על שם הטעם.
AGP יוצר וריאציות לכל שילוב של סוג build וגרסת מוצר. אם לא מגדירים טעמים, הווריאנטים נקראים על שם סוגי ה-build. אם מגדירים את שניהם, הווריאנט נקרא <flavor><Buildtype>. לדוגמה, אם יש לכם סוגי build release ו-debug, וטעמים demo ו-full, AGP ייצור וריאציות:
demoReleasedemoDebugfullReleasefullDebug
השלבים הבאים
אחרי שראיתם את המושגים שקשורים ל-build, כדאי לעיין במבנה ה-build של Android בפרויקט.