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

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

Model: این لایه وظیفه انتزاع منابع داده را بر عهده دارد. Model و ViewModel برای دریافت و ذخیره داده ها با هم کار می کنند.

View: هدف این لایه اطلاع رسانی به ViewModel در مورد فعالیت کاربر است. این لایه ViewModel را مشاهده می کند و حاوی هیچ نوع منطق برنامه نیست.

ViewModel: جریان‌های داده‌ای را که مربوط به View هستند را نشان می‌دهد. علاوه بر این، به عنوان یک پیوند بین Model و View عمل می کند.

قبل از ادامه صحبتمان باید به این نکته اشاره کنم که در دوره جامع آموزش برنامه نویسی اندروید وبسایت دانجشویار بطور جامع و کاربردی معماری 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 در اندروید