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

در این آموزش شما را راهنمایی خواهم کرد :

  • معماری MVI چیست
  • MVI چگونه کار می کند
  • مزایا و معایب MVI
  • نحوه ایجاد پروژه با معماری MVI

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

MVI مخفف Model-View-Intent است. این الگو به تازگی در اندروید معرفی شده است. بر اساس اصل جریان به صورت یک طرفه و براساس چارچوب Cycle.js کار می کند.

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

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

MVI چگونه کار می کند؟

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

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

مزایا و معایب MVI

بیایید ببینیم مزایا و معایب MVI چیست

مزایای MVI

  • حفظ وضعیت دیگر چالشی با این معماری نیست، زیرا عمدتاً بر روی وضعیت ها تمرکز دارد.
  • از آنجایی که یک جهته است ، جریان داده را می توان به راحتی ردیابی و پیش بینی کرد.
  • این امر ایمنی نخ را تضمین می کند زیرا حالت اشیاء تغییرناپذیر هستند.
  • اشکال زدایی در هنگام وقوع خطا آسان است .
  • از آنجایی که هر جزء مسئولیت خود را انجام می دهد، بیشتر جدا شده است.
  • آزمایش برنامه نیز آسان تر خواهد بود زیرا می توانیم منطق تجاری را برای هر وضعیت ترسیم کنیم.

معایب MVI

  • این منجر به تعداد زیادی کد بویلر می شود زیرا ما باید برای هر اقدام کاربر یک حالت یا وضعیتی را حفظ کنیم.
  • همانطور که می دانیم، برای همه حالت ها object (شی) های زیادی ایجاد می کند. این امر مدیریت حافظه برنامه را بسیار سخت می کند.
  • در حالی که مدیریت تغییرات پیکربندی، مدیریت موقعیت های هشدار می تواند چالش برانگیز باشد. برای مثال اگر اینترنت وجود نداشته باشد، یک snack bar را نشان می دهیم ، با تغییر تنظیمات، snack bar را دوباره نشان می دهیم. از نظر قابلیت استفاده ، باید به این موضوع رسیدگی شود.

با این پیش زمینه که خوندید ، بیایید یک برنامه کوچک با MVI ایجاد کنیم

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

بیایید با راه اندازی پروژه اندروید شروع کنیم.

ساخت یک پروژه :

  • شروع یک پروژه جدید در اندروید
  • انتخاب یک Empty Activity و بعد برروی دکمه Next بزنید
  • نام برنامه : MVI-Architecture-Android-Beginners
  • نام پکیج : mindorks.framework.mvi
  • زبان: کاتلین
  • برروی دکمه Finish بزنید
  • پروژ تان در حال آماده شدن است

اضافه کردن dependencies

 <em>// Added Dependencies</em>  implementation "androidx.recyclerview:recyclerview:1.1.0"  implementation 'android.arch.lifecycle:extensions:1.1.1'  implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'  implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'  implementation 'com.github.bumptech.glide:glide:4.11.0'   //retrofit  implementation 'com.squareup.retrofit2:retrofit:2.8.1'  implementation "com.squareup.retrofit2:converter-moshi:2.6.2"   //Coroutine  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"  

ساختار پروژه

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

پیاده سازی لایه data (Data Layer)

حال در این قسمت لایه data را راه اندازی می کنیم.

در پکیج داده ، بسته های api ، model و repository خواهیم داشت. ما این بسته ها را ایجاد خواهیم کرد و دررون هر پکیج کلاس هایی اضافه می کنیم.

بیایید کلاس هایی را اضافه کنیم که از API پشتیبانی می کنند.

ما به مدلی نیاز داریم که براساس پاسخی که به آن داده می شود تغییر کند. کلاس داده (data class) User.kt را مطابق شکل زیر ایجاد کنید.

  package com.mindorks.framework.mvi.data.model  import com.squareup.moshi.Json  data class User(     @Json(name = "id")     val id: Int = 0,     @Json(name = "name")     val name: String = "",     @Json(name = "email")     val email: String = "",     @Json(name = "avatar")     val avatar: String = "" )   

توجه: ما از کلمه کلیدی suspend برای پشتیبانی از کتابخانه Coroutines استفاده کرده ایم تا بتوانیم آن را از یک Coroutine یا تابع تعلیق دیگری فراخوانی کنیم.

در این پروژه از کتابخانه های Kotlin-Coroutines و Flow API استفاده شده است.

یک کلاس ApiService.kt ایجاد کنید که در آن روش های HTTP را برای برقراری ارتباط با API مشخص می کنیم.

 package com.mindorks.framework.mvi.data.api  import com.mindorks.framework.mvi.data.model.User  interface ApiHelper {      suspend fun getUsers(): List<User>  } 

اکنون سازنده retrofit را اضافه کنید که URL نقطه پایانی را می سازد و خدمات REST را مصرف کنند.

 package com.mindorks.framework.mvi.data.api  import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory  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)  } 

اکنون باید این رابط را برای واکشی List<Users> پیاده سازی کنیم، ApiHelperImpl.kt را ایجاد کنیم.

 package com.mindorks.framework.mvi.data.api  import com.mindorks.framework.mvi.data.model.User  class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {      override suspend fun getUsers(): List<User> {         return apiService.getUsers()     } } 

اکنون ما آماده هستیم تا با سرویس های restful در لایه داده خود ارتباط برقرار کنیم.

برای درخواست داده ها به یک repository نیاز داریم. در مورد ما، ما متد getUsers را از Activity از طریق ViewModel فراخوانی می کنیم تا لیست کاربران را دریافت کنیم.حالا کلاسی به نام MainRepository.kt را ایجاد کنید.

 package com.mindorks.framework.mvi.data.repository  import com.mindorks.framework.mvi.data.api.ApiHelper   class MainRepository(private val apiHelper: ApiHelper) {      suspend fun getUsers() = apiHelper.getUsers()  } 

بنابراین لایه داده ما آماده است. اکنون که به بخش رابط کاربری می‌رسیم، به یک adapter برای recyclerview ، Intent برای ذخیره اقدامات کاربر، main activity ما، MainViewModel در viewModel، حالت View که در آن حالت‌های مختلفی را تعریف کرده‌ایم که باید داده‌ها را در viewها بارگذاری کنیم، نیاز داریم.

MainAdapter را در adapter package ایجاد کنید

 package com.mindorks.framework.mvi.ui.main.adapter  import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.mindorks.framework.mvi.R import com.mindorks.framework.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             Glide.with(itemView.imageViewAvatar.context)                 .load(user.avatar)                 .into(itemView.imageViewAvatar)         }     }      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)     }  }  

اکنون MainState.kt را در بسته viewstate اضافه می کنیم. این مهمترین بخش MVI است. در این کلاس حالت های Idle, loading, users, error را تعریف می کنیم. هر حالت وضیعت را می توان با intent در view بارگذاری کرد.

  package com.mindorks.framework.mvi.ui.main.viewstate  import com.mindorks.framework.mvi.data.model.User  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 ایجاد کنید :

 package com.mindorks.framework.mvi.ui.main.viewmodel  import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.mindorks.framework.mvi.data.repository.MainRepository import com.mindorks.framework.mvi.ui.main.intent.MainIntent import com.mindorks.framework.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)             }         }     } }  

در اینجا در ViewModel، ما در حال مشاهده userIntent برای انجام عمل بر روی آن هستیم.

و بر اساس پاسخ لایه داده، داخل متد fetchUser حالت را تغییر می دهیم. و این حالت در Main Activity مشاهده می شود.

و حالا ViewModelFactory را تحت پکیج util راه اندازی کنیم.

در این کلاس ما از viewModel خود یک نمونه می سازیم و نمونه ViewModel را برمی گردانیم.

 package com.mindorks.framework.mvi.util  import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.mindorks.framework.mvi.data.api.ApiHelper import com.mindorks.framework.mvi.data.repository.MainRepository import com.mindorks.framework.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")     }  } 

حالا XML layout را طراحی می کنیم.

در پوشه layout، داخل پوشه activity_main.xml کد زیر را می نویسیم :

 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     xmlns:tools="http://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>   

به کلاس MainAcitvity.kt تان بروید و پکیج under view را اضافه کنید . این اکتیویتی است که ورودی را از کاربر دریافت می کند، بر اساس این MVI وضعیت های ذکر شده در viewModel را بررسی می کند و حالت خاص را در view بارگذاری می کند.

بیایید ببینیم MainActivity چگونه از درخواست داده ها، مدیریت وضعیت ها مراقبت می کند.

  package com.mindorks.framework.mvi.ui.main.view  import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.mindorks.framework.mvi.R import com.mindorks.framework.mvi.data.api.ApiHelperImpl import com.mindorks.framework.mvi.data.api.RetrofitBuilder import com.mindorks.framework.mvi.data.model.User import com.mindorks.framework.mvi.util.ViewModelFactory import com.mindorks.framework.mvi.ui.main.adapter.MainAdapter import com.mindorks.framework.mvi.ui.main.intent.MainIntent import com.mindorks.framework.mvi.ui.main.viewmodel.MainViewModel import com.mindorks.framework.mvi.ui.main.viewstate.MainState import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch  @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()     } }   

در اینجا، ما قصد واکشی (fetch) داده ها را با کلیک روی دکمه (User Action) ارسال می کنیم.

همچنین، ما در حال مشاهده حالت ViewModel برای تغییرات وضعیت هستیم. و با استفاده از شرط “when” وضعیت هدف پاسخ را مقایسه کرده و حالات مربوطه را بارگذاری می کنیم.

در نهایت مجوز اینترنت را به پروژه خود اضافه کنید. موارد زیر را در فایل AndroidManifest.xml اضافه کنید:

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

اکنون پروژه را بسازید و برنامه را روی دستگاه اجرا کنید. باید داده ها را در رابط کاربری را بارگذاری کند.

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

توسط asadroid

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

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