סקירה כללית של Gradle build

בדרך כלל, אפליקציות ל-Android נוצרות באמצעות מערכת ה-build של Gradle. לפני שנצלול לפרטים של הגדרת ה-build, נבחן את המושגים שעומדים מאחורי ה-build כדי שתוכלו לראות את המערכת כמכלול.

מהו build?

מערכת build ממירה את קוד המקור לאפליקציה שניתן להריץ. בדרך כלל, תהליך ה-build כולל כמה כלים לניתוח, להדרכה, לקישור ולאריזה של האפליקציה או הספרייה. Gradle משתמש בגישה מבוססת-משימות כדי לארגן ולהריץ את הפקודות האלה.

משימות מכילות פקודות שממירות את הקלט שלהן לפלט. יישומי פלאגין מגדירים משימות והגדרות שלהן. כשמחילים פלאגין על ה-build, מתבצע רישום של המשימות שלו וחיבור שלהן באמצעות הקלט והפלט שלהן. לדוגמה, החלת Android Gradle Plugin‏ (AGP) על קובץ ה-build תירשם את כל המשימות הנדרשות ליצירת APK או ספריית Android. הפלאגין java-library מאפשר ליצור קובץ jar מקוד מקור של Java. יש פלאגינים דומים ל-Kotlin ולשפות אחרות, אבל פלאגינים אחרים נועדו להרחיב פלאגינים. לדוגמה, הפלאגין protobuf נועד להוסיף תמיכה ב-protobuf לפלאגינים קיימים כמו AGP או java-library.

ב-Gradle נוטים להשתמש במקובל במקום בהגדרות, כך שהתוספים יגיעו עם ערכי ברירת מחדל טובים מראש, אבל אפשר להגדיר את ה-build באופן נוסף באמצעות שפה ייעודית לדומיין (DSL) מצהירה. ה-DSL תוכנן כך שתוכלו לציין מה תרצו ליצור, ולא איך תרצו ליצור אותו. הלוגיקה שבתוספים מנהלת את האופן שבו זה יתבצע. ההגדרה הזו מצוינה בכמה קובצי build בפרויקט (ובפרויקטים המשניים).

קלט של משימות יכול להיות קבצים ודירקטורים, וגם מידע אחר שמקודד כסוגי Java (מספר שלם, מחרוזות או כיתות בהתאמה אישית). הפלט יכול להיות רק ספרייה או קובץ, כי צריך לכתוב אותו בדיסק. חיבור פלט של משימה לכניסה של משימה אחרת מקשר בין המשימות כך שצריך להריץ את אחת לפני השנייה.

Gradle תומך בכתיבת קוד שרירותי והצהרות על משימות בקובצי ה-build, אבל זה עלול להקשות על הכלים להבין את ה-build ועל התחזוקה שלו. לדוגמה, אפשר לכתוב בדיקות לקוד בתוך יישומי פלאגין, אבל לא בקובצי build. במקום זאת, כדאי להגביל את ההצהרות על משימות ועל לוגיקת ה-build לפלאגינים (שאתם או מישהו אחר מגדירים) ולהצהיר איך אתם רוצים להשתמש בלוגיקה הזו בקובצי ה-build.

מה קורה כשמפעילים build ב-Gradle?

ה-builds של Gradle פועלים בשלושה שלבים. בכל אחת מהשלבים האלה מתבצעת הפעלה של חלקים שונים של קוד שאתם מגדירים בקובצי ה-build.

  • הפעלה קובעת אילו פרויקטים ופרויקטים משניים נכללים ב-build, ומגדירה נתיבי classpath שמכילים את קובצי ה-build ואת הפלאגינים שהוחלו. השלב הזה מתמקד בקובץ הגדרות שבו מגדירים את הפרויקטים שרוצים לפתח ואת המיקומים שמהם רוצים לאחזר פלאגינים וספריות.
  • הגדרה: המערכת רושמת משימות לכל פרויקט ומפעילה את קובץ ה-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 = 34
    // ...

    defaultConfig {
        applicationId = "com.example.app"
        minSdk = 34
        // ...
    }
}

Groovy

android {
    namespace 'com.example.myapplication'
    compileSdk 34
    // ...

    defaultConfig {
        applicationId "com.example.myapplication"
        minSdk 24
        // ...
    }
}

מאחורי הקלעים, קוד ה-DSL נראה כך:

fun Project.android(configure: ApplicationExtension.() -> Unit) {
    ...
}

interface ApplicationExtension {
    var compileSdk: Int
    var namespace: String?

    val defaultConfig: DefaultConfig

    fun defaultConfig(configure: DefaultConfig.() -> Unit) {
        ...
    }
}

