אלנתן דרך שיחת היכרות

MVVM או MVI: איזו ארכיטקטורה לבחור לאפליקציית אנדרואיד

אלנתן דרך, פורסם במקור באנגלית,

כשבונים ארכיטקטורה לאפליקציית אנדרואיד, שתי התבניות הפופולריות ביותר הן MVVM (Model-View-ViewModel) ו-MVI (Model-View-Intent). שתיהן נועדו לבנות את הקוד בצורה מסודרת, אבל כל אחת ניגשת אחרת לאינטראקציה עם הממשק ולניהול ה-State. במאמר הזה נעבור על ההבדלים ביניהן, עם דגש על ה-ViewModel, למה הוא חיוני, ולמה ההפרדה בין הממשק ללוגיקה העסקית היא אבן יסוד באפליקציה שצריכה לגדול.

מה זה ViewModel?

ה-ViewModel הוא רכיב מרכזי בארכיטקטורת MVVM באנדרואיד. הוא מתווך בין הממשק (View) לבין הלוגיקה העסקית ושכבת הנתונים (Model). הנה למה הוא כל כך חשוב:

  1. מחזיק את ה-State: ה-ViewModel שומר ומנהל את הנתונים של הממשק בצורה שמודעת למחזור החיים. הוא שורד שינויי תצורה, כמו סיבוב מסך או החלפת שפה, כך שהמשתמש לא מחכה לטעינה מחדש של הנתונים.
  2. מנתק את הממשק מהלוגיקה: כשהלוגיקה העסקית יושבת ב-ViewModel, הקוד נקי יותר, קל יותר לבדיקה ולתחזוקה. ה-ViewModel מכין את הנתונים לתצוגה ומגיב לפעולות המשתמש, וה-View אחראי רק על הציור.
  3. ריאקטיבי: בעזרת כלים כמו StateFlow, ה-ViewModel מזרים עדכונים ל-View, כך שהממשק תמיד מסונכרן עם הנתונים.

למה להפריד בין הממשק ללוגיקה העסקית?

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

MVVM מול MVI: ההבדלים המרכזיים

1. ניהול State

MVVM: ה-ViewModel מנהל את ה-State וחושף StateFlow שה-View מאזין לו. העדכונים זורמים בכיוון אחד: Model → ViewModel → View. הפשטות הזו הופכת את MVVM לאינטואיטיבית, אבל אם לא מנהלים את זה בזהירות, מצבי הממשק עלולים לצאת מסנכרון.

MVI: זרימת נתונים חד-כיוונית עם מקור אמת יחיד, שנקרא בדרך כלל State, ומייצג את כל מצב הממשק. הכוונות של המשתמש (Intents) נשלחות ל-Reducer, שמעדכן את ה-State, וה-State מוצג ב-View. הגישה הזו צפויה מאוד ומפחיתה באגים, אבל יכולה להוסיף מורכבות באפליקציות גדולות.

2. עדכוני ממשק

MVVM: עדכוני הממשק מונעים בדרך כלל מאירועים: ה-View מאזין לשינויים ב-LiveData או ב-StateFlow שה-ViewModel חושף. זה גמיש ועובד טוב עם ה-Views הקלאסיים של אנדרואיד.

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

3. מורכבות

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

MVI: נותנת יותר צפיות ושליטה ב-State, אבל מוסיפה קוד תבניתי (boilerplate) ועקומת למידה תלולה יותר. בממשקים עם הרבה אינטראקציה זה יכול להפוך למסורבל.

אז מה לבחור?

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

MVI מתאימה לאפליקציות עם ניהול State מורכב, כמו פיצ'רים בזמן אמת או טפסים מרובי שלבים, שבהן צפיות היא קריטית.

דוגמה: מונה ב-MVVM

// ViewModel
class CounterViewModel : ViewModel() {
    private val _counter = MutableStateFlow(0) // Mutable state holder
    val counter: StateFlow<Int> = _counter.asStateFlow() // Exposing as immutable StateFlow

    fun increment() {
        _counter.value += 1
    }

    fun decrement() {
        _counter.value -= 1
    }
}

// View (Activity or Fragment)
class CounterActivity : AppCompatActivity() {
    private lateinit var viewModel: CounterViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Collecting StateFlow updates in a lifecycle-aware manner
        lifecycleScope.launchWhenStarted {
            viewModel.counter.collect { count ->
                counterTextView.text = count.toString()
            }
        }

        incrementButton.setOnClickListener { viewModel.increment() }
        decrementButton.setOnClickListener { viewModel.decrement() }
    }
}

דוגמה: אותו מונה ב-MVI

// State
data class CounterState(val count: Int = 0)

sealed class CounterEvent {
    object Increment : CounterEvent()
    object Decrement : CounterEvent()
}

// ViewModel
class CounterViewModel : ViewModel() {
    private val _state = MutableStateFlow(CounterState()) // Mutable state holder
    val state: StateFlow<CounterState> = _state.asStateFlow() // Exposing as immutable StateFlow

    fun handleEvent(event: CounterEvent) {
        val current = _state.value
        _state.value = when (event) {
            is CounterEvent.Increment -> current.copy(count = current.count + 1)
            is CounterEvent.Decrement -> current.copy(count = current.count - 1)
        }
    }
}

// View (Activity or Fragment)
class CounterActivity : AppCompatActivity() {
    private lateinit var viewModel: CounterViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Collecting StateFlow updates in a lifecycle-aware manner
        lifecycleScope.launchWhenStarted {
            viewModel.state.collect { state ->
                counterTextView.text = state.count.toString()
            }
        }

        incrementButton.setOnClickListener { viewModel.handleEvent(CounterEvent.Increment) }
        decrementButton.setOnClickListener { viewModel.handleEvent(CounterEvent.Decrement) }
    }
}

לסיכום

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

המאמר תורגם לעברית מהמקור באנגלית.