ورود / ثبت نام سبد خرید 0
زمان آن رسیده که در برنامه نویسی اندروید یک معماری جدیدی استفاده کنیم که با استفاده از DataBinding پیاده سازی میشود. در این مقاله ابتدا توضیحات و مفاهیم اصلی معماری mvvm به شما آموزش داده میشود سپس یک مثال واقعی بصورت قدم به قدم برای ساختن یک پروژه ی ساده با معماری mvvm انجام خواهیم داد پس تا آخر این مقاله ی آموزشی همراه باشید
همیشه در برنامه نویسی شیوه های مختلفی بوجود می آید که هدف اکثر این شیوه ها راحت تر کردن برنامه نویسی ، منظم کردن آن و در نهایت افزایش بهره وری یک برنامه نویس است. هر چند که تعدادی از برنامه نویسان همیشه رویه ی خود را دارند و تن به این سیستم ها نمیدهند اما باید بدانید که برای افزایش سرعت و کارآیی در پروژه های متوسط به بالا بهتر است از یک اصولی استفاده شود. اصولی که اکثرا برنامه نویسان برآن پایبند هستند. مثلا MVC در بحث وب ، به بسیاری از پروژه ها کمک اساسی کرد فریمورک های مختلفی برپایه ی MVC به وجود آمد که کدها را منظم تر و آسان تر کرده بود. برنامه نویسی اندروید نیز جزو آن دسته از حوزه هایی هست که نامنظمی های خود را دارد و وقتی پروژه ای از اندازه ی متوسط خود فراتر میرود بهم ریختگی زیاد میشود و وجود یک سیستم برای ساماندهی کدها ضروری است. در این مقاله به معماری MVVM میپردازیم.
برای شروع بهتر است بدانیم که MVVM مخفف Model-View-ViewModel است و یک نوع الگوی معماری هست که توسط John Gossman معرفی شده تا در هنگام استفاده از Data Binding جایگزین الگوهای قدیمی مثل MVC و MVP باشد.
مفهوم MVVM کلا این است که در پروژه های برنامه نویسی، قسمت presentation logic از business logic جدا باشد که این کار با انتقال آن به کلاس خاص انجام میشود.
خب پس این سه عبارت در عنوان MVVM چه معنی میدهد ؟ در لیست زیر ببینید :
معماری MVVM چه قابلیت هایی به ما میدهد ؟
از آنجا که هیچ چیزی صد در صد کامل نیست MVVM نیز معایبی دارد :
در ابتدا که معماری MVVM را شروع میکنید این الگو نیاز به تغییراتی در ساختار اندروید دارد. در واقع بازنگری قسمت های مختلف و استفاده سنتی آنها ضروری است. برای مثال بیایید Activity ساده ی اندرویدی را در نظر بگیریم.
هر اکتیویتی یک فایل layout دارد که از نوع XML است و یک کلاس متصل به آن که JAVA است. به نظرتون فایل xml همان view ما و فایل Java همان ViewModel ما هست ؟ کاملا اینطور نیست.
اگر بگوییم فایل Java ما هم یک View است چطور ؟
به هر حال custom view دارای هم xml است و هم کلاس java ولی آنها بصورت واحد در نظر گرفته میشوند.البته بدون فایل لیتوت xml هم میتوانید کار کنید ولی باید ویجت های ضروری را با استفاده از کد بسازید.
بنابراین میتونیم نتیجه بگیریم که در این معماری Activity همان View هست ( فایل xml + کلاس java ).
ولی سوال اینجاست که ViewModel چیست و جای آن کجاست ؟
در حقیقت ViewModel یک آبجکت کاملا جدا است و آن چیزیست که ما به فایل xml با استفاده از متود binding.setViewModel میفرستیم که دارای فیلد ها و متودهایی هست که ما بتوانیم view ها را با model ها bind کنیم.
در اینجا Model ها همان تعریف و مفهوم سنتی خود را دارد ولی چیزی که میخام بهش اضافه کنم اینه که در ViewModel مستقیما به دیتابیس یا API ها ، مستقیما اشاره نکنید.در عوض ، برای هر VM یک Repository ایجاد کنید ، اینطوری کد تمیزتر و کم حجم تر خواهد بود.
بصورت کلی اگر بخواهیم بدانیم در MVVM کی کجاست ؟ بهتره یه مثال بزنیم . فرض کنید شما بعنوان یک مشتری به یک رستوران میروید گارسون از شما سفارش غذا میگیرد و به آشپز اعلام میکند و آشپز بعد از آماده سازی غذا آنرا به گارسون میدهد تا گارسون به دست شما برساند. آیا شما بعنوان مشتری آشپز را دیدید ؟ خیر ، ولی محصول آشپز که همان غذاست به دست شما رسیده است .
در معماری MVVM مشتری همان View است و گارسون همان ViewModel هست که واسطه ی بین شما و آشپز بود و در نهایت model همان آشپز است و غذایی که دست همه چرخیده و توسط گارسون به دست شما رسیده همان داده ها یا DATA اپلیکیشن است. آشپز که همان Model ما بود با استفاده از وسایلی که در آشپزخانه است غذا را آماده میکند. در این بخش آشپزخانه را بعنوان ریپوزیتوری در نظر بگیرید.
اگر قصد دارید اپلیکیشن اندرویدی خود را با استفاده از معماری mvvm پیاده سازی کنید دو راه دارید یکی استفاده از data binding و دیگری استفاده از Rx.java
در این مقاله ی آموزشی قصد ما استفاده از data binding هست .
وقتی میخواهیم از MVVM در اپ اندرویدی استفاده کنیم معماری پروژه ی اندرویدی ما مشابه تصویر زیر میباشد :
در ادامه به نحوه ی کدنویسی و پیاده سازی یک پروژه ی ساده بر مبنای معماری MVVM میپردازیم. برای تمرین ادامه ی این مقاله ی آموزشی بهتر است ابتدا یک پروژه ی خام و جدید در محیط اندروید استودیو بسازید و سپس طبق کدهای زیر جلو بروید.
ابتدا وابستگی های زیر را به build.gradle پروژه ی خود اضافه کنید :
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'android.arch.lifecycle:extensions:1.1.1' implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.amitshekhar.android:rx2-android-networking:1.0.2' implementation 'io.reactivex.rxjava2:rxjava:2.2.18' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
سپس روی گزینه ی sync بزنید و حتما اتصال شما به اینترنت با IP ایران نباشد ( چون برای دانلود بعضی از پکیج ها تحریم هستیم ) و سپس منتظر بمانید تا گردل عمل بیلد و بازسازی پروژه را تمام بکنه.
ساختار اولیه و ابتدایی پروژه های MVVM در محیط اندروید استودیو باید به شکل زیر باشد :
بسته ی utils که در تصویر بالا میبینید را به شکل زیر تنظیم کنید :
package com.mindorks.framework.mvvm.utils enum class Status { SUCCESS, ERROR, LOADING }
خب حالا به یک کلاس نیاز داریم که وضعیت شبکه را به لایه ی UI بفرستد و نام آن کلاس رو Resource میزاریم.
پس یک دیتاکلاس از نوع کاتلین بسازید و اسم اون رو Resource بزارید. این کلاس باید درون پکیج utils قرار بگیره.
package com.mindorks.framework.mvvm.utils data class Resource<out T>(val status: Status, val data: T?, val message: String?) { companion object { fun <T> success(data: T?): Resource<T> { return Resource(Status.SUCCESS, data, null) } fun <T> error(msg: String, data: T?): Resource<T> { return Resource(Status.ERROR, data, msg) } fun <T> loading(data: T?): Resource<T> { return Resource(Status.LOADING, data, null) } } }
در این صورت پکیج utils ما آماده است.
در ادامه قصد داریم لایه ی data را بسازیم.
حالا پاسخ API ما که بصورت جیسان ارائه شده به شکل زیر خواهد بود :
[ { "id": "1", "name": "Mrs. Nedra Gerhold", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/to_soham/128.jpg", "email": "Lonzo6@hotmail.com" }, { "id": "2", "name": "Spencer McKenzie", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/irae/128.jpg", "email": "Josiah.Hane@gmail.com" } ]
براساس این خروجی API ما باید کلاس data خودمون را پیاده سازی کنیم.
حالا داخل پکیج model که بالاتر ساختیم یک کلاس کاتلین به اسم User بسازید.
کلاس User که ساختیم باید به شکل زیر باشه :
package com.mindorks.framework.mvvm.data.model import com.google.gson.annotations.SerializedName data class User( @SerializedName("id") val id: Int = 0, @SerializedName("name") val name: String = "", @SerializedName("email") val email: String = "", @SerializedName("avatar") val avatar: String = "" )
حالا نیازه که ما یک لایه برای Network درست کنیم .
کدهای درون ApiService باید به شکل زیر باشد :
package com.mindorks.framework.mvvm.data.api import com.mindorks.framework.mvvm.data.model.User import io.reactivex.Single interface ApiService { fun getUsers(): Single<List<User>> }
سپس باید یک کلاس جدیدی بسازیم به اسم ApiServiceImpl که از اینترفیسی که بالا ساختیم ارث بری کند نام اینترفیس ApiService بود پس باید کدهای این کلاس به شکل زیر باشد :
package com.mindorks.framework.mvvm.data.api import com.mindorks.framework.mvvm.data.model.User import com.rx2androidnetworking.Rx2AndroidNetworking import io.reactivex.Single class ApiServiceImpl : ApiService { override fun getUsers(): Single<List<User>> { return Rx2AndroidNetworking.get("https://5e510330f2c0d300147c034c.mockapi.io/users") .build() .getObjectListSingle(User::class.java) } }
حالا یک کلاس جدیدی بسازید به اسم ApiHelper که درون پکیج api قرار دارد و محتویات آن به شکل زیر است :
package com.mindorks.framework.mvvm.data.api class ApiHelper(private val apiService: ApiService) { fun getUsers() = apiService.getUsers() }
درون data یک پکیج دیگری بسازید به نام repository و درون این پکیج یک کلاس جدیدی به اسم MainRepository بسازید که محتویاتش را از کد زیر استفاده کنید :
package com.mindorks.framework.mvvm.data.repository import com.mindorks.framework.mvvm.data.api.ApiHelper import com.mindorks.framework.mvvm.data.model.User import io.reactivex.Single class MainRepository(private val apiHelper: ApiHelper) { fun getUsers(): Single<List<User>> { return apiHelper.getUsers() } }
حالا لایه ی data ما آماده است.
حالا به مرحله ی نهایی رسیده ایم و در این بخش باید یک لایه برای UI درست کنیم و سپس پروژه را build و اجرا کنیم. کارهایی که در این بخش باید انجام بدهیم :
درون viewmodel یک کلاس کاتلین جدید به نام MainViewModel بسازید که دارای کدهای زیر میباشد :
package com.mindorks.framework.mvvm.ui.main.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mindorks.framework.mvvm.data.model.User import com.mindorks.framework.mvvm.data.repository.MainRepository import com.mindorks.framework.mvvm.utils.Resource import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers class MainViewModel(private val mainRepository: MainRepository) : ViewModel() { private val users = MutableLiveData<Resource<List<User>>>() private val compositeDisposable = CompositeDisposable() init { fetchUsers() } private fun fetchUsers() { users.postValue(Resource.loading(null)) compositeDisposable.add( mainRepository.getUsers() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ userList -> users.postValue(Resource.success(userList)) }, { throwable -> users.postValue(Resource.error("Something Went Wrong", null)) }) ) } override fun onCleared() { super.onCleared() compositeDisposable.dispose() } fun getUsers(): LiveData<Resource<List<User>>> { return users } }
ما در اینجا از LiveData استفاده میکنیم.
حالا بیایید لیوت xml خودمون رو بسازیم.
توی فولدر لیوت ها فایل 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" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
حالا در فولدر layout هاا یک فایل جدید item_layout.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:id="@+id/container" android:layout_width="match_parent" android:layout_height="60dp"> <ImageView android:id="@+id/imageViewAvatar" android:layout_width="60dp" android:layout_height="0dp" android:padding="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <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_marginLeft="8dp" android:layout_marginTop="4dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/imageViewAvatar" 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>
یک پکیج جدید درون main بسازید با نام adapter و درونش یک کلاس جدید به اسم MainAdapter درست کنید که شامل کدهای زیر باشه :
package com.mindorks.framework.mvvm.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.mvvm.R import com.mindorks.framework.mvvm.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) } }
درون پکیج ui یک پکیج دیگری به اسم base بسازید و سپس درون آن یک فایل کلاس به نام ViewModelFactory بسازید و محتویات زیر را درون آن بنویسید :
package com.mindorks.framework.mvvm.ui.base import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.mindorks.framework.mvvm.data.api.ApiHelper import com.mindorks.framework.mvvm.data.repository.MainRepository import com.mindorks.framework.mvvm.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 خودمان را تکمیل بکنیم :
package com.mindorks.framework.mvvm.ui.main.view import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.mindorks.framework.mvvm.R import com.mindorks.framework.mvvm.data.api.ApiHelper import com.mindorks.framework.mvvm.data.api.ApiServiceImpl import com.mindorks.framework.mvvm.data.model.User import com.mindorks.framework.mvvm.ui.base.ViewModelFactory import com.mindorks.framework.mvvm.ui.main.adapter.MainAdapter import com.mindorks.framework.mvvm.ui.main.viewmodel.MainViewModel import com.mindorks.framework.mvvm.utils.Status import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var mainViewModel: MainViewModel private lateinit var adapter: MainAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setupUI() setupViewModel() setupObserver() } private fun setupUI() { recyclerView.layoutManager = LinearLayoutManager(this) adapter = MainAdapter(arrayListOf()) recyclerView.addItemDecoration( DividerItemDecoration( recyclerView.context, (recyclerView.layoutManager as LinearLayoutManager).orientation ) ) recyclerView.adapter = adapter } private fun setupObserver() { mainViewModel.getUsers().observe(this, Observer { when (it.status) { Status.SUCCESS -> { progressBar.visibility = View.GONE it.data?.let { users -> renderList(users) } recyclerView.visibility = View.VISIBLE } Status.LOADING -> { progressBar.visibility = View.VISIBLE recyclerView.visibility = View.GONE } Status.ERROR -> { //Handle Error progressBar.visibility = View.GONE Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() } } }) } private fun renderList(users: List<User>) { adapter.addData(users) adapter.notifyDataSetChanged() } private fun setupViewModel() { mainViewModel = ViewModelProviders.of( this, ViewModelFactory(ApiHelper(ApiServiceImpl())) ).get(MainViewModel::class.java) } }
و در آخر چون این اپلیکیشن از اینترنت استفاده میکند مجوز دسترسی به اینترنت را در فایل AndroidManifest اعمال کنید :
<uses-permission android:name="android.permission.INTERNET"/>
تبریک . حالا شما یک اپلیکیشن ساده در معماری MVVM با زبان کاتلین ساخته اید البته در جاوا نیز پیاده سازی به این شکل است که ما برای ساده تر کردن کدها از کاتلین استفاده کردیم. حالا میتوانید اپ را build کنید و نتیجه ی کار خود را ببینید.
مقالات دیگر مرتبط با این مقاله ی آموزشی :
دوره های آموزشی مرتبط با این مقاله :