כל בלוק ב-DSL מיוצג על ידי פונקציה שמקבלת פונקציית lambda כדי להגדיר אותה, ומאפיין עם אותו שם כדי לגשת אליה. כך, הקוד בקובצי ה-build נראה יותר כמו מפרט נתונים.

יחסי תלות חיצוניים

מערכת ה-build של Maven הציגה מפרט, אחסון ומערכת ניהול של תלות. הספריות מאוחסנות במאגרים (שרתים או ספריות), עם מטא-נתונים שכוללים את הגרסה שלהן ואת יחסי התלות שלהן בספריות אחרות. אתם מציינים את המאגרים שבהם יתבצע החיפוש, את הגרסאות של יחסי התלות שבהם אתם רוצים להשתמש, ומערכת ה-build מורידת אותם במהלך ה-build.

פריטים של Maven מזוהים לפי שם הקבוצה (חברה, מפתח וכו'), שם הפריט (שם הספרייה) וגרסה של הפריט. בדרך כלל הוא מיוצג כ-group:artifact:version.

הגישה הזו משפרת באופן משמעותי את ניהול הגרסאות של ה-build. לעתים קרובות מאגרים כאלה נקראים 'מאגרי Maven', אבל זה לא קשור ל-Maven, אלא לאופן שבו הארטיפקטים נארזים ומתפרסמים. המאגרים והמטא-נתונים האלה שימשו לשימוש חוזר במספר מערכות build, כולל Gradle (ו-Gradle יכול לפרסם למאגרים האלה). במאגרים ציבוריים אפשר לשתף את התוכן עם כולם, ובמאגרים של החברה אפשר לשמור על יחסי התלות הפנימיים בתוך החברה.

אפשר גם לחלק את הפרויקט למודולים ולחלק אותו לפרויקטים משניים (שנקראים גם 'מודולים' ב-Android Studio), שאפשר להשתמש בהם גם כיחסי תלות. כל פרויקט משנה יוצר פלט (כמו קובצי jar) שפרויקטים משניים או הפרויקט ברמה העליונה יכולים לצרוך. כך תוכלו לבודד את החלקים שצריך לבנות מחדש, וכך לקצר את זמן ה-build, וגם להפריד טוב יותר את האחריות באפליקציה.

בהמשך המאמר נסביר בפירוט איך מציינים יחסי תלות בקטע הוספת יחסי תלות ל-build.

יצירת וריאנטים

כשיוצרים אפליקציה ל-Android, בדרך כלל רוצים ליצור כמה וריאנטים. הווריאציות מכילות קוד שונה או נוצרות עם אפשרויות שונות, והן מורכבות מסוגים שונים של גרסאות build וסוגים שונים של מוצרים.

סוגי גרסאות build משתנים בהתאם לאפשרויות ה-build שהוגדרו. כברירת מחדל, AGP מגדיר את סוגי ה-build 'release' ו-'debug', אבל אפשר לשנות אותם ולהוסיף עוד (למשל, לצורך עיבוד נתונים או בדיקה פנימית).

ב-debug build לא מתבצעת הפחתת קוד או ערפול של האפליקציה, כך שה-build מתבצע מהר יותר וכל הסמלים נשמרים כפי שהם. בנוסף, האפליקציה מסומנת כ'ניתנת לניפוי באגים', חותמת עליה באמצעות מפתח ניפוי באגים כללי ומאפשרת גישה לקבצים של האפליקציה שמותקנים במכשיר. כך תוכלו לבחון נתונים שמורים בקבצים ובמסדי נתונים בזמן הריצה של האפליקציה.

ב-build של גרסה מתבצעת אופטימיזציה של האפליקציה, חתימה עליה באמצעות מפתח הגרסה והגנה על קובצי האפליקציה המותקנים.

באמצעות סוגים של מוצרים, אפשר לשנות את המקור הכלול ואת הגרסאות של יחסי התלות של האפליקציה. לדוגמה, יכול להיות שתרצו ליצור גרסאות 'דמו' ו'מלאה' לאפליקציה, או גרסאות 'חינם' ו'בתשלום'. כותבים את המקור המשותף בספרייה 'ראשית' של קבוצת מקורות, ומחליפים או מוסיפים מקור בקבוצת מקורות ששמה זהה לשם הטעם.

AGP יוצר וריאציות לכל שילוב של סוג build וסוג מוצר. אם לא מגדירים טעמים, השמות של הווריאנטים יהיו שמות של סוגי ה-build. אם מגדירים את שניהם, שם הווריאנט יהיה <flavor><Buildtype>. לדוגמה, עם סוגי build‏ release ו-debug, וסוגים demo ו-full, מערכת AGP תיצור את הווריאציות הבאות:

  • demoRelease
  • demoDebug
  • fullRelease
  • fullDebug

השלבים הבאים

אחרי שסיפרנו על המושגים של ה-build, נסביר על מבנה ה-build של Android בפרויקט.