الگویِ معماری MVI در اندروید به زبان ساده

الگوهای معماری سری MVx مطمئنا جزو لزومات یک برنامه نویس اندروید هستند ، از بین این معماری ها گوگل بر روی معماری MVVM توجه خاصی داشته و ابزارهایی مثل LiveData و ... را برای اونها معرفی کرده . امروز قصد معرفی یک معماری جدید مبتنی بر MVx داریم به نام MVI !

نکته : من در این مقاله قصد ندارم در مورد MVC و MVP و MVVM توضیح بدم ، فرض بر اینه که شما با این معماری ها آشنا هستید ، اما مقایسه ای بین این سه معماری و MVI در انتهای مقاله انجام میدم .

https://media.giphy.com/media/3ohhwyXhrNXmL831XG/giphy.gif

مقدمه

الگوی معماری MVI مبتنی بر برنامه نویسی واکنش گراست (Reactive) ، احتمالا به اسامی مثل RxJava و RxAndroid برخوردید که ابزارهایی برای پیاده سازی این نوع برنامه نویسی در محیط اندروید در اختیار شما قرار می‌دهند . خیلی مختصر و مفید بگیم : "کاربر با UI تعامل میکند ، این تعامل موجب ایجاد تغییراتی در حالت موجود می‌شود و این تغییرات باید به کاربر نشان داده شود" !

کلمه MVI در واقع مخفف سه اسم هستند : Model - View - Intent و در واقع یک معماری برای cycle.js بوده که بعدا در اندروید هم پیاده سازی شده .

بگذارید با یک شکل از منابعِ اصلیِ MVI شروع کنیم (ذهن آدم همیشه عکس رو بهتر از متن می‌فهمه) :

در یک نگاه کل فرایند MVI به این صورت است
در یک نگاه کل فرایند MVI به این صورت است

خب در این عکس هر سه عامل اصلی در معماری MVI رو می‌بینید ، کاربر با view تعامل (interaction) می‌کنه و این تعامل به صورت intent هایی در میان ، توجه کنید که ما اصلا در مورد کلاس Intent که شما باهاش اکتیویتی باز می‌کنید حرف نمی‌زنیم ، intent از کلمه intention میاد که معنی لغوی اون میشه "قصد" برای انجام کار ! تعاملاتی که کاربر انجام میده به صورت ورودی هایی برای intent ما در میان (مثلا کلیک کردن بر روی یه دکمه) و خروجی این intent ها به مدل وارد میشن .

قسمت Model های ما چی هستند ؟ اینجا مفهوم model کمی با قبل فرق می‌کنه ، تابع model در شکل بالا خروجی Intent های ما رو می‌گیره و بعد از انجام فرایندهای لازم بر اساس اون state های جدید رو درست می‌کنه ، این state چیه ؟ همون طوری که اول مقاله گفتم این معماری اول در فریمورک های جاواسکریپت پیاده سازی شده ، تو بعضی از این فریمورک ها (یه به فارسی بگیم ، چهارچوب ها) مفهومی به نام state داریم ، معنی لغوی اون که مشخصه و میشخه "وضعیت" ، ما متغیرهایی که مستقیما به تغییرات UI ارتباط دارن رو با state ها جابه‌جا می‌کنیم و این state ها هستن که وضعیت UI ما رو درست می‌کنن . دقت کنید که در MVI این state ها باید تغییر ناپذیر (immutable) باشن و هر وقت خواستیم تغییراتی در اونها بدیم باید از اول ساخته بشن . دلیل این مساله اینه که ما فقط یک منبع موثق برای وضعیتمون نیاز داریم و نمی‌خوایم قسمت های دیگه توی برنامه مون بتونن تغییراتی رو این قسمت بدن و این طوری خیالمون راحت تره که می‌تونیم اونا به اشتراک بذاریم بدون اینکه مساله ای براشون پیش بیاد .

قسمت View ما هم که مشخصه ! چیزی که به کاربر نشون می‌دیم ، state های جدیدی که توی این پروسه تولید میشن باعث تغییرات View میشن و View می‌تونه event های جدید رو به حالت Intent در بیاره و یک پروسه جدید رو شروع کنه !

برنامه نویسی واکنش گرا

خب واکنش گرایی در اینجا به چه صورت در میاد ؟ ما بالا ذکر کردیم که برنامه نویسی ما واکنش گراست و این مفهوم دقیقا همون مفهوم Intent ها رو برای ما دارن ، ما قصد انجام کاری رو می‌کنیم و سیستم به ازای تغییراتی که پدید میاد واکنش میده . پس نیاز داریم (البته نه لزوما) از ابزاری مثل RxJava برای کارمون استفاده کنیم ، با RxJava ما می‌تونیم وضعیت خودمون ور مشاهده (Observe) کنیم و به محض تغییرات ، کارای لازم انجام بشه .

یک چرخه از اتفاقات در MVI
یک چرخه از اتفاقات در MVI

نکته : شما می‌تونید به جای RxJava از LiveData استفاده کنید یا اصلا می‌تونید از هیچ کدوم از اینا استفاده نکنید ولی پیشنهاد من روی RxJava یا LiveData هست چون اکثر منابع برای MVI بر حسب این دو کتابخونه کاراشون رو انجام دادن .

خب ، از اینجا به بعد من ترجیح میدم که یک پیاده سازی ساده از قضیه برای شما داشته باشم و بعد مقایسه ای بین این معماری ها انجام بدیم

https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif

پیاده سازی

برای پیاده‌سازی این معماری یک کتابخونه مهم داریم که خیلی از کارها رو برای ما انجام میده ، کتاب خونه mosby که مخصوص MVP و MVI طراحی شده (در واقع این کتابخونه توسط شخصی که MVI رو برای اندروید معرفی کرده ارائه شده) . کاری که ما می‌خوایم انجام بدیم (در انتهای مقاله لینک کد رو میذارم تا بتونید کتابخونه ها رو از gradle پیدا کنید) اینه که برنامه‌ای بنویسیم که به ازای کلیک کاربر به اون یک متن رو نمایش بده (ساده ترین حالتی که می‌تونیم برنامه بنویسیم) .

اولین قدم برای پیاده سازی این برنامه پیاده سازی یک interface برای تعاملات UI هست ، هدف کلی برنامه اینه که با کلیک برروی یک دکمه یک واکنش نسبت به اون کلیک انجام بدیم و یک متن رو روی صفحه نشون بدیم ، این interface رو از MvpView (که برای mosby هست) Implement می‌کنیم :

interface MainView : MvpView{
    fun loadText() : Observable<Unit>
    fun render(state : MainViewState)
}

دو تابعی که می‌بینید یکی وظیفه ایجاد event برای کلیک (loadText) و اون یکی وظیفه تغییرات UI رو داره ، اینجا یک کلاس دیگه به اسم MainViewState داریم که همون State های برنامه ما هستند :

sealed class MainViewState {
    object LoadingState : MainViewState()
    data class DataState(val text: String) : MainViewState()
    data class ErrorState(val error: Throwable) : MainViewState()
}

خب یکم کاتلین استفاده شده ، sealed class ها کلاس هایی هستند که وقتی از اونها شی می‌گیریم میتونیم بررسی کنیم که از جنس کدوم یکی از زیرمجموعه هاش شی گرفته شده ، بهتر توضیح بدیم این طوریه که ما وقتی بخوایم از MainViewState یک شی بگیریم ، یا باید از جنس LoadingState باشه ، یا از جنس DataState و یا از جنس ErrorState ! این کلاس ها یکی از بهترین روش های پیاده سازی state ها هستند چون اونا رو قابل اطمینان می‌کنند .

بعد از اون کلاس اکتیویتی‌مون رو می‌سازیم ، طبق کتابخونه mosby این کلاس باید از MviActivity ارث برده باشه و همین طور با MainView اون رو implement کرده باشیم :

class MainActivity : MviActivity<MainView,MainPresenter>() , MainView

ما چهار عنصر در این کلاس داریم ، دو از اونها باید از کلاس های کتابخونه اضافه شده override بشه :

override fun createPresenter(): MainPresenter {
 return MainPresenter()
}
override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

دو قسمت بعدی در واقع برای MainView ما بودند که باید override بشن :

override fun loadText() = btn.clicks()

این event ماست ، btn.click با استفاده از rxbinding به برنامه اضافه شده ، هر زمان که btn (که دکمه ما در صفحه است) کلیک بخوره ، ما میاییم loadText رو که از جنس Observable (مشاهده شونده) بوده رو تغییر می‌دیم (جلوتر توی presenter این قضیه رو بررسی می‌کنیم) ،به ازای این event تغییرات و پروسه ای انجام میشه .

override fun render(state: MainViewState) {
 when(state)
    {
 is MainViewState.LoadingState ->
        {
            txt.text = "Loading..."
 btn.isEnabled = false
 }
 is MainViewState.DataState ->
        {
            txt.text = state.text
 btn.isEnabled = true
 }
 is MainViewState.ErrorState ->
        {
            txt.text = state.error.message
 btn.isEnabled = true
 }
    }
}

این تابع آخر ما در این کلاس هست که وظیفه اش بروزرسانی UI بر اساس state هایی که به اون پاس میدیم هست ، همون طوری که می‌بینید اگه MainViewState از جنس LoadingState باشه میاییم و نوشته "loading" رو به کاربر نشون میدیم و الی آخر .

حالا این تغییرات به کجا وارد میشن ؟ به کلاس Presenter ما (از نظرِ من ، MVi در واقع ترکیبی از MVP و MVVP با اضافه کردن یک سری چیزای دیگه حساب میشه)

class MainPresenter : MviBasePresenter<MainView, MainViewState>() {
 override fun bindIntents() {
 val myState : Observable<MainViewState> = intent(MainView::loadText)
            .subscribeOn(Schedulers.io())
            .switchMap { MyInteractor.getMyText() }
 .observeOn(AndroidSchedulers.mainThread())
        subscribeViewState(myState,MainView::render)
    }
}

خب ، تابع bindIntent جزو توابعی هست که mosby پیاده سازی کرده ، وقتی یک event از سمت اکتیویتی ایجاد بشه ، این تابع صدا زده میشه ، ما توی این تابع هست که از RxJava استفاده می‌کنیم ، همونطوری که می‌بینید اول بررسی میشه که اگه جنس ورودی اون از loadText بوده باشه بیاد یک پروسه جدید رو بر روی Thread جدید ایجاد کنه و اون رو بر روی Main Thread اعمال کنه ، MyInteractor.getMyText هم سیلِ دیتاهایی هست که ما به سیستم وارد می‌کنیم (میتونه یه Api باشه یا یه دیتابیس یا هر چیز دیگه) ، تابع دیگه که صدا زده میشه subscribeViewState هست که از توابع mosby حساب میشه و موجب میشه که یک state جدید بر حسب پروسه ای که اجرا شده ایجاد بشه (دقت کنید بالاتر گفتم ما state ها رو عوض نمی‌کنیم بلکه از اول اونا رو درست می‌کنیم) و در اکتیویتی render بشه .

object MyInteractor
{
 fun getMyText() : Observable<MainViewState>
    {
 return MyRepository.loadText()
            .map <MainViewState>{ MainViewState.DataState(it.text) }
 .startWith(MainViewState.LoadingState)
            .Return { MainViewState.ErrorState(it) }
 }
}

در این قسمت که ما در presenter از اون استفاده کردیم میاییم سیلِ داده ها رو مشخص می‌کنیم ، ابتدا با یک state که در حالت loading است کار رو شروع می‌کنیم (startWith) و اگر به اروری برخوردیم state مناسب اون رو می‌فرستیم (Return ) و اگه همه چیز مرتب بود یک سیلِ دیتا رو برمی‌گردونیم ، در این کد سیلِ دیتا شبیه سازی شده که با MyRepository.loadText انجام میشه :

object MyRepository {
 fun loadText() : Observable<TextModel>
    {
 return Observable.just(TextModel("Hello"))
    }
}
data class TextModel(val text : String)

وقتی که برنامه رو اجرا کنیم عبارت hello رو بر روی صفحه نمایش می‌تونیم ببینم .

مقایسه و جمع بندی

مساله مهمی که ما در MVI از اون بهره‌مند میشم ، اطمینان به پروسه های طی شده هست ، MVI از لحاظی به MVP شبیه هست ، هر دو presneter ای دارند که پروسه ها در اونجا انجام می‌پذیره اما در تغییرات UI در MVI تنها از تابع render صورت می‌گیره ولی در MVP ما ممکنه چندین متد برای این تغییرات داشته باشم ، MVI از لحاظی شبیه MVVM هست چون در جفت این معماری ها مبحث "مشاهده" اتفاقات رو داریم ولی در MVVM تغییرات ViewModel ما از هرجایی میتونه اتفاق بیفته و بعد از مدتی وقتی اگه برنامه خیلی بزرگ بشه به تناسب اون پیچیدگی بسیاری رو هم برای ما میاره .

خب منظور ما اینه که MVI بهترین گزینه برای پیاده سازیه ؟ نه

هیچ معیار مشخصی برای انتخاب "بهترین" معماری نداریم ، همه اینها بستگی به شما و تیمِ شما داره (به قول فیلمِ "مردی که آنجا نبود" از "برادران کوئن" ، طرز نگاه ما به یک پدیده ماهیت اونو عوض می‌کنه) ، به شخصه فعلا قصد استفاده از MVI رو پروژه های شخصی و شرکتی خودم ندارم و منتظر می‌مونم که جامعه توسعه دهندگان بیشتر روی این معماری آزمون و خطا کنند ، MVI در ابتدا آنچنان معماری آسانی برای فهم نیست و به نظرم در حال حاضر معماری هایی مثل MVVM بیشتر در برنامه‌نویسی اندروید محبوبیت دارند (یک نفر جایی در مورد MVI کامنت گذاشته بود که این کلمه مخفف Model View I don't care هست) اما اونچه مشخص هست اینه که یک توسعه دهنده باید انواع تکنولوژی ها رو بدونه تا در آینده و در پروژه های بعدی که قراره انجام بده مشکلی نداشته باشه .

هدف از این مقاله ارائه یک دیدگاه ساده از MVI بود ، شما می‌تونید مثال های پیچیده تر رو در قسمت منابع بررسی کنید ، امیدوارم مفید به فایده بوده باشه ❤️ .

همچنین میتونید این کد رو در گیتِ من ببینید .
https://gitlab.com/drflakelorenzgerman/simple-mvi/tree/sass

منابع :