آشنایی با الگویِ معماری MVI در اندروید

MVI

معماری MVI نظریه جدیدی را مطرح و پیاده سازی کرد که فقط با یک بار نوشتن یک متد، بتوان تمامی تعاملات Ui با کاربر را کنترل کرد و در واقع تا حدی از شر CallBack‌ ها خلاص شد.

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

معماری MVI چیست؟

معماری MVI یک معماری نوظهور بوده که در چند سال اخیر منتشر شده است. این معماری بر پایه‌ی Model-View-Intent است و دو اصل (یک طرفه بودن) و (چرخه‌ای بودن) بنا شده که از فریمورک Cycle.js الهام گرفته است. معماری MVI به نسبت معماری‌های دیگر مانند MVP ،MVC و یا MVVM تفاوت‌های بسیار دارد که در ادامه به آن می‌پردازیم.

منظور از سه کلمه Model-View-Intent چیست؟

همانطور که در قسمت قبلی اشاره کردیم، معماری MVI بر پایه‌ی Model ،View و Intent است. در ادامه به تعریف این سه کلمه می‌پردازیم:

Model: فراهم کننده‌ی حالت (state)های مختلف در اپلیکیشن است. Models در معماری MVI باید غیرقابل تغییر و همچنین جریان‌های داده بین آن و سایر لایه‌های اپلیکیشن یک طرفه باشد.

View: همانند دیگر معماری‌ها، از اینترفیس‌ها استفاده کرده و قرارداد‌هایی را برای View‌هایی مانند Activities و Fragments به وجود می‌آورد و آن‌ها را پیاده سازی می‌کند.

Intent: اعلام کننده هر فعالیتی هستند. این فعالیت می‌تواند توسط کاربر و یا خود اپلیکیشن به وجود آمده باشد که برای همه‌ی فعالیت‌ها (actions)، View یک Intent دریافت می‌کند، همچنین Presenter به Intent گوش می‌کند (Observes می‌کند).

مزایا و معایب معماری MVI چیست؟

در قسمت‌های قبل به معرفی معماری MVI پرداختیم. حال می‌خواهیم به بررسی دقیق‌ مزایا و معایب معماری MVI بپردازیم.

مزایا‌ی معماری MVI چیست؟

مزایای معماری MVI که می‌توان به آن اشاره نمود:

داده‌ها در معماری MVI به صورت یک طرفه (unidirectional) و دارای چرخه‌ی دورانی (cyclical data flow) هستند.

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

در زمان حیات (Lifecycle) اپلیکیشن View، فقط یک حالت (State) را کنترل و به لایه‌های مختلف اطلاع می‌دهد.

دیباگ (Debug) کردن راحت‌تر اپلیکیشن به دلیل اینکه در هر مرحله stateها مشخص هستند.

Models‌ در معماری MVI غیرقابل تغییر (Immutable) هستند که باعث می‌شود در جابه‌جایی بین Thread‌ها مشکل و یا تغییری برای Data در اپلیکیشن‌های بزرگ به وجود نیاید.

معایب معماری MVI

معایب معماری MVI که می‌توان به آن اشاره نمود:

تفاوت اصلی که می‌توان در معماری MVI برشمرد، مشکل در فهم آن برای برنامه نویسان کم تجربه و تازه‌کار است. زیرا برای فهم این معماری نیازمند درک مفاهیم دیگری مانند Reactive Programming، Multi-Threading، RxJava و.. هستید.

در معماری MVI به دلیل آنکه برای هر State، شئ جدیدی از Model خود ایجاد می‌کنیم، ممکن است با گذر زمان و بزرگ‌تر شدن اپلیکیشن مشکلاتی مانند اختلال در حافظه و یا پر شدن حافظه (Memory leak) به وجود آید. در هنگام توسعه‌ی نرم افزار حتما باید به این نکته توجه داشته باشید.

معماری MVI باعث تکرار کد‌ها در قسمت‌های مختلف اپلیکیشن می‌شود زیرا برای هر تعامل کاربر یک State جدید ایجاد می‌کند.

قابلیت‌های معماری MVI چیست؟

در قسمت قبل توضیح دادیم که Models در معماری MVI نگهدارنده‌ی State هستند و همین موضوع سبب محبوبیت این معماری شده است. این قابلیت باعث آن شده که Model کنترل کننده کل دیتای موجود در اپلیکیشن باشد و در صورت نیاز آن را به لایه‌های بالاتر و یا پایین‌تر اپلیکیشن ارسال کند.

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

در معماری MVI همه‌ی لایه‌ها امکان تغییر دیتا در Model را ندارند. به عنوان مثال View‌ها فقط می‌توانند با استفاده از یک متد، state را گرفته و آن را نمایش دهند.

تفاوت‌های بین معماری MVI و MVP چیست؟

به طور کلی دو معماری MVP و MVI در بیشتر موارد شبیه به هم هستند، اما تفاوت اصلی بین این دو معماری در دو مورد است که در ادامه به آن اشاره می‌کنیم:

Models در معماری MVI

هنگامی که در برنامه نویسی واکنشی یا Reactive، در سطح UI عکس العمل و یا تغییری از سطح کاربر و یا خود اپلیکیشن ایجاد ‌شود، به عنوان مثال با کلیک کردن بر روی یک Button و یا ارسال یک درخواست به سمت سرور و نمایش نتیجه‌ی آن، یک State جدید به وجود می‌آید. این تغییرات می‌تواند در پس زمینه‌ی اپلیکیشن و یا در سطح UI رخ دهد، مانند نمایش یک ProgressBar و یا دریافت نتیجه از سمت سرور.

برای درک بهتر، یک Model را در معماری MVP تصور کنید. این کلاس نتیجه‌ای را که از سمت سرور دریافت شده است در خود نگه می‌دارد که به صورت زیر است:

data class Picture(
        var id: Int? = null,
        var name: String? = null,
        var url: String? = null,
        var date: String? = null
)

مثال بالا Presenter از Model بالا استفاده کرده و آن را آماده‌ی نمایش در سطح UI می‌کند:

class MainPresenter(private var view: MainView?) {
    override fun onViewCreated() {
        view.showLoading()
        loadPictureList { pictureList ->
            pictureList.let {
                this.onQuerySuccess(pictureList)
            }
        }
    }
    override fun onQuerySuccess(data: List<Picture>) {
        view.hideLoading()
        view.displayPictureList()
    }
}

در چنین حالتی، مشکل خاصی به وجود نمی‌آید اما معماری MVI برخی از مسائلی که ممکن است در این بین به وجود بیاید را مورد بررسی قرار داده و برطرف کرده است:

دریافت چندین ورودی و در معماری‌های MVVM و MVP، داخل Presenter و ViewModel چندین ورودی و خروجی را باید کنترل کرد که در گذر زمان و بزرگ‌تر شدن اپلیکیشن می‌تواند مشکل آفرین باشد.

وجود چندین حالت (State) مختلف: در معماری‌های MVVM و MVP، ممکن است حالت‌های مختلفی برای اپلیکیشن به وجود بیاید که توسعه دهنده آن‌ها را با CallBack‌ها کنترل و برنامه نویسی می‌کند. این موضوع در شرایط استثنایی ممکن است مشکل آفرین باشد.

در چنین مواقعی معماری MVI به کمک ما برای حل این مسائل می‌آید که در این صورت Model ما بدین شکل می‌باشد:

sealed class PictureState {
    object LoadingState : PictureState()
    data class DataState(val data: List<Picture>) : PictureState()
    data class ErrorState(val data: String) : PictureState()
    data class ConfirmationState(val picture: Picture) : PictureState()
    object FinishState : PictureState()
}

در واقع Models در معماری MVI سعی در کنترل حالت‌های مختلف و اعلام آن‌ها به ViewModel و View و یا Presenter دارد که باعث می‌شود از تکرار کدنویسی یک State در لایه‌های مختلف اپلیکیشن جلوگیری گردد.

و همچنین Presenter ما در این مثال به صورت زیر است:

class MainPresenter {
    private val compositeDisposable = CompositeDisposable()
    private lateinit var view: MainView
    fun bind(view: MainView) {
        this.view = view
        compositeDisposable.add(observePictureDeleteIntent())
        compositeDisposable.add(observePictureDisplay())
    }
    fun unbind() {
        if (!compositeDisposable.isDisposed) {
            compositeDisposable.dispose()
        }
    }
    private fun observePictureDisplay() = loadPictureList()
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe { view.render(PictureState.LoadingState) }
            .doOnNext { view.render(it) }
            .subscribe()
}

حال اپلیکیشن شما فقط یک خروجی دارد که آن State یک View است و با استفاده از متد ()render قابل نمایش است، این متد وضعیت فعلی اپلیکیشن را به View اطلاع می‌دهد. ما اطمینان داریم که در طول حیات اپلیکیشن Model فقط یک حالت را کنترل می‌کند و در کلاس‌های دیگر قابل تغییر نیستند. به عبارتی دیگر در طول حیات اپلیکیشن فقط یک حالت را نشان می‌دهد.

Views و Intents در معماری MVI

مانند معماری MVP، معماری MVI برای یک یا چند View اینترفیسی به عنوان قرارداد (Contract) ایجاد کرده و توسط یک یا چندین Activity یا Fragment پیاده سازی می‌شود. View‌ها در معماری MVI تمایل دارند از یک ()render استفاده کرده و از آن برای نمایش UI به کاربر استفاده کنند. همچنین View‌ها از ()intentهایی استفاده می‌کنند که Observable هستند تا به تعامل کاربر با اپلیکیشن پاسخ دهند.

نکته: منظور از Intentها در معماری MVI اندروید، کلاس android.content.Intent نیست. بلکه منظور کلاسی است که بیانگر تغییرات و عملکرد اپلیکیشن است.

در این صورت View به شکل زیر است:

class MainActivity : MainView {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    //۱
    override fun displayPictureIntent() = button.clicks()
    //۲
    override fun render(state: PictureState) {
        when (state) {
            is PictureState.DataState -> renderDataState(state)
            is PictureState.LoadingState -> renderLoadingState()
            is PictureState.ErrorState -> renderErrorState(state)
        }
    }
    //۴
    private fun renderDataState(dataState: PictureState.DataState) {
        //Render picture list
    }
    //۳
    private fun renderLoadingState() {
        //Render progress bar on screen
    }
    //۵
    private fun renderErrorState(errorState: PictureState.ErrorState) {
        //Display error mesage
    }
}
  1. متد displayMovieIntent: تعاملات کاربر را به Intent مناسب Bind می‌کند. در این مثال کلیک کردن بر روی یک Button را به عنوان intent اطلاع می‌دهد.
  2. متد render: نشان دهنده‌ی State فعلی View است. همچنین بخشی از اینترفیس (Interface) MainView نیز می‌باشد.
  3. متد renderDataState: این متد دیتایی را که از Model دریافت کرده برای نمایش آماده می‌کند.
  4. متد renderLoadingState: این متد نمایش دهنده‌ی حالت بارگذاری (Loading) در View است.
  5. متد renderErrorState: این متد نمایش دهنده‌ی ارور به وجود آمده در View است.

ایجاد یک پروژه اندروید با معماری MVI

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

نکته: این پروژه نیازمند دانستن مفاهیمی همچون Coroutine، Retrofit و Glide است.

برای ایجاد پروژه جدید مراحل زیر را به ترتیب انجام دهید:

  1. انتخاب گزینه‌ی Create a New Project.
  2. انتخاب Empty Activity و کلیک بر روی گزینه‌ی Next.
  3. انتخاب نام مورد نظر، انتخاب محل ذخیره و انتخاب زبان کاتلین (Kotlin) برای پروژه و سپس کلیک بر روی گزینه‌ی Finish.

بعد از گذراندن مراحل بالا پروژه‌ی خود را ایجاد کرده‌ایم و مراحل بعدی را پیش می‌بریم:

ما در این پروژه از سه کامپوننت (Component) Glide ،Retrofit و Coroutine استفاده می‌کنیم. برای استفاده از این کتابخانه‌ها، وابستگی‌های زیر را در فایل build.gradle اضافه کنید (می‌توانید آخرین نسخه‌ی موجود را در سایت سازنده و یا داکیومنت اندروید مشاهده کنید):

//lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:{last-version}'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:{last-version}'
//glide
implementation 'com.github.bumptech.glide:glide:{last-version}'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:{last-version}'
implementation "com.squareup.retrofit2:converter-moshi:{last-version}"
//coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:{last-version}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:{last-version}"

در ادامه ما برای پروژه‌ی خود، پکیج بندی را انجام می‌دهیم:

قبل از شروع، در فایل AndroidManifest.xml دسترسی زیر را اضافه می‌کنیم:

<uses-permission android:name="android.permission.INTERNET" />

در قدم اول، ما نیاز به یک Model به صورت زیر داریم که آن را User نام گذاری می‌کنیم (این کلاس را داخل پکیج repository اضافه کنید):

data class User(
        val id: Int? = null,
        val name: String? = null,
        val email: String? = null 
)

در قدم بعدی ما در پکیج api کلاس‌های زیر را ایجاد می‌کنیم:

در این پروژه ما نیاز به کلاسی تحت عنوان ApiService داریم تا بتوانیم از کتابخانه Retrofit استفاده کنیم:

interface ApiService {
   @GET("users")
   suspend fun getUsers(): List<User>
}

در ادامه اینترفیسی تحت عنوان ApiHelper را ایجاد می‌کنیم:

interface ApiHelper {
    suspend fun getUsers(): List<User>
}

نکته: برای آن که بتوانیم از Coroutine استفاده کنیم باید متد (Function) خود را به suspend fun تغییر بدهیم.

حال Object به اسم RetrofitBuilder ایجاد می‌کنیم و در آن متد‌های زیر را قرار می‌دهیم:

object RetrofitBuilder {
    private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
    private fun getRetrofit() = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
    val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}

در قدم بعدی ما نیاز به کلاسی جهت پیاده سازی اینترفیس ApiHelper داریم که به صورت زیر است:

class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {
    override suspend fun getUsers(): List<User> {
        return apiService.getUsers()
    }
}

تا این مرحله، بخش درخواست به سرور ما و همچنین پیاده سازی Retrofit تکمیل شد. در قدم بعدی نیازمند یک ریپازیتوری هستیم تا با استفاده از آن بتوانیم در ViewModel متد getUser را صدا بزنیم (این کلاس را داخل پکیج repository اضافه کنید):

class MainRepository(private val apiHelper: ApiHelper) {
    suspend fun getUsers() = apiHelper.getUsers()
}

پکیچ model ما در این پروژه آماده است. در قدم بعدی ما در بخش ui پکیجی تحت عنوان adapter ایجاد می‌کنیم. برای پیاده سازی RecyclerView نیازمند کلاسی جهت پیاده سازی Adapter و ViewHolder هستیم که آن را به صورت زیر ایجاد کرده و می‌نویسیم:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.sevenlearn.mvi.R
import com.sevenlearn.mvi.data.model.User
import kotlinx.android.synthetic.main.item_layout.view.*
class MainAdapter(
    private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {
    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(user: User) {
            itemView.textViewUserName.text = user.name
            itemView.textViewUserEmail.text = user.email
        }
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        DataViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_layout, parent,
                false
            )
        )
    override fun getItemCount(): Int = users.size
    override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
        holder.bind(users[position])
    fun addData(list: List<User>) {
        users.addAll(list)
    }
}

سپس پکیج دیگری تحت عنوان intent ایجاد کرده و کلاس زیر را در آن اضافه می‌کنیم (این کلاس را در پکیجی با نام intent در پکیج ui می‌توانید قرار دهید):

sealed class MainIntent {
    object FetchUser : MainIntent()
}

در قدم بعدی به بخش مهم اپلیکیشن یعنی کلاس MainState می‌رسیم، این کلاس یکی از مهم‌ترین بخش‌های اپلیکیشن در معماری MVI است که آن را در پکیجی با نام viewstate اضافه می‌کنیم:

sealed class MainState {
    object Idle : MainState()
    object Loading : MainState()
    data class Users(val user: List<User>) : MainState()
    data class Error(val error: String?) : MainState()
}

سپس ViewModel خود را ایجاد می‌کنیم و آن را MainViewModel می‌نامیم:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sevenlearn.mvi.data.repository.MainRepository
import com.sevenlearn.mvi.ui.main.intent.MainIntent
import com.sevenlearn.mvi.ui.main.viewstate.MainState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
class MainViewModel(
    private val repository: MainRepository
) : ViewModel() {
    val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<MainState>(MainState.Idle)
    val state: StateFlow<MainState>
        get() = _state
    init {
        handleIntent()
    }
    private fun handleIntent() {
        viewModelScope.launch {
            userIntent.consumeAsFlow().collect {
                when (it) {
                    is MainIntent.FetchUser -> fetchUser()
                }
            }
        }
    }
    private fun fetchUser() {
        viewModelScope.launch {
            _state.value = MainState.Loading
            _state.value = try {
                MainState.Users(repository.getUsers())
            } catch (e: Exception) {
                MainState.Error(e.localizedMessage)
            }
        }
    }
}

در قدم بعدی در پکیج util کلاس MainViewModelFactory را ایجاد می‌کنیم:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.sevenlearn.mvi.data.api.ApiHelper
import com.sevenlearn.mvi.data.repository.MainRepository
import com.sevenlearn.mvi.ui.main.viewmodel.MainViewModel
class ViewModelFactory(private val apiHelper: ApiHelper) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(MainRepository(apiHelper)) as T
        }
        throw IllegalArgumentException("Unknown class name")
    }
}

سپس کلاس MainActivity را به صورت زیر کدنویسی می‌کنیم:

@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
    private lateinit var mainViewModel: MainViewModel
    private var adapter = MainAdapter(arrayListOf())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupUI()
        setupViewModel()
        observeViewModel()
        setupClicks()
    }
    private fun setupClicks() {
        buttonFetchUser.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.userIntent.send(MainIntent.FetchUser)
            }
        }
    }
    private fun setupUI() {
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.run {
            addItemDecoration(
                DividerItemDecoration(
                    recyclerView.context,
                    (recyclerView.layoutManager as LinearLayoutManager).orientation
                )
            )
        }
        recyclerView.adapter = adapter
    }
    private fun setupViewModel() {
        mainViewModel = ViewModelProviders.of(
            this,
            ViewModelFactory(
                ApiHelperImpl(
                    RetrofitBuilder.apiService
                )
            )
        ).get(MainViewModel::class.java)
    }
private fun observeViewModel() {
        lifecycleScope.launch {
            mainViewModel.state.collect {
                when (it) {
                    is MainState.Idle -> {
                    }
                    is MainState.Loading -> {
                        buttonFetchUser.visibility = View.GONE
                        progressBar.visibility = View.VISIBLE
                    }
                    is MainState.Users -> {
                        progressBar.visibility = View.GONE
                        buttonFetchUser.visibility = View.GONE
                        renderList(it.user)
                    }
                    is MainState.Error -> {
                        progressBar.visibility = View.GONE
                        buttonFetchUser.visibility = View.VISIBLE
                        Toast.makeText(this@MainActivity, it.error, 
			Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }
    private fun renderList(users: List<User>) {
        recyclerView.visibility = View.VISIBLE
        users.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }
        adapter.notifyDataSetChanged()
    }
}

سپس به سراغ فایل‌های xml می‌رویم. در مرحله‌ی اول فایل main_activity.xml را به صورت زیر طراحی می‌کنیم:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.view.MainActivity">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />
    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/buttonFetchUser"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/fetch_user"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

و همچنین فایلی به نام item_user.xml ایجاد کرده و آن را به صورت زیر کد نویسی می‌کنیم:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserName"
        style="@style/TextAppearance.AppCompat.Large"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="MindOrks" />
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserEmail"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textViewUserName"
        app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
        tools:text="MindOrks" />
</androidx.constraintlayout.widget.ConstraintLayout>

و در مرحله آخر در فایل strings.xml این خط را اضافه می‌کنیم:

<string name="fetch_user">Fetch User</string>

هنگامی که بر روی گزینه FATCH USER کلیک کنید، سه حالت می‌تواند رخ دهد:

  1. state نمایش ProgressBar.
  2. state نمایش نتیجه‌ای که از سرور دریافت شده است.
  3. State نمایش Error که می‌تواند با قطع اتصال اینترنت و یا بیش از حد زمان بردن درخواست به سمت سرور (Timeout) رخ دهد.

پرسش‌های رایج در رابطه با معماری MVI

سوال اول : آیا معماری MVI مشکلی با کتابخانه‌هایی مثل RxJava دارد؟ آیا نیاز به استفاده آن در این معماری داریم؟

جواب : خیر معماری MVI مشکلی با این چنین کتابخانه‌ها ندارد ولی بعضی از توسعه دهندگان از آن استفاده می‌کنند تا کد نویسی را راحت‌تر و توسعه پذیر‌تری داشته باشند.

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

جواب : خیر، این سوال بسته به سناریوی توسعه دهنده متفاوت است. به طور کلی Intent بیانگر قصدی در اپلیکیشن است، مانند ارسال درخواستی به سرور. با این حال انتظار نباید داشت که در همه اپلیکیشن‌هایی که با معماری MVI نوشته شده‌اند پکیجی با نام Intent را مشاهده کرد.

چرا معماری MVI را برای پروژه‌های اندرویدی خود انتخاب کنیم؟

در این مقاله سعی کردیم معماری MVI را به صورت عملی به شما معرفی کنیم. معماری MVI تمرکز بیشتری روی لایه‌ی Model داشته و از این رو متمایز با معماری‌های دیگر نظیر MVP و یا MVVM است. این معماری هنوز محبوبیت آنچنانی ندارد اما می‌تواند در آینده نظر توسعه دهندگان اندروید را به خود جلب کند.

آخرین نوشته ها

تماس با ما

 کرج، شاهین ویلا، بلوار امام خمینی ، خیابان نهم شرقی ، برج شاهین ،طبقه اول واحد2

 91014618

  info@shopingserver.net

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

Erfan Akbarieh

Erfan Akbarieh

مطالب مرتبط