سبد خرید

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

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

معماری MVI رو میشه جز خاص ترین معماری ها برای توسعه نرم افزار در اندروید بحساب آورد.

این معماری جز قوی ترین معماری های اندروید برای مدیریت کردن هر چه بهتر State یا وضعیت هستش.

اندروید معماری های مختلفی داره، مثل :

  • MVC (Model View Controller)
  • MVP (Model View Presenter)
  • MVVM (Model View ViewModel)
  • MVI (Model View Intent)

معماری MVC که جز اولین معماری های اندروید بحساب میاد و این روزها دیگه کاربردی نداره و تقریبا 5 سالی هستش که دیگه کسی ازش برای برنامه نویسی اندروید استفاده نمیکنه.

ولی در عوض 3 معماری بعدی جز معماری های پرطرفدار برای برنامه نویسی اندروید هستش.

بین برنامه نویس های ایرانی MVVM جایگاه ویژه ای داره ولی زمانی که بخوای توی شرکت های خیلی بزرگ و تراز اول کار کنی قطعا دونستن MVI یک مزیت بزرگ برای شما بحساب میاد.

اجزای تشکیل دهنده معماری MVI

معماری MVI از کلمات Model View Intent تشکیل شده.

Model: مدل که اولین کلمه مربوط به این معماری هستش، برخلاف معماری های رایج دیگه از جمله MVP و MVVP فقط شامل Data class (کلاسی که مدل دیتاهای ما رو میسازه) نمیشه.

در این بخش ما State (وضعیت) های مختلف برنامه رو کنترل میکنیم.

وضعیت هایی مانند : خالی بودن اطلاعات، نمایش Loading (برای نمایش بارگذاری مطالب)، نمایش مطالب، نمایش خطا و… میشه.

(البته این نکته رو هم باید بگم که فقط و فقط شامل این موارد نیست و این موارد صرفا مثال هایی جهت آشنایی هستن)

View: این بخش شامل صفحاتی هستش که کاربر باهاشون کار میکنه مانند Activity یا Fragment.

درواقع ما نتیجه نهایی رو به کاربر در این بخش نشون میدیم، مثلا اطلاعات RecyclerView رو پُر میکنیم، جزئیات یک محصول رو نشون میدیم و یا…

Intent: این بخش هم که آخرین بخش MVI هستش، همون عملیات و یا Action هایی هستش که قصد داریم انجام بدیم.

مثلا : میخوایم اطلاعات یک محصول رو نشون بدیم، لیست محصولات یک دسته رو نشون بدیم، لیست اطلاعات ذخیره شده در دیتابیس رو نشون بدیم و…

(البته این نکته رو هم باید بگم که فقط و فقط شامل این موارد نیست و این موارد صرفا مثال هایی جهت آشنایی هستن)

ساختار معماری MVI در اندروید

این معماری از کلمات Model View Intent ساخته شده که در شکل زیر هم دیاگرام مربوط به این معماری رو برای شما قرار دادم.

ابتدا این شکل رو با دقت بررسی کن، بعد در ادامه به طور کامل توضیح میدم.

mvi_android_structure_diagram-معماری-mvi

اگر به شکل بالا خوب توجه کنی هر کدوم از اجزای اصلی تشکیل دهنده این معماری به نحوی باهم در ارتباط هستن.

یکی از مهمترین نکاتی که در تصویر بالا هست و باید بهش خوب دقت کنی اینه که این ارتباط ها کاملا یکطرفه هستن.

یعنی جهت این فلش ها فقط به یک سمت هست.

همین مورد جز یکی از اصلی ترین مزیت های معماری MVI بحساب میاد.

زمانی که بخش های مختلف به صورت یکطرفه باهم در ارتباط هستن، نشون دهنده این هست که هرکدوم از بخش ها به محض انجام دادن کار مخصوص به خودشون اون اطلاعات رو در اختیار بخش دیگری قرار میدن.

اینطوری نگه داری، توسعه نرم افزار و خطایابی هم خیلی راحتتر میشه.

در این معماری اول از همه کاربر درخواست مورد نظرش رو به Intent میگه.

منظور ما از Intent اصلا Intent خود اندروید برای کار با صفحات مختلف مثل Activity یا … نیستش.
بلکه کلاسی هستش که کدهای خودمون رو داخلش قرار میدیم.

در اینجا درخواست های کاربر یا میتونن شامل مواردی باشن که خودش میخواد انجام بده، مثلا به محض کلیک کردن روی یک دکمه ای یک کار خاصی اتفاق بیافته.

یا اینکه میتونه شامل مواردی باشه که ما میخوایم اتفاق بیافته، مثلا به محض اینکه اپلیکیشن اجرا شد لیست محصولات رو نشون بده.

چرا معماری MVI مهمه ؟

همونطور که بالاتر بهش اشاره کردم MVI یکی از مهمترین معماری ها برای شرکت های بزرگ هستش.

زمانی که State یا وضعیت های مختلف برنامه براتون مهمه این معماری میتونه حرف اول رو بزنه.

چون تمامی ارتباط ها به صورت یکطرفه هستن پس مدیریت و کنترل کردن وضعیت ها خیلی بهتر انجام میگیرن.

این معماری در نگاه اول شاید کمی دشوار بنظر بیاد، ولی اگه خوب یادبگیرید میبیند که چه قدرت زیادی داره.

MVI چطور کار میکنه؟

  1. اول از همه عملیاتی که مدنظرمون هست رو در Intent پیاده سازی میکنیم.
  2. سپس برای اون عملیات، وضعیت (State) های مختلفی رو تعریف میکنیم.
  3. درنهایت در Activity یا Fragment کدهای مربوط به Intent و State رو پیاده سازی میکنیم.

عملکرد بالا رو در قالب یک مثال ساده توضیح میدم که درکش راحتتر باشه.

فرض کن که میخوایم یکسری اطلاعات رو از سرور بگیریم، پس سناریو اینطوریه که قراره لیست محصولات رو در صفحه اصلی نشون بدیم.

اول از همه میایم عملیات یا اکشن مورد نیاز رو در کلاس Intent تعریف میکنیم، عملیاتی که توی این سناریو داریم مربوط به بارگذاری لیست محصولات از سمت سرور هستش.

سپس به واسطه اون عملیات میایم وضعیت های مختلف رو در کلاس State تعریف میکنیم، وضعیت هایی مانند :

  • نمایش لودینگ
  • نمایش خطا
  • نمایش خالی بودن
  • نمایش اطلاعات بارگذاری شده

درنهایت توی Activity یا Fragment میایم اول از همه Intent رو صدا میزنیم که با سرور ارتباط برقرار کنیم و درخواست رو ارسال کنیم و سپس وضعیت های مختلف کلاس State رو مدیریت میکنیم.

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

سناریو بالا رو الان میخوایم در قالب کد نویسی باهم پیاده سازی کنیم.

اول از همه کتابخونه های زیر رو به پروژه خودت اضافه کن و پروژه رو sync کن.

//Architecture components
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//Image Loading
implementation "io.coil-kt:coil:2.1.0"
//Gson
implementation 'com.google.code.gson:gson:2.9.1'

توی این آموزش من قصد دارم از Coroutines (کوروتین) و Flow (فلو) استفاده کنم.

حالا که کتابخونه ها اضافه شدن، نوبت این رسیده که ازشون استفاده کنیم.

همانطور که قبلا هم بهتون گفتم اول از همه باید عملیات مورد نظرمون رو داخل کلاس Intent بنویسیم.

پس یک کلاس از نوع Sealed Class درست میکنیم.

اگه راجب انواع کلاس ها در کاتلین آشنایی نداره، پیشنهاد میکنم توی دوره رایگانی که داخل سایت قرار دادم شرکت کنی، چون به صورت رایگان کاتلین و انوع کلاس ها و شی گرایی رو آموزش دادم.

بعد که کلاس رو ساختیم میتونیم از کدهای زیر داخل استفاده کنیم:

sealed class MainPageIntent{
    object LoadAllProducts: MainPageIntent()
}

البته اگر برای انجام اون عملیات نیاز به دریافت ورودی داشتی، باید بجای object از data class استفاده کنی و ورودی رو براش قرار بدی.

بعد از این مرحله باید بریم سراغ کلاس State، کلاسی که وضعیت های مختلف اون عملیات رو باید براش پیاده سازی کنیم.

برای اکشنی که در بالا نوشتیم برای بارگذاری لیست محصولات میتونیم وضعیت هایی مانند موارد زیر رو تعریف کنیم.

sealed class MainPageState {
    object Idle : MainPageState()
    object LoadingProducts : MainPageState()
    object Empty : MainPageState()
    data class Error(val error: String) : MainPageState()
    data class LoadAllProducts(val list: MutableList<ProductsList>) : MainPageState()
}

در تکه کد بالا وضعیت های مختلفی رو تعریف کردم.

وضعیت هایی برای حالت پیش از بارگذاری، برای نمایش خطا و…

توسط این وضعیت ها میتونم حالت های مختلف اون اکشن مورد نیاز رو کنترل کنم.

برای ایجاد ارتباط بین این 2 تا کلاس نیاز به یک واسط داریم که من از ViewModel برای اینکار میخوام استفاده کنم.

class MainViewModel @Inject constructor(private val repository: MainPageRepository) : ViewModel() {
    val intentChannel = Channel<MainPageIntent>()
    private val _state = MutableStateFlow<MainPageState>(MainPage.Idle)
    val state: StateFlow<MainPageState> get() = _state

    init {
        handleIntents()
    }

    private fun handleIntents() = viewModelScope.launch {
        intentChannel.consumeAsFlow().collect { intent ->
            when (intent) {
                is MainPageIntent.LoadProductsList -> fetchingProductsList()
            }
        }
    }

    private fun fetchingProductsList() = viewModelScope.launch {
        val response = repository.productsList()
        _state.emit(MainPageState.LoadingProducts)
        when (response.code()) {
            in 200..202 -> {
                _state.value = if (response.body()!!.meals != null) {
                    MainPageState.LoadAllProducts(response.body()!!.list)
                } else {
                    MainPageState.Empty
                }
            }
            in 400..499 -> {
                _state.emit(MainPageState.Error(""))
            }
            in 500..599 -> {
                _state.emit(MainPageState.Error(""))
            }
        }
    }

کد بالا مربوط به کلاس ویومدل هستش که ارتباط بین کلاس های Intent و State رو برقرار کردم.

توی این آموزش من از Coroutines Channel (کانال های کوروتین) و StateFlow استفاده کردم.

اگر راجب این موارد آشنایی نداری میتونی از لینک روبرو به طور کامل این موارد رو یاد بگیری : دوره نخبگان معماری اندروید – آموزش معماری های MVVM , MVP و MVI

درنهایت نیاز به Activity یا Fragment داریم که بتونیم نتیجه نهایی رو به کاربر نشون بدیم.

برای اینکار من کدها رو داخل Fragment نوشتم.

class ProductsListFragment : Fragment() {
    //Binding
    private var _binding: FragmentProductsListBinding? = null
    private val binding get() = _binding!!

    //Other
    private val viewModel: ListViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentProductsListBinding.inflate(layoutInflater)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //InitViews
        binding.apply {
            //Scope
            lifecycleScope.launch {
            //Call Api
                viewModel.intentChannel.send(MainPageIntent.LoadAllProducts)
                //Get
                viewModel.state.collect { state ->
                    when (state) {
                        is MainPageState.Idle -> {}
                        is MainPageState.LoadingProducts  -> {
                            homeLoading.isVisible(true, productsRv)
                        }
                        is MainPageState.LoadAllProducts -> {
                            homeLoading.isVisible(false, productsRv)
                            productsAdapter.setData(state.list)
                            productsRv.setupRecyclerView(
                                LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false),
                                productsAdapter
                            )
                        }
                        is MainPageState.Error -> {
                            homeLoading.isVisible(false, productsRv)
                            Toast.makeText(requireContext(), state.error, Toast.LENGTH_SHORT).show()
                        }
                        is MainPageState.Empty -> {
                            checkConnectionOrEmpty(true, PageState.EMPTY)
                        }
                    }
                }
            }
        }
    }

توی تکه کد بالا اول از همه درخواست ارتباط با سرور رو توسط viewModel.intentChannel.send ارسال کردیم و در جواب Response برگشت داده شده حالت های مختلف رو توسط viewModel.state.collect به طور کامل مدیریت کردیم.

معماری MVI یکی از مهمترین ترین و درعین حال شاید در ابتدا سختترین معماری ها باشه.

پیشنهاد میکنم حتما حتما این معماری رو خوب و با دقت یاد بگیر که توی شرکت ها و پروژه های بزرگ خیلی میتونه کمکت کنه.

درضمن اگه میخوای به طور کامل معماری MVI رو آموزش ببینی و نکات خیلی مهم و کم یابش رو کامل یادبگیری توصیه میکنم حتما توی این دوره شرکت کنی.

در دوره نخبگان معماری نکات بسیار مهم و حیاتی معماری MVI آموزش داده شده با اطمینان این قول رو بهت میدم که نمیتونی توی هیچ دوره ای پیدا کنید و حاصل سال ها تجربه شخصی بنده روی پروژه های بزرگ و مختلف هستش.

دیدگاه‌ها ۲