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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

מידע נוסף על היתרונות של ViewModels זמין במאמר The ViewModel as a business logic state holder (ה-ViewModel כמאגר מצב של לוגיקה עסקית).

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

מידע נוסף על collectAsStateWithLifecycle

אל תשלחו אירועים מ-ViewModel לממשק המשתמש. מעבדים את האירוע באופן מיידי ב-ViewModel וגורמים לעדכון המצב עם התוצאה של הטיפול באירוע. מידע נוסף על אירועים בממשק המשתמש זמין במאמר טיפול באירועים של ViewModel.
שימוש באפליקציה עם פעילות אחת. אם לאפליקציה יש יותר ממסך אחד, משתמשים בNavigation 3 כדי לנווט בין המסכים ולקשר עמוק לאפליקציה.
שימוש ב-Jetpack Compose. אפשר להשתמש ב-Jetpack Compose כדי לבנות אפליקציות חדשות לטלפונים, לטאבלטים, למכשירים מתקפלים ול-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, צריך לבדוק בקפידה אם זה המקום הנכון.
שימוש בקורוטינות וברצפי פעולות.

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

  • Kotlin flows for receiving application data
  • פונקציות suspend לביצוע פעולות באמצעות viewModelScope
שימוש ב-ViewModels ברמת המסך.

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

  • רכיבים הניתנים להרכבה ברמת המסך
  • יעדים או גרפים כשמשתמשים ב-Jetpack Navigation

כדי להגדיר ViewModel עבור קומפוזיציות מורכבות יותר, או כאלה עם התנהגות דינמית שמבוססת על מצב, משתמשים ב-rememberViewModelStoreOwner() כדי להגדיר את ה-ViewModel ישירות לאתר הקריאה של הקומפוזיציה.

משתמשים במחזיקי מצב פשוטים ברכיבי ממשק משתמש שאפשר לעשות בהם שימוש חוזר. כדי לטפל במורכבות ברכיבי ממשק משתמש שאפשר להשתמש בהם שוב, כדאי להשתמש במחלקות פשוטות של מחזיקי מצב. כשעושים את זה, אפשר להעלות את המצב ולשלוט בו מבחוץ.
אל תשתמשו ב-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
            )

    // ...
}

מחזור חיים

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

המלצה תיאור
מומלץ להשתמש באפקטים שמודעים למחזור החיים ברכיבים קומפוזביליים במקום להחליף את הקריאות החוזרות (callback) של מחזור החיים של 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
  • בדיקות ביצועים באמצעות Macrobenchmark למדידת הפעלת האפליקציה ועיבוד ממשק המשתמש
עדיף להשתמש ב-fakes במקום ב-mocks. מידע נוסף על השימוש ב-fakes זמין במאמר שימוש ב-test doubles ב-Android.
בדיקת StateFlows. כשבודקים את StateFlow, מבצעים את הפעולות הבאות:

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

ביצועים

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

המלצה תיאור
כדאי להשתמש ב-R8 כדי לצמצם את גודל האפליקציה ולבצע אופטימיזציה. כדי להסיר קוד שלא נמצא בשימוש, לעשות ערפול קוד למחלקות ולשכתב קוד כדי לשפר את הביצועים בזמן הריצה, צריך להפעיל את R8 במצב מלא ב<b>תצורת build</b> שלכם.
הטמעה של פרופילים של Baseline ופרופילים להפעלה. יוצרים פרופילי Baseline ופרופילים של הפעלה כדי לבצע קומפילציה מראש של נתיבי קוד קריטיים. הפעולה הזו מפחיתה באופן משמעותי את זמן האחזור של תהליך ההפעלה (TTID ו-TTFD) ומצמצמת את הבעיות בממשק (jank) ואת שגיאות ה-ANR.
בודקים את הביצועים באמצעות Macrobenchmark. כדי לבדוק את הביצועים של האפליקציה ולמנוע רגרסיות, אפשר להשתמש בספריית Macrobenchmark.

מודלים

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

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

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

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

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

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

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

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

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

מאמרי עזרה

צפייה בתוכן