המלצות לארכיטקטורה של Android

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

השיטות המומלצות שבהמשך מחולקות לפי נושאים. לכל המלצה יש עדיפות שמשקפת את מידת החשיבות שלה. רשימת העדיפויות היא:

  • מומלץ מאוד: כדאי ליישם את השיטה הזו, אלא אם היא סותרת באופן מהותי את הגישה שלכם.
  • מומלץ: סביר להניח שהשיטה הזו תשפר את האפליקציה.
  • אופציונלי: במקרים מסוימים, שימוש בשיטה הזו יכול לשפר את האפליקציה.

ארכיטקטורה שכבתית

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

המלצה תיאור
להשתמש בשכבת נתונים מוגדרת בבירור. שכבת הנתונים חושפת את נתוני האפליקציה לשאר האפליקציה ומכילה את רוב הלוגיקה העסקית של האפליקציה.
  • יוצרים מאגרי מידע גם אם הם מכילים רק מקור נתונים אחד.
  • באפליקציות קטנות, אפשר לבחור למקם סוגים של שכבת נתונים בdataחבילה או במודול.
משתמשים בשכבת ממשק משתמש מוגדרת בבירור. שכבת ממשק המשתמש מציגה את נתוני האפליקציה במסך ומשמשת כנקודת אינטראקציית המשתמש העיקרית. ‫Jetpack פיתוח נייטיב היא ערכת הכלים המודרנית המומלצת לבניית ממשק המשתמש של האפליקציה.
  • באפליקציות קטנות, אפשר לבחור למקם סוגים של שכבת נתונים בuiחבילה או במודול.
מידע נוסף על שיטות מומלצות לגבי שכבת ממשק המשתמש זמין במאמר בנושא שכבת ממשק המשתמש.
חשיפת נתוני אפליקציה משכבת הנתונים באמצעות מאגר.

מוודאים שרכיבים בשכבת ממשק המשתמש, כמו פונקציות Composable או ViewModels, לא מקיימים אינטראקציה ישירה עם מקור נתונים. דוגמאות למקורות נתונים:

  • מסדי נתונים, DataStore, ‏ SharedPreferences, ‏ Firebase APIs.
  • ספקי מיקום GPS.
  • ספקי נתונים של Bluetooth.
  • ספקי סטטוס של קישוריות לרשת.
משתמשים בקורוטינות וב-Flows. משתמשים בקורוטינות וב-Flows כדי לתקשר בין השכבות.

מידע נוסף על שיטות מומלצות לשימוש בקורוטינות זמין במאמר בנושא שיטות מומלצות לשימוש בקורוטינות ב-Android.

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

שכבת ממשק המשתמש

התפקיד של שכבת ממשק המשתמש הוא להציג את נתוני האפליקציה במסך ולשמש כנקודת האינטראקציה העיקרית של המשתמש. ריכזנו כאן כמה שיטות מומלצות לשכבת ממשק המשתמש:

המלצה תיאור
פועלים לפי ההנחיות בנושא זרימת נתונים חד-כיוונית (UDF). פועלים לפי העקרונות של זרימת נתונים חד-כיוונית (UDF), שבהם ViewModels חושפים את מצב ממשק המשתמש באמצעות תבנית הצופה ומקבלים פעולות מממשק המשתמש באמצעות קריאות לשיטות.
כדאי להשתמש ב-AAC ViewModels אם היתרונות שלהם רלוונטיים לאפליקציה שלכם. משתמשים ב-AAC ViewModels כדי לטפל בלוגיקה העסקית, ומאחזרים נתוני אפליקציה כדי לחשוף את מצב ממשק המשתמש לממשק המשתמש.

מידע נוסף על שיטות מומלצות לשימוש ב-ViewModel זמין במאמר המלצות לארכיטקטורה.

מידע נוסף על היתרונות של ViewModels זמין במאמר The ViewModel as a business logic state holder.

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

מידע נוסף על collectAsStateWithLifecycle

אל תשלחו אירועים מ-ViewModel לממשק המשתמש. מעבדים את האירוע באופן מיידי ב-ViewModel וגורמים לעדכון המצב עם התוצאה של הטיפול באירוע. מידע נוסף על אירועי ממשק משתמש זמין במאמר טיפול באירועים של ViewModel.
שימוש באפליקציה עם פעילות אחת. אם לאפליקציה יש יותר ממסך אחד, משתמשים בNavigation 3 כדי לנווט בין המסכים ולקשר עמוק לאפליקציה.
משתמשים ב-Jetpack פיתוח נייטיב. אפשר להשתמש ב-Jetpack פיתוח נייטיב כדי לבנות אפליקציות חדשות לטלפונים, לטאבלטים, למכשירים מתקפלים ול-Wear OS.

בקטע הקוד הבא אפשר לראות איך אוספים את מצב ממשק המשתמש באופן שמודע למחזור החיים:

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

ViewModels אחראים לספק את מצב ממשק המשתמש ולגשת לשכבת הנתונים. ריכזנו כאן כמה שיטות מומלצות לשימוש ב-ViewModels:

המלצה תיאור
חשוב לשמור על עצמאות של ViewModels ממחזור החיים של Android. ב-ViewModels, אל תחזיקו הפניה לסוג שקשור למחזור חיים. אל תעבירו את Activity, Context או Resources כתלות. אם משהו צריך Context ב-ViewModel, צריך לבדוק בקפידה אם הוא נמצא בשכבה הנכונה.
משתמשים בקורוטינות וב-Flows.

ה-ViewModel מתקשר עם שכבות הנתונים או הדומיין באמצעות:

  • תהליכי Kotlin לקבלת נתוני אפליקציה
  • פונקציות suspend לביצוע פעולות באמצעות viewModelScope
משתמשים ב-ViewModels ברמת המסך.

אל תשתמשו ב-ViewModels בחלקים של ממשק משתמש שאפשר לעשות בהם שימוש חוזר. כדאי להשתמש ב-ViewModels במקרים הבאים:

  • רכיבים הניתנים להרכבה ברמת המסך,
  • Activities/Fragments in Views,
  • יעדים או תרשימים כשמשתמשים ב-Jetpack Navigation.
משתמשים במחזיקי מצב פשוטים ברכיבי ממשק משתמש שאפשר לעשות בהם שימוש חוזר. כדי לטפל במורכבות ברכיבי ממשק משתמש לשימוש חוזר, משתמשים במחזיקי מצב פשוטים. כשעושים את זה, אפשר להעלות את המצב ולשלוט בו מבחוץ.
אל תשתמשו ב-AndroidViewModel. משתמשים במחלקת ViewModel ולא במחלקת AndroidViewModel. אל תשתמשו במחלקה Application ב-ViewModel. במקום זאת, מעבירים את התלות לממשק המשתמש או לשכבת הנתונים.
חשיפת מצב של ממשק משתמש. הגדרת ViewModels כך שיחשפו נתונים לממשק המשתמש באמצעות מאפיין יחיד שנקרא uiState. אם בממשק המשתמש מוצגים כמה נתונים לא קשורים, המכונה הווירטואלית יכולה לחשוף כמה מאפיינים של מצב ממשק המשתמש.
  • הגדרת uiState כStateFlow.
  • יוצרים את uiState באמצעות האופרטור stateIn עם המדיניות WhileSubscribed(5000) אם הנתונים מגיעים כזרם נתונים משכבות אחרות בהיררכיה. (ראו דוגמה לקוד).
  • במקרים פשוטים יותר שבהם אין זרמי נתונים שמגיעים משכבת הנתונים, אפשר להשתמש ב-MutableStateFlow שנחשף כ-StateFlow שלא ניתן לשינוי.
  • אתם יכולים לבחור להגדיר את ${Screen}UiState כסיווג נתונים שיכול להכיל נתונים, שגיאות ואותות טעינה. אם המצבים השונים הם בלעדיים, המחלקה יכולה להיות גם מחלקה אטומה.

בקטע הקוד הבא אפשר לראות איך חושפים את מצב ממשק המשתמש מ-ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

מחזור חיים

פועלים לפי השיטות המומלצות לעבודה עם מחזור החיים של פעילות:

המלצה תיאור
מומלץ להשתמש באפקטים שמתעדכנים בהתאם למחזור החיים ברכיבים קומפוזביליים במקום לבטל את ההגדרות של מחזורי החיים של Activity.

אל תבטלו את השיטות של מחזור החיים Activity, כמו onResume, כדי להריץ משימות שקשורות לממשק המשתמש. במקום זאת, כדאי להשתמש ב-LifecycleEffects של Compose או בהיקפי coroutine שמודעים למחזור החיים:

  • אפשר להשתמש ב-LifecycleStartEffect כדי לבצע עבודה סינכרונית כשהפעילות מתחילה ומסתיימת.
  • אפשר להשתמש ב-LifecycleResumeEffect כדי לבצע עבודה סינכרונית כשהפעילות מתחדשת ומושהית.
  • משתמשים ב-repeatOnLifecycle כדי לבצע עבודה אסינכרונית בתגובה לאירועים במחזור החיים.
  • איסוף נתונים אסינכרוניים מ-Flows באמצעות collectAsStateWithLifecycle.

בקטע הקוד הבא מוסבר איך לבצע פעולות בהינתן מצב מחזור חיים מסוים:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

טיפול ביחסי תלות

כדאי לפעול לפי השיטות המומלצות לניהול תלויות בין רכיבים:

המלצה תיאור
להשתמש בהזרקת תלות. מומלץ להשתמש בשיטות מומלצות של הזרקת תלות, בעיקר הזרקת בנאי כשזה אפשרי.
היקף לכל רכיב לפי הצורך. הגדרת היקף למאגר תלות כשסוג הנתונים מכיל נתונים שניתנים לשינוי וצריך לשתף אותם, או כשסוג הנתונים יקר לאתחול ונעשה בו שימוש נרחב באפליקציה.
שימוש ב-Hilt. באפליקציות פשוטות, אפשר להשתמש ב-Hilt או בהחדרת תלות ידנית. כדאי להשתמש ב-Hilt אם הפרויקט שלכם מורכב מספיק – לדוגמה, אם הוא כולל את אחד מהרכיבים הבאים:
  • כמה מסכים עם ViewModels
  • שימוש ב-WorkManager
  • יש ViewModels בהיקף של מקבץ פעילויות קודמות (back stack) של הניווט

בדיקה

ריכזנו כאן כמה שיטות מומלצות לבדיקות:

המלצה תיאור
מה כדאי לבדוק

אלא אם הפרויקט פשוט כמו אפליקציית 'שלום עולם', כדאי לבדוק אותו. לפחות את הפרטים הבאים:

  • בדיקות יחידה (unit testing) ל-ViewModels, כולל Flows
  • בדיקות יחידה של ישויות בשכבת הנתונים – כלומר, מאגרי מידע ומקורות נתונים
  • בדיקות של ניווט בממשק המשתמש שמועילות כבדיקות רגרסיה ב-CI
עדיף להשתמש ב-fakes במקום ב-mocks. מידע נוסף על השימוש ב-fakes מופיע במאמר שימוש ב-test doubles ב-Android.
בדיקת StateFlows. כשבודקים את StateFlow, מבצעים את הפעולות הבאות:

מידע נוסף זמין במאמרים מה כדאי לבדוק ב-Android ובדיקת פריסת פיתוח נייטיב.

מודלים

כדאי לפעול לפי השיטות המומלצות הבאות כשמפתחים מודלים באפליקציות:

המלצה תיאור
באפליקציות מורכבות, כדאי ליצור מודל לכל שכבה.

באפליקציות מורכבות, כדאי ליצור מודלים חדשים בשכבות או ברכיבים שונים כשזה הגיוני. הנה כמה דוגמאות:

  • מקור נתונים מרוחק יכול למפות את המודל שהוא מקבל דרך הרשת למחלקה פשוטה יותר עם הנתונים שהאפליקציה צריכה בלבד.
  • מאגרי מידע יכולים למפות מודלים של DAO למחלקות נתונים פשוטות יותר, עם המידע ששכבת ממשק המשתמש צריכה בלבד.
  • ‫ViewModel יכול לכלול מודלים של שכבת הנתונים במחלקות UiState.

מוסכמות למתן שמות

כשנותנים שם לבסיס הקוד, כדאי לפעול לפי השיטות המומלצות הבאות:

המלצה תיאור
שיטות מתן שמות.
אופציונלי
משתמשים בצירופי פועל כדי לתת שמות לשיטות – לדוגמה, makePayment().
מתן שמות למאפיינים.
אופציונלי
משתמשים בצירופי שם עצם כדי לתת שמות למאפיינים – לדוגמה, inProgressTopicSelection.
מתן שמות למקורות נתונים.
אופציונלי
כשסיווג חושף זרם Flow או כל זרם אחר, מוסכמת מתן השמות היא get{model}Stream. לדוגמה, getAuthorStream(): Flow<Author>. אם הפונקציה מחזירה רשימה של מודלים, צריך להשתמש בשם המודל ברבים: getAuthorsStream(): Flow<List<Author>>.
מתן שמות להטמעות של ממשקים.
אופציונלי
חשוב להשתמש בשמות משמעותיים להטמעות של ממשקים. אם לא מוצאים שם טוב יותר, משתמשים ב-Default כקידומת. לדוגמה, בממשק NewsRepository יכול להיות שיהיה לכם OfflineFirstNewsRepository או InMemoryNewsRepository. אם לא מוצאים שם טוב, אפשר להשתמש ב-DefaultNewsRepository. צריך להוסיף את הקידומת Fake להטמעות מזויפות, כמו בדוגמה FakeAuthorsRepository.

מקורות מידע נוספים

למידע נוסף על ארכיטקטורת Android, אפשר להיעזר במקורות המידע הנוספים הבאים:

מסמכים

צפייה בתוכן