آموزش معماری MVI در اندروید به صورت گام به گام
الگوهای معماری در اندروید روز به روز در حال پیشرفت هستند. همانطور که ما برنامه ها را توسعه می دهیم ، با چالش ها و مشکلات جدیدی روبرو می شویم. با ادامه حل چالش های مشابه، الگوهای جدیدی کشف خواهند شد. به عنوان توسعه دهندگان اندروید ، 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
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"/>
اکنون پروژه را بسازید و برنامه را روی دستگاه اجرا کنید. باید داده ها را در رابط کاربری را بارگذاری کند.
دیدگاهتان را بنویسید