برنامه‌نویسی اندروید

آموزش معماری MVI در اندروید به صورت گام به گام

آموزش معماری MVI در اندروید

الگوهای معماری در اندروید روز به روز در حال پیشرفت هستند. همانطور که ما برنامه ها را توسعه می دهیم ، با چالش ها و مشکلات جدیدی روبرو می شویم. با ادامه حل چالش های مشابه، الگوهای جدیدی کشف خواهند شد. به عنوان توسعه دهندگان اندروید ،  MVC ، MVP و MVVM را به عنوان متداول ترین الگوهای مورد استفاده داریم.

همه آنها از یک رویکرد برنامه نویسی ضروری استفاده می کنند. با این رویکرد، حتی اگر بیشتر چالش‌های ما حل شود ، ما همچنان با چالش‌هایی در رابطه با ایمنی thread ها ، حفظ حالت‌های برنامه مواجه هستیم . با این کار، بیایید ببینیم الگوی معماری MVI چیست، چگونه این چالش ها را حل می کند، و چگونه می توان با MVI شروع به کار کرد.

با بزرگ‌تر شدن پروژه‌های اندرویدی، مدیریت State و جریان داده‌ها به یکی از مهم‌ترین چالش‌های توسعه تبدیل می‌شود. معماری‌های مختلفی مانند:

  • MVC
  • MVP
  • MVVM

برای حل این مشکلات معرفی شدند، اما در اپلیکیشن‌های مدرن، مخصوصاً پروژه‌های مبتنی بر Reactive Programming و Jetpack Compose، معماری MVI محبوبیت زیادی پیدا کرده است.

MVI مخفف:

Model - View - Intent

است.

این معماری تمرکز ویژه‌ای روی:

  • مدیریت State
  • جریان داده یک‌طرفه
  • کاهش Bug
  • Predictable UI
  • Reactive UI

دارد.

بیایید ببینیم که نقش هر یک از اجزای MVI چیست.

  • Model : برخلاف سایر الگوها، در مدل MVI وضعیت رابط کاربری را نشان می دهد. به عنوان مثال، UI ممکن است حالت های مختلفی مانند بارگیری داده، تغییر در رابط کاربری با اقدامات کاربر، خطاها، وضعیت های موقعیت فعلی صفحه نمایش کاربر داشته باشد. هر حالت شبیه به شی در مدل ذخیره می شود.
  • View : View در MVI رابط ما است که می تواند در Activities و Fragment ها پیاده سازی شود. این به معنای داشتن یک ظرف است که می تواند حالت های مختلف مدل را بپذیرد و آن را به عنوان یک رابط کاربری نمایش دهد. آنها از مقاصد قابل مشاهده استفاده می کنند برای پاسخ به اقدامات کاربر.
  • Intent : این Intent آنطور که اندروید قبلاً معرفی کرده بود نیست. نتیجه اقدامات کاربر به عنوان یک مقدار ورودی به Intent ارسال می شود. به نوبه خود، می توانیم بگوییم که مدل هایی را به عنوان ورودی به Intent ها ارسال خواهیم کرد که می توانند آن را از طریق Views بارگیری کنند.

معماری MVI

MVI چیست؟

در معماری MVI تمام تغییرات رابط کاربری از طریق یک مسیر مشخص انجام می‌شوند.

کاربر یک Intent ارسال می‌کند، سپس سیستم آن را پردازش کرده و در نهایت یک State جدید تولید می‌شود.

ساختار کلی:

User Action
    ↓
Intent
    ↓
ViewModel
    ↓
New State
    ↓
UI Update
ایجاد یک پروژه با معماری MVI

تفاوت MVI با MVVM

معماری MVVM

در MVVM معمولاً:

  • ViewModel چندین LiveData یا StateFlow دارد
  • State ممکن است پراکنده باشد
  • مدیریت وضعیت پیچیده‌تر می‌شود

معماری MVI

در MVI:

  • فقط یک State اصلی وجود دارد
  • تمام تغییرات از طریق Intent انجام می‌شود
  • Data Flow یک‌طرفه است
  • Debug و Testing راحت‌تر است

ساختار پروژه

برای پروژه ، ما یک نسخه ساده از MVI را پیاده سازی می کنیم و پکیج های ما در پروژه به شکل زیر خواهد بود.

ساختار پروژه mvi

مزایای معماری MVI

1. مدیریت بهتر State

تمام وضعیت صفحه داخل یک State نگهداری می‌شود.

2. جریان داده قابل پیش‌بینی

هیچ تغییر مستقیمی روی UI وجود ندارد.

3. مناسب Jetpack Compose

معماری MVI با Compose هماهنگی فوق‌العاده‌ای دارد.

4. تست‌پذیری بالا

به‌دلیل Stateless بودن UI، تست بسیار ساده‌تر می‌شود.

معایب MVI

1. Boilerplate بیشتر

نیاز به تعریف:

  • Intent
  • State
  • Reducer

دارد.

2. پیچیدگی اولیه

برای پروژه‌های کوچک ممکن است بیش‌ازحد پیچیده باشد.

اجزای اصلی MVI

معماری MVI معمولاً از بخش‌های زیر تشکیل می‌شود:

Intent
State
ViewModel
Reducer
View
Repository

مفهوم Intent

Intent نشان‌دهنده عملی است که کاربر انجام می‌دهد.

مثال‌ها:

  • کلیک روی دکمه
  • جستجو
  • Refresh
  • ارسال فرم

تعریف Intent

sealed class UserIntent {

    data object LoadUsers : UserIntent()

    data class Search(
        val query: String
    ) : UserIntent()
}

مفهوم State

State نمایانگر وضعیت کامل UI است.

تعریف State

data class UserState(

    val isLoading: Boolean = false,

    val users: List<String> = emptyList(),

    val error: String? = null
)

مفهوم Reducer

Reducer وظیفه تولید State جدید را دارد.

ساخت پروژه نمونه

در این آموزش یک صفحه ساده می‌سازیم که:

  • کاربران را دریافت می‌کند
  • Loading نمایش می‌دهد
  • خطا را مدیریت می‌کند

Dependency های موردنیاز

build.gradle

dependencies {

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0"

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"

    implementation "androidx.compose.ui:ui"

    implementation "androidx.compose.material3:material3"
}

ساخت Intent

sealed class UserIntent {

    data object LoadUsers : UserIntent()

    data class SearchUser(
        val query: String
    ) : UserIntent()
}

ساخت State

data class UserState(

    val loading: Boolean = false,

    val users: List<String> = emptyList(),

    val error: String? = null
)

ساخت Repository

class UserRepository {

    suspend fun getUsers(): List<String> {

        delay(2000)

        return listOf(
            "Ali",
            "Sara",
            "Maryam",
            "Reza"
        )
    }
}

ساخت ViewModel در MVI

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _state =
        MutableStateFlow(UserState())

    val state: StateFlow<UserState> =
        _state

    fun onIntent(intent: UserIntent) {

        when(intent) {

            is UserIntent.LoadUsers -> {
                loadUsers()
            }

            is UserIntent.SearchUser -> {

            }
        }
    }

    private fun loadUsers() {

        viewModelScope.launch {

            _state.value =
                _state.value.copy(
                    loading = true
                )

            try {

                val users =
                    repository.getUsers()

                _state.value =
                    _state.value.copy(
                        loading = false,
                        users = users
                    )

            } catch (e: Exception) {

                _state.value =
                    _state.value.copy(
                        loading = false,
                        error = e.message
                    )
            }
        }
    }
}

توضیح جریان داده

مراحل اجرای برنامه:

User Click
    ↓
Intent ارسال می‌شود
    ↓
ViewModel پردازش می‌کند
    ↓
Repository داده را دریافت می‌کند
    ↓
State جدید ساخته می‌شود
    ↓
UI آپدیت می‌شود

اتصال UI به ViewModel

صفحه Compose

@Composable
fun UserScreen(
    viewModel: UserViewModel
) {

    val state by
        viewModel.state.collectAsState()

    LaunchedEffect(Unit) {

        viewModel.onIntent(
            UserIntent.LoadUsers
        )
    }

    Box(
        modifier = Modifier.fillMaxSize()
    ) {

        when {

            state.loading -> {

                CircularProgressIndicator()
            }

            state.error != null -> {

                Text(
                    text = state.error!!
                )
            }

            else -> {

                LazyColumn {

                    items(state.users) {

                        Text(text = it)
                    }
                }
            }
        }
    }
}

چرا State واحد مهم است؟

در MVI کل وضعیت صفحه داخل یک State ذخیره می‌شود.

مثال:

UserState(
    loading = false,
    users = list,
    error = null
)

این موضوع باعث می‌شود:

  • وضعیت UI قابل پیش‌بینی باشد
  • Debug راحت‌تر شود
  • Race Condition کمتر شود

مدیریت Event ها در MVI

گاهی لازم است Event هایی مثل:

  • Toast
  • Navigation
  • Snackbar

مدیریت شوند.

برای این کار معمولاً از SharedFlow استفاده می‌شود.

نمونه Event

sealed class UserEvent {

    data class ShowToast(
        val message: String
    ) : UserEvent()
}

SharedFlow در ViewModel

private val _event =
    MutableSharedFlow<UserEvent>()

val event = _event.asSharedFlow()

ارسال Event

_event.emit(
    UserEvent.ShowToast(
        "Users Loaded"
    )
)

دریافت Event در Compose

LaunchedEffect(Unit) {

    viewModel.event.collect {

        when(it) {

            is UserEvent.ShowToast -> {

            }
        }
    }
}

استفاده از Reducer حرفه‌ای

در پروژه‌های بزرگ بهتر است Reducer جداگانه داشته باشید.

نمونه Reducer

fun reduce(
    oldState: UserState,
    result: Result
): UserState {

    return when(result) {

        is Result.Loading -> {
            oldState.copy(
                loading = true
            )
        }

        is Result.Success -> {
            oldState.copy(
                loading = false,
                users = result.users
            )
        }

        is Result.Error -> {
            oldState.copy(
                loading = false,
                error = result.message
            )
        }
    }
}

بهترین ساختار پروژه MVI

presentation/
    intent/
    state/
    event/
    reducer/
    viewmodel/

domain/
data/
repository/
network/

تفاوت Intent و Event

Intent

از UI به ViewModel ارسال می‌شود.

مثال:

Button Click
Search
Refresh

Event

از ViewModel به UI ارسال می‌شود.

مثال:

Toast
Snackbar
Navigation

استفاده از StateFlow در MVI

در پروژه‌های مدرن بهتر است از:

StateFlow

استفاده کنید.

زیرا:

  • Reactive است
  • با Compose هماهنگ است
  • Lifecycle Aware است

چرا MVI برای Compose عالی است؟

Jetpack Compose بر اساس State کار می‌کند.

در MVI نیز:

State → UI

بنابراین هماهنگی بسیار بالایی وجود دارد.

مدیریت Loading در MVI

_state.value =
    _state.value.copy(
        loading = true
    )

مدیریت خطا

_state.value =
    _state.value.copy(
        error = e.message
    )

تست‌پذیری MVI

در MVI تست بسیار راحت‌تر است.

مثال:

assertEquals(
    true,
    state.loading
)

استفاده از Clean Architecture همراه MVI

ترکیب:

MVI + Clean Architecture

یکی از بهترین معماری‌های مدرن اندروید است.

تفاوت MVI و Redux

معماری MVI الهام گرفته از Redux است.

شباهت‌ها:

  • Single State
  • Reducer
  • Immutable State

Immutable State چیست؟

در MVI نباید State را مستقیم تغییر دهید.

اشتباه:

state.loading = true

صحیح:

state.copy(
    loading = true
)

بهترین روش‌های پیاده‌سازی MVI

  1. State کوچک نگه دارید
  2. Event و State را جدا کنید
  3. Reducer مستقل داشته باشید
  4. از Immutable Data استفاده کنید
  5. منطق Business را داخل Repository قرار دهید

اشتباهات رایج در MVI

  1.  قرار دادن Event داخل State
  2. داشتن چند State مستقل
  3.  تغییر مستقیم State
  4.  ارسال Intent از Repository

مثال کامل چرخه MVI

User Click Search
      ↓
SearchIntent
      ↓
ViewModel
      ↓
Repository API Call
      ↓
Reducer
      ↓
New State
      ↓
Compose Recompose

چه زمانی از MVI استفاده کنیم؟

MVI مناسب است برای:

  • پروژه‌های بزرگ
  • اپلیکیشن‌های Compose
  • سیستم‌های Reactive
  • پروژه‌های چند State
  • اپلیکیشن‌های RealTime

چه زمانی مناسب نیست؟

برای:

  • پروژه‌های بسیار کوچک
  • صفحات ساده
  • Prototype ها

ممکن است MVVM ساده‌تر باشد.

کتابخانه‌های محبوب MVI

  • Orbit MVI
  • Mavericks
  • Mobius
  • MVIKotlin

نمونه Orbit MVI

class UserViewModel : ContainerHost<
    UserState,
    UserEvent
>, ViewModel() {

}

جمع‌بندی

در این مقاله با معماری MVI در اندروید آشنا شدیم.

همچنین یاد گرفتیم:

  • Intent چیست
  • State چیست
  • Reducer چگونه کار می‌کند
  • جریان داده در MVI
  • اتصال MVI به Compose
  • مدیریت Event
  • استفاده از StateFlow
  • بهترین روش‌های معماری

معماری MVI یکی از قدرتمندترین معماری‌های مدرن اندروید است و مخصوصاً در پروژه‌های مبتنی بر Jetpack Compose عملکرد فوق‌العاده‌ای دارد.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *