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

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

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

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

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

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

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

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

  • מסדי נתונים, DataStore, SharedPreferences ו-Firebase APIs.
  • ספקי מיקום לפי GPS.
  • ספקי נתונים של Bluetooth.
  • ספק סטטוס קישוריות לרשת.
שימוש בפונקציות רפיטיביות (coroutines) ובתהליכים (flows). משתמשים בפונקציות רפליקות (coroutines) ובזרמים (flows) כדי לתקשר בין השכבות.

שיטות מומלצות נוספות לשימוש ב-coroutines

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

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

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

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

כאן אפשר למצוא עוד שיטות מומלצות ליצירת מודלים.

כאן מפורטות ההטבות של ViewModels.

שימוש באוסף המצבים של ממשק המשתמש תוך התייחסות למחזור החיים. איסוף המצב של ממשק המשתמש ממשק המשתמש באמצעות ה-builder המתאים של פונקציית הריצה המשותפת (coroutine) עם תמיכה במחזור חיים: repeatOnLifecycle במערכת View ו-collectAsStateWithLifecycle ב-Jetpack Compose.

מידע נוסף על repeatOnLifecycle

מידע נוסף על collectAsStateWithLifecycle.

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

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

צפיות

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

פיתוח נייטיב

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

ViewModel

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

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

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

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

אל תשתמשו ב-ViewModels בחלקים של ממשק המשתמש שניתנים לשימוש חוזר. מומלץ להשתמש ב-ViewModels במקרים הבאים:

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

    // ...
}

מחזור חיים

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

המלצה תיאור
אל תשנו את שיטות מחזור החיים ב-Activities או ב-Fragments. אין לשנות את שיטות מחזור החיים, כמו onResume, ב-Activities או ב-Fragments. במקום זאת, צריך להשתמש ב-LifecycleObserver. אם האפליקציה צריכה לבצע פעולה כשמחזור החיים מגיע ל-Lifecycle.State מסוים, צריך להשתמש ב-API של repeatOnLifecycle.

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

צפיות

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

פיתוח נייטיב

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

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

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

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

בדיקה

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

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

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

  • מודלים של תצוגה מפורטת לבדיקת יחידה, כולל Flows.
  • ישויות בשכבת הנתונים של בדיקת היחידה. כלומר, מאגרים ומקורות נתונים.
  • בדיקות ניווט בממשק המשתמש ששימושיות כבדיקות רגרסיה ב-CI.
עדיף להשתמש בזיופים על פני חיקויים. מידע נוסף זמין במאמר שימוש במכפילים של בדיקה במסמכי התיעוד של Android.
בדיקת StateFlows. כשבודקים את StateFlow:

מידע נוסף זמין במדריך מה צריך לבדוק ב-Android DAC.

דגמים

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

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

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

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

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

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

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