معماری MVVM الگوی معماری نرم شناخته شده‌ای است که بر تمام اشکالات الگوهای طراحی MVP و MVC غلبه دارد. MVVM پیشنهاد می کند که منطق ارائه داده‌ها (Views یا UI) را از بخش منطق تجاری اصلی برنامه جدا کنید. در این مقاله از سری مقالات آموزش برنامه نویسی اندروید به ساختن یک اپ به کمک معماری MVVM در اندروید و کاربرد Retrofit خواهیم پرداخت.

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

معماری MVVM (Model-View-ViewModel) یک الگوی طراحی نرم‌افزار است که به کاهش وابستگی بین اجزای مختلف اپلیکیشن کمک می‌کند. این معماری از سه بخش اصلی تشکیل شده است:

  1. مدل (Model)
  2. ویو (View)
  3. ویومدل (ViewModel)

تاریخچه معماری MVVM در اندروید

معماری MVVM اولین بار توسط شرکت مایکروسافت در سال ۲۰۰۵ معرفی شد. این الگو در ابتدا برای توسعه Windows Presentation Foundation (WPF) و Silverlight طراحی شد تا به توسعه‌دهندگان امکان جداسازی منطق تجاری از بخش رابط کاربری را بدهد. هدف اصلی مایکروسافت از ارائه این معماری، بهبود Data Binding در محیط‌های برنامه‌نویسی دسکتاپ بود.

با گسترش فناوری‌های توسعه موبایل، معماری MVVM به یکی از محبوب‌ترین الگوهای طراحی در توسعه اپلیکیشن‌های اندرویدی تبدیل شد. گوگل نیز با معرفی Android Architecture Components، ابزارهایی مانند ViewModel و LiveData را ارائه داد که باعث شد MVVM به یک استاندارد رایج در توسعه اندروید تبدیل شود. امروزه، این معماری یکی از بهترین روش‌ها برای ساخت اپلیکیشن‌های مقیاس‌پذیر، تست‌پذیر و پایدار در اندروید محسوب می‌شود.

لایه‌های مجزای MVVM در اندروید

مدل (Model)

این لایه مسئول مدیریت داده‌ها است و وظیفه انتزاع منابع داده را بر عهده دارد. Model معمولاً شامل کلاس‌های داده (Data Classes) و منابع داده مانند دیتابیس‌ها و APIها می‌شود. در معماری MVVM، لایه Model باید استقلال کامل از View و ViewModel داشته باشد تا تغییرات در UI بر روی آن تأثیری نگذارد. این بخش فقط داده‌ها را پردازش کرده و از جزئیات مربوط به نمایش بی‌اطلاع است. Model و ViewModel برای دریافت و ذخیره داده‌ها با هم کار می‌کنند و داده‌های این بخش معمولاً از طریق Repository Pattern مدیریت می‌شوند.

ویو (View)

این لایه همان UI اپلیکیشن است که با کاربر تعامل دارد و وظیفه نمایش داده‌ها را بر عهده دارد، بدون اینکه منطق پردازش داده را در خود داشته باشد. لایه View مسئول اطلاع‌رسانی به ViewModel درباره فعالیت‌های کاربر است. در اندروید، ویو معمولاً شامل Activity، Fragment و XML Layout است. این بخش با استفاده از Data Binding می‌تواند به‌صورت خودکار تغییرات در داده‌ها را نمایش دهد، بدون اینکه نیازی به بروزرسانی دستی باشد.

ویومدل (ViewModel)

ViewModel یک لایه میانی بین Model و View است که داده‌ها را پردازش کرده و برای نمایش آماده می‌کند. این لایه از LiveData برای مدیریت تغییرات داده و Data Binding برای ارتباط موثر با ویو استفاده می‌کند.

ViewModel داده‌ها را حتی در صورت چرخش صفحه حفظ می‌کند و باعث می‌شود اطلاعات از بین نرود. این لایه به هیچ عنوان به UI وابسته نیست و فقط وظیفه پردازش داده‌ها را بر عهده دارد. برای بهینه‌سازی عملکرد، ViewModel از تکنولوژی‌هایی مانند Coroutines یا RxJava برای پردازش‌های ناهمزمان استفاده می‌کند که باعث بهبود عملکرد اپلیکیشن و کاهش تأخیر در دریافت داده‌ها از سرور یا دیتابیس می‌شود.

معماری MVVM در اندروید

مزایای استفاده از معماری MVVM در اندروید

