пятница, 5 июля 2019 г.

Проектируем приложение с MVVM

image

Я уже писал про ViewModel у себя в блоге, но я писал немного про другое использование данного класса, там мы рассматривали сохранение состояния данных при вращениии экрана что бы мы не теряли данные. Сейчас же мы поговорим про паттерн MVVM который используется для визуального упрощения кода и устанавливает правила для каждого класса выполнять свою работу.

Что же такое MVVM? Вот выдержка из википедии:
Model-View-ViewModel (т.е. MVVM) — это шаблон архитектуры клиентских приложений, который был предложен Джоном Госсманом (John Gossman) как альтернатива шаблонам MVC и MVP при использовании технологии связывания данных (Data Binding). Его концепция заключается в отделении логики представления данных от бизнес-логики путем вынесения её в отдельный класс для более четкого разграничения.

Что такое MVVM разобрались, теперт давайте разберемся как же это реализуется. В моем примере это будет реализовано через запрос на сервер который будет возвращать нам какие-то данные, а мы в свою очередь будем эти данные получать в модель, и отправлять в адаптер. Выглядит довольно просто, давайте посмотрим как же оно выглядит в реальности.

app/build.gradle
dependencies {
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'

    implementation 'com.squareup.retrofit2:retrofit:2.6.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.0.1'

    implementation "android.arch.lifecycle:extensions:1.1.1"
    implementation "android.arch.lifecycle:viewmodel:1.1.1"
}

Начнем с того что подключим нужные библиотеки, их у нас будет две, это Retrofit — который мы будем использовать для запросов на сервер и LifecycleExtentions который нужен для ипользования ViewModelProviders и других классов.

С библиотеками разобрались, теперь нам нужно создать модель в которую мы будем парсить данные с сервера. Данные мы будем получать с фейкового сервера который я взял отсюда — jsonplaceholder.typicode.com. Там можно найти много разных запросов которые будут возвращать разные данные, очень удобно для написания примеров. На основе респонса запроса к jsonplaceholder.typicode.com/posts создаем модель на сайте www.jsonschema2pojo.org и добавляем эту модель к себе в проект.

PostModel.kt
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

class PostModel {
    @SerializedName("userId")
    @Expose
    var userId: Int? = null
    @SerializedName("id")
    @Expose
    var id: Int? = null
    @SerializedName("title")
    @Expose
    var title: String? = null
    @SerializedName("body")
    @Expose
    var body: String? = null
}

Определяем все нужные параметры которые мы будем использовать, я решил оставить так как оно выглядит в респонсе, что бы не путать, использовать мы будем только title и body.

API.kt
import dajver.com.mvvm.api.model.PostModel
import retrofit2.Call
import retrofit2.http.GET

interface API {

    @get:GET("posts")
    val posts: Call<List<PostModel>>
}

Тут мы описали в какой запрос мы будем стучаться и каким способом, это стандартный способ работы с Retrofit. Если кто не работал с ним советую, у меня даже была статья с примером и описанием работы с этой библиотекой, советую ознакомиться.

RestClient.kt
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory

class RestClient {
    private val service: API

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(ScalarsConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        service = retrofit.create(API::class.java)
    }

    companion object {
        const val BASE_URL = "https://jsonplaceholder.typicode.com/"

        private val instance = RestClient()

        fun instance(): API {
            return instance.service
        }
    }
}

Здесь мы описываем все настройки для ретрофита, создаем инстанс данного класса для удобного его использования, задаем хост на который мы будем стучатьдля получения респонса и прописываем конвертеры для парсинга респонсом.

Теперь давайте создадим наш класс ViewModel который будет стучаться в RestClient и получать данные, а после получения отправлять их в ViewModel.

PostViewModel.kt
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dajver.com.mvvm.api.RestClient
import dajver.com.mvvm.api.model.PostModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class PostViewModel : ViewModel() {

    private var mutableLiveData: MutableLiveData<List<PostModel>>? = null

    fun getPosts() : MutableLiveData<List<PostModel>>? {
        if(mutableLiveData == null) {
            mutableLiveData = MutableLiveData()
            loadPosts()
        }
        return mutableLiveData
    }

    private fun loadPosts() {
        RestClient.instance().posts.enqueue(object : Callback<List<PostModel>> {
            override fun onResponse(call: Call<List<PostModel>>, response: Response<List<PostModel>>) {
                if(response.code() < 400) {
                    mutableLiveData!!.value = response.body()
                }
            }

            override fun onFailure(call: Call<List<PostModel>>, t: Throwable) {
                t.printStackTrace()
            }
        })
    }
}

Мы создали метод loadPosts() который стучится на сервер и получает какой то респонс оттуда, этот респонс он получает в модель List, собственно эту модель мы берем и пихаем в MutableLiveData который хранится в этом классе. А дальше у нас есть метод getPosts() который возвращает нам эту MutableLiveData<List> если он у нас не пустой, а если пустой то он делает запрос на сервер и получает его. В этом случае у вас не будут делаться лишние запросы на сервер когда вы будете гулять между фрагментами или активити, ну и это улучшит перфоманс нашего приложения.

item_posts.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" android:padding="10dp">

    <TextView
            android:text="TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/name" android:textColor="@android:color/black"
            android:textStyle="bold" android:padding="10dp"/>
    <TextView
            android:text="TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/content" android:padding="10dp"
            android:textSize="18sp"/>
</LinearLayout>

Вот так будет выглядеть наш айтем в списке. Просто два текстовых поля.

PostViewHolder.kt
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import dajver.com.mvvm.api.model.PostModel
import kotlinx.android.synthetic.main.view_posts.view.*

class PostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    fun bind(postModel: PostModel) {
        itemView.name.text = postModel.title
        itemView.content.text = postModel.body
    }
}

Теперь создадим ViewHolder для отображения данных в адаптере. У нас будет отображатсья title и body из модели.

PostRecyclerAdapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import dajver.com.mvvm.R
import dajver.com.mvvm.adapter.holder.PostViewHolder
import dajver.com.mvvm.api.model.PostModel

class PostRecyclerAdapter(private val postList: List<PostModel>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return PostViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_posts, parent, false))
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val viewHolder = holder as PostViewHolder
        viewHolder.bind(postList[position])
    }

    override fun getItemCount(): Int {
        return postList.size
    }
}

Ну и здесь уже создаем адаптер для отображения наших данных из респонса.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_gravity="center"
              android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/recyclerView"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:layout_weight="0.1">
    </androidx.recyclerview.widget.RecyclerView>

</LinearLayout>

Активити у нас будет просто с RecyclerView. В XML мы сразу указали что будем использовать LinearLayoutManager для отображения списка.

MainActivity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import dajver.com.mvvm.adapter.PostRecyclerAdapter
import dajver.com.mvvm.api.model.PostModel
import dajver.com.mvvm.viewmodel.PostViewModel
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

        val model = ViewModelProviders.of(this).get(PostViewModel::class.java!!)
        model.getPosts()!!.observe(this,
            Observer<List<PostModel>> {
                val adapter = PostRecyclerAdapter(it)
                recyclerView.adapter = adapter
            })
    }
}

В onCreate() у нас помимо инициализации леяута появилось пара новых строк. Нам они нужны для того что бы мы могли отобразить наш список с актуальными данными которые мы получили из ViewModel, которые были получены с сервера.

Еще надо дать пермишены на доступ в интернет в AndroidManifest.xml и собственно все.

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

Вот такая магия получается у нас, код максимально упрощенный, максимально чистый, любой новые человек в команде посмотрев на него сразу поймет что происходит, как по мне это лучше решение для такого рода задач.

Исходники:
GitHub

Комментариев нет:

Отправить комментарий