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

אפליקציות ל-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. במקום זאת, צריך להגביל את הלוגיקה של הבנייה ואת ההצהרות של המשימות לפלאגינים (שאתם או מישהו אחר מגדירים), ולהצהיר איך רוצים להשתמש בלוגיקה הזו בקובצי הבנייה.

מה קורה כשמריצים Gradle build?

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

  • האתחול קובע אילו פרויקטים ותתי-פרויקטים נכללים ב-build, ומגדיר את נתיבי המחלקות שמכילים את קובצי ה-build והתוספים שהוחלו. בשלב הזה מתמקדים בקובץ הגדרות שבו מצהירים על הפרויקטים שרוצים ליצור ועל המיקומים שמהם רוצים לאחזר פלאגינים וספריות.
  • Configuration (הגדרה) רושם משימות לכל פרויקט ומבצע את קובץ ה-build כדי להחיל את מפרט ה-build של המשתמש. חשוב להבין שלקוד ההגדרה לא תהיה גישה לנתונים או לקבצים שנוצרו במהלך ההפעלה.
  • ההרצה מבצעת את ה'בנייה' בפועל של האפליקציה. הפלט של ההגדרה הוא גרף מכוון ללא מעגלים (DAG) של משימות, שמייצג את כל שלבי הבנייה הנדרשים שהמשתמש ביקש (המשימות שסופקו בשורת הפקודה או כברירות מחדל בקובצי הבנייה). הגרף הזה מייצג את הקשר בין המשימות, בין אם הוא מוגדר במפורש בהצהרה של המשימה או שהוא מבוסס על הקלט והפלט שלה. אם למשימה יש קלט שהוא הפלט של משימה אחרת, היא חייבת לפעול אחרי המשימה האחרת. בשלב הזה, משימות לא עדכניות מופעלות לפי הסדר שמוגדר בתרשים. אם נתוני הקלט של משימה לא השתנו מאז ההפעלה האחרונה שלה, 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) שאפשר להשתמש בהם בפרויקטים משניים אחרים או בפרויקט ברמה העליונה. השימוש ב-Gradle יכול לקצר את זמן הבנייה, כי הוא מאפשר לבודד את החלקים שצריך לבנות מחדש, וגם להפריד טוב יותר בין האחריות השונות באפליקציה.

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

וריאנטים של build

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

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

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

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

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

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

  • demoRelease
  • demoDebug
  • fullRelease
  • fullDebug

השלבים הבאים

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