یکی از مهم‌ترین دلایلی که MVVM در توسعه اپلیکیشن‌های اندرویدی محبوب شده است، مزایای فراوان این معماری است. در ادامه، مهمترین مزایای استفاده از MVVM آمده است:

کاهش وابستگی بین اجزا
در این معماری، View و Model از یکدیگر جدا هستند، بنابراین تغییرات در یکی از این بخش‌ها تأثیری بر دیگری ندارد. این ویژگی باعث توسعه و نگهداری آسان‌تر پروژه می‌شود.

افزایش قابلیت تست‌پذیری
از آنجا که ViewModel به UI وابسته نیست، منطق برنامه را می‌توان به‌راحتی تست کرد و کیفیت کد را افزایش داد.

مدیریت بهینه چرخه حیات
با استفاده از ViewModel، داده‌ها پس از چرخش صفحه (Screen Rotation) از بین نمی‌روند، در نتیجه تجربه کاربری بهتری فراهم می‌شود و از حذف داده‌ها در هنگام تغییر وضعیت صفحه جلوگیری می‌کند.

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

بهبود عملکرد با پردازش‌های ناهمزمان
MVVM با ترکیب LiveData و ابزارهایی مانند RxJava یا Coroutines، مدیریت پردازش‌های ناهمزمان را به طور مؤثری انجام دهد و عملکرد اپلیکیشن را بهبود بخشد.

مقایسه معماری MVVM با MVC و MVP در اندروید

ویژگی‌هاMVVMMVPMVC
تفکیک مسئولیت‌هاقویمتوسطضعیف
قابلیت تست‌پذیریبسیار بالابالامتوسط
وابستگی View به سایر لایه‌هاکممتوسطزیاد
پشتیبانی از Data Bindingداردنداردندارد
مدیریت چرخه حیاتقویمتوسطضعیف
استفاده در پروژه‌های بزرگآسانمتوسطدشوار

ساخت یک اپلیکیشن ساده با معماری MVVM

در ادامه این مقاله می‌آموزیم که چگونه می‌توانیم با استفاده از معماری MVVM در اندروید و زبان Kotlin یک اپلیکیشن ساده برای نام و تصویر فیلم‌ها بسازیم. برای ساخت این اپلیکیشن به معماری MVVM و Retrofit Library نیاز داریم. Retrofit کتابخانه‌ای است که به ما در ایجاد request در اندروید کمک می کند. داده ها را از وب سایت پایگاه داده فیلم (TMDB) واکشی خواهیم کرددر این مثال، ما لیستی از فیلم های محبوب را دریافت می کنیم.

گام‌‎های ایجاد اپ

1- در سایت https://developer.themoviedb.org/reference/movie-popular-list عضو می شویم و وارد سایت می شویم.

2- با انتخاب گزینه API Reference از قسمت بالا سمت چپ وارد صفحه مربوط به درخواست API می شویم.

3- دکمه Get API Key را کلیک می‌کنیم.

4- با پر کردن فرم مشخصات و پذیرش شرایط کلید API و API Read Access Token را دریافت خواهیم کرد.

5- با انتخاب زبان کاتلین در آدرس زیر مشاهده می کنیم که از تابع get() برای request استفاده می‌شود:

https://developer.themoviedb.org/reference/intro/getting-started

6- با جاگذاری API Key در قسمت AUTHORIZATION و فشردن دکمه Try It! و دریافت پاسخ 200 در خروجی JSON خواهیم داشت که آن را یادداشت می‌نماییم.

7- اکنون پروژه‌ای جدید در اندروید استودیو ایجاد می‌کنیم.

8- چون می‌خواهیم از اینترنت استفاده کنیم سه permission زیر را در  AndroidManifest می‌نویسیم و از کاربر آنها را دریافت می‌کنیم:

<uses-permission android:name=”android.permission.INTERNET”/>
<uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE”/>
<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE”/>

9- کتابخانه Retrofit را در فایل Build.gradle(app) اضافه می کنیم:

//add retrofit library

implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

رتروفیت (Retrofit) کتابخانه‌ای بسیار قدرتمند جهت اتصال به سرور و ارتباط با API سمت سرور است. این کتابخانه توسط شرکت Square پشتیبانی می‌شود و مورد تایید گوگل است . 

10- کتابخانه‌های View Model و Live Data را نیز به فایل فوق اضافه می‌کنیم:

def lifecycle_version = "2.6.0-alpha01"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
implementation group: 'androidx.lifecycle', name: 'lifecycle-extensions', version: '2.2.0'

LiveData  به ما کمک میکند تا به جای بررسی مداوم برای تغییرات در منبع اطلاعات، هر زمان تغییری ایجاد شد به طور خودکار رابط کاربری از آن آگاهی پیدا کرده و تغییرات لازم در UI صورت گیرد. ViewModel نیز در این راه به ما کمک بسیاری میکند.

11- برای کار با تصاویر کتابخانه‌ Glide  را نیز به فایل مزبور اضافه می‌کنیم:

implementation 'com.github.bumptech.glide:glide:4.13.2'

12- برای فعال کردن view binding، این کد را داخل بلوک Android در فایل build.gradle(app) اضافه می‌کنیم:

buildFeatures {
    viewBinding = true
}

13- اکنون به کمک پلاگین JSON To Kotlin Class در اندروید استودیو و با راست کلیک روی package اصلی اپ و انتخاب گزینه New و سپس زیر منوی Kotlin-data class from JSON وpaste مقدار JSON دریافتی از سایت developer.themoviedb.org درون پنجره Generate Kotlin Data Class Code دو کلاس داده‌ای (data class) به نام‌های Movies و Result ایجاد خواهد شد. (اگر پلاگین فوق را درون اندروید استودیو ندارید به مسیر File -> Settings -> Plugins بروید و پلاگین را نصب کنید.)

data class Movies(
    val page: Int,
    val results: List<Result>,
    val total_pages: Int,
    val total_results: Int
)


data class Result(
    val adult: Boolean,
    val backdrop_path: String,
    val genre_ids: List<Int>,
    val id: Int,
    val original_language: String,
    val original_title: String,
    val overview: String,
    val popularity: Double,
    val poster_path: String,
    val release_date: String,
    val title: String,
    val video: Boolean,
    val vote_average: Double,
    val vote_count: Int
)

در حقیقت در کلاس بالا فقط از poster_path برای یافتن مسیر پوستر هر فیلم و title برای نمایش عنوان آن استفاده می‌کنیم.

14- روی package اصلی راست کلیک کرده و اینترفیس MovieApi را با توجه به گام 5 ایجاد می کنیم:

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface MovieApi {
    @GET("popular?")
    fun getPopularMovies(@Query("api_key") api_key : String) : Call<Movies>

}

در interface  بالا Query با دریافت api_ky از متد get برای ارسال request استفاده می‌کند.

15- دوباره روی package فوق راست کلیک می‌کنیم و object زیر را می‌سازیم:

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitInstance {
    val api : MovieApi by lazy {
        Retrofit.Builder()
            .baseUrl(
                "https://api.themoviedb.org/3/movie/"
            )
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(MovieApi::class.java)
    }
}

برای پیاده سازی الگوی طراحی Singleton از object  استفاده می‌کنیم. در کاتلین object کلاس خاصی است که فقط یک نمونه دارد.  Singleton  یک الگوی طراحی است که تضمین می‌کند که یک کلاس فقط یک نمونه داشته باشد و یک نقطه دسترسی سراسری به شیء را فراهم می‌کند.  این الگو باعث صرفه‌جویی در منابع ram می‌شود. در object بالا از lazy برای پیاده سازی Singleton استفاده کرده‌ایم.

در اینجا یک متغیر val به نام api از نوع MovieApi ساخته می‌شود و درون این ساختار از کتابخانه Retrofit استفاده می‌کنیم تا به کمک interface به نام MovieApi و توسط متد get درون آن ساختاری JSON شکل دریافت کنیم و به وسیله GsonConverterFactory آن را تبدیل ‌کنیم.

gson متدهای ساده toJson() و fromJson() را برای تبدیل اشیاء جاوا به JSON و بالعکس ارائه می کند  و GsonConverterFactory مبدلی است که از Gson برای سریال سازی (serialization ) به JSON و بالعکس استفاده می کند.

16- در مسیر  app > res > layout > activity_main.xml   کد زیر را اضافه می‌کنیم تا خروجی‌ها که شامل نام و تصویر فیلم‌ها هستند درون یک recyclerview به نمایش درآیند:

<?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=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_movies"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:listitem="@layout/movie_layout">
    </androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

17- برای recycler view یک فایل layout جدید درون پوشه res->layout به نام movie_layout ایجاد می کنیم تا حاوی ImageView و TextView برای نمایش نام و تصویر هر فیلم‌ باشد:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
        android:id="@+id/movieImage"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:scaleType="fitCenter"
        android:src="@color/teal_200"
        android:contentDescription="@string/image_of_movies"/>
    <TextView
        android:id="@+id/movieName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/movieImage"
        android:textSize="30sp"
        android:text="@string/movie_name"
        android:textAlignment="center"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_marginTop="5dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

18- یک کلاس Movie Adapter برای RecyclerView ایجاد می کنیم. در این کلاس از Glide برای نمایش تصویر پوستر هر فیلم استفاده می‌کنیم. در این کلاس از آرایه movieList استفاده می‌شود که آرایه‌ای از اشیا Result هست و هر عضو آن دارای خاصیت رشته‌ای poster_path است که با اضافه شدن به مسیر https://image.tmdb.org/t/p/w500 مسیر تصویر پوستر فیلم در سایت را به Glide برای قراردادن در imageview مربوطه در recyclerview می‌دهد. خاصیت دیگر هر عضو title هست که برای متن textview مربوطه در recyclerview به کار می‌رود. چون با دیگر فیلدها کاری نداریم می‌توانستیم هنگام تبدیل از JSON باقی فیلدهای کلاس را حذف کنیم.

import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.mvvmmoviedetail.databinding.MovieLayoutBinding
class MovieAdapter : RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
    private var movieList = ArrayList<Result>()
    @SuppressLint("NotifyDataSetChanged")
    fun setMovieList(movieList: List<Result>) {
        this.movieList = movieList as ArrayList<Result>
        notifyDataSetChanged()
    }
    class ViewHolder(val binding: MovieLayoutBinding) : RecyclerView.ViewHolder(binding.root) {}
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            MovieLayoutBinding.inflate(
                LayoutInflater.from(
                    parent.context
                )
            )
        )
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        Glide.with(holder.itemView)
            .load("https://image.tmdb.org/t/p/w500" + movieList[position].poster_path)
            .into(holder.binding.movieImage)
        holder.binding.movieName.text = movieList[position].title
    }
    override fun getItemCount(): Int {
        return movieList.size
    }
}

19- یک کلاس View Model با live-data ایجاد می‌کنیم تا از معماری MVVM درون اپ خود استفاده کنیم، باید درون این اپ از apikey دریافتی استفاده کنیم و آن را به object فوق پاس دهیم. باتوجه به Response های دریافتی گزینه‌هایی خواهیم داشت:

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MovieViewModel : ViewModel() {
    private var movieLiveData = MutableLiveData<List<Result>>()
val apiKey = "69d66957eebff9666ea46bd464773cf0"
  fun getPopularMovies() {
      RetrofitInstance.api.getPopularMovies(apiKey).enqueue(object  : Callback<Movies>{
          override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
                    if (response.body()!=null){
                        movieLiveData.value = response.body()!!.results
                    }
              else{
                  return
                    }
          }
          override fun onFailure(call: Call<Movies>, t: Throwable) {
                Log.d("TAG",t.message.toString())
          }
      })
  }
    fun observeMovieLiveData() : LiveData<List<Result>> {
        return movieLiveData
    }
}

20- درون MainActivity از کد زیر استفاده می‌کنیم. در این کد تابع prepareRecyclerView برای استفاده از فایل Adapter و تنظیم RecyclerView به کار رفته است:

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
class MainActivity : AppCompatActivity() {  
    private lateinit var binding : ActivityMainBinding
    private lateinit var viewModel: MovieViewModel
    private lateinit var movieAdapter : MovieAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        prepareRecyclerView()
        viewModel = ViewModelProvider(this)[MovieViewModel::class.java]
        viewModel.getPopularMovies()
        viewModel.observeMovieLiveData().observe(this, Observer { movieList ->
          movieAdapter.setMovieList(movieList)
        })
}
private fun prepareRecyclerView() {
        movieAdapter = MovieAdapter()
        binding.rvMovies.apply {
          layoutManager = GridLayoutManager(applicationContext,2)
           adapter = movieAdapter
        }
    }
}

خروجی اپ باید به شکل زیر خواهد بود:

پروژه نهایی ساخت اپ با معماری MVVM در اندروید

در این مقاله به آموزش معماری MVVM در اندروید در قالب پروژه ساخت اپ پرداختیم. چنانچه هرگونه سوال در این‌باره دارید میتوانید از قسمت دیدگاه ها با ما درمیان بگذارید.