Поиск по этому блогу

вторник, 6 августа 2019 г.

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

Поговорить об архитектуре всегда интересно, в особенности когда ты ее используешь повсеместно во всех своих приложениях. Сейчас очень популярно и модно везде использовать MVVM архитектуру которую я описывал тут буквально недавно. 
image
Сегодня я хочу поговорить про такой паттерн который используется в большинстве современных приложений, который называется MVP, на его замену пришел уже названный ранее MVVM который является более простым и понятным паттерном, но MVP в современной разработке используется везде где можно и нельзя, и так же она имеет применение в мобильной разработке, которую я так сильно люблю.

Если кратко что такое MVP то вот вам развертка:
— Model — уровень данных. Все называют его как «уровень бизнес логики», но для меня он очень абстрактный и в большинстве случаев его можно интерпретировать как угодно, но только не так что бы было понятно. Поэтому в своих приложениях я называю его Repository и он работает с базой данных или сервером или какой-то внутреней структурой данных.

— View — уровень отображения. Это любая Activity, Fragment или Custom View котоые описывают работу с объектами отображения (TextView, RecyclerView и т.д.). Напомню, что изначально все Android приложения подчинены структуре MVP, где Controller это Activity или Fragment.

— Presenter — прослойка между View и Model. View передаёт ему происходящие события, презентер обрабатывает их, при необходимости обращается к Model и возращает View данные на отрисовку.

Что же мы из этого всего вынесли полезного?
— View — знает о Presenter. Отображает все что возвращает презентер;
— Presenter — знает о View и Model (Repository). Берет данные из модели и отдает во вью;
— Model (Repository) — сама по себе. Она у нас получает данные и отдает тому кто попросит;

Теперь же перейдем к практической части. У нас нужно провести небольшую подготовку прежде чем реализовывать сам паттерн в нашем проекте. По этому давайте подключим библиотеки, создадим модели и подключим работу с сервером.

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'
}
Подключаем Retrofit, RecyclerView и appcompat для работы с сервером, списками и остальными важными частями android sdk.

Дальше по стандарту описываем работу с сервером как это уже было в сотне статей которые я тут описывал, все будет аналогично десятку примеров которые тут были.

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
}
Подготавливаем модель для обработки данных с сервера, по традиции получать будем тайтл и описание, и будем потом его отображать в списке.

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

interface API {
    @get:GET("posts")
    val posts: Call<List<PostModel>>
}
Далее описываем запрос к серверу, в нашем случае мы будем получать список постов, в нашу модельку которую создали выше.

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
        }
    }
}
Дальше создаем класс который абстрагирует работу с сервером, используя интерфейс API, в котором мы описали сам запрос.

Далее, после того как мы описали работу с сервером, нам нужно описать интерфейсы которые будут иметь методы с которыми мы в дальнейшем будем взаимодействовать между объектами самого MVP.

PostRepository.kt
interface PostsRepository {
    fun getPosts()
}
Даный интерфейс говорит о том что в нашем имплементации данного интерфейса будет содержать один метод, который будет получать посты с сервера. 

PostsRepositoryImpl.kt
import dajver.com.mvpexample.api.RestClient
import dajver.com.mvpexample.api.model.PostModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class PostsRepositoryImpl(private val onPostsFetchedListener: OnPostsFetchedListener) : PostsRepository {

    override fun getPosts() {
        RestClient.instance().posts.enqueue(object : Callback<List<PostModel>> {
            override fun onResponse(call: Call<List<PostModel>>, response: Response<List<PostModel>>) {
                if(response.code() < 400) {
                    onPostsFetchedListener.showPosts(response.body()!! as ArrayList<PostModel>)
                }
            }

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

    interface OnPostsFetchedListener {
        fun showPosts(posts: ArrayList<PostModel>)
    }
}
Здесь мы создали класс и заимплементили наш интерфейс, после этого создали метод который описывает работу с сервером, сделали вызов RestClient'a и в onResponse отправляем колбек на подписавшегося на тот коллбек класса, в нашем случае это будет Presenter. Так же в конструкторе мы говорим о том что каждый класс который будет создавать инстанс данного репозитория должен подписаться на коллбек для получения списка постов.

PostPresenter.kt
interface PostsPresenter {
    fun onItemWasClicked(position: Int)
}
В данном интерфейсе мы будем обратаывать клик по айтему в списке. И так же в конструкторе будем подгружать данные с сервера. 

PostPresenterImpl.kt
import dajver.com.mvpexample.api.model.PostModel
import dajver.com.mvpexample.mvc.repository.PostsRepository
import dajver.com.mvpexample.mvc.repository.PostsRepositoryImpl
import dajver.com.mvpexample.mvc.PostsView

class PostsPresenterImpl(private val mView: PostsView) : PostsPresenter, PostsRepositoryImpl.OnPostsFetchedListener {

    private val mRepository: PostsRepository

    private var postModelList: ArrayList<PostModel>? = null

    init {
        mRepository = PostsRepositoryImpl(this)
        mRepository.getPosts()
    }

    override fun showPosts(posts: ArrayList<PostModel>) {
        postModelList = posts
        mView.showPosts(postModelList!!)
    }

    override fun onItemWasClicked(position: Int) {
        mView.onPostClick(postModelList!![position])
    }
}
В конструкторе мы передаем View, потому что наш активити должен содержать имлемент к View, а презентер будет обращаться к этим элементам View и работать с ними. Так же в init методе мы создаем объект Repository и сразу вызываем метод getPosts что бы получить данные с сервера по запуску приложения. И так же на коллбек showPosts, отображаем посты во View (активити) и по клику onItemWasClicked отправляем клик в View (активити).

PostsView.kt
import dajver.com.mvpexample.api.model.PostModel

interface PostsView {
    fun onPostClick(text: PostModel)
    fun showPosts(posts: List<PostModel>)
}
Вот так будут выглядеть три наших интерфейса которые будут взаимодействовать между друг другом. PostsRepository будет получать инфу из сервера и будет передавать ее в PostsPresenter, а PostsView будет отображать с помощью Presenter'a все это безобразие.

Так же нам еще осталось создать адаптер с холдером для отображения списка, опускать уже эту информацию не буду, и нарисую тут как будет выглядеть эти два класса.

PostViewHolder.kt
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import dajver.com.mvpexample.adapter.PostRecyclerAdapter
import dajver.com.mvpexample.api.model.PostModel
import kotlinx.android.synthetic.main.item_posts.view.*

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

    fun bind(postModel: PostModel, onItemCLickListener: PostRecyclerAdapter.OnItemCLickListener?) {
        itemView.name.text = postModel.title
        itemView.content.text = postModel.body

        itemView.setOnClickListener {
            onItemCLickListener!!.onItemCLick(adapterPosition)
        }
    }
}
В айтеме у нас будет два поля, одно будет как тайтл, второе как дескрипшн, собственно что мы в этом холдере и делаем, берем из данной нам модели данные и отображаем в этих текстовых полях. Так же говорим что весь айтем будет кликабельный и мы по к лику передаем позицию в списке.

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

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

    private var onItemCLickListener: OnItemCLickListener? = null

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

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

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

    fun setOnItemClickListener(onItemClickListener: OnItemCLickListener) {
        this.onItemCLickListener = onItemClickListener
    }

    interface OnItemCLickListener {
        fun onItemCLick(position: Int)
    }
}
Стандартно рисуем адаптер, инициализируем холдер, передаем туда список полученый с сервера и лисенер который будет контролировать клики по списку.

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>
Вот так будет выглядеть айтем списка. Два текстовых поля с небольшими отступами.

Далее нам нужно заимплементировать PostsView в нашей MainActivity и подписаться на коллбеки которые будут возвращать нам или респонс или клик по айтему из презентера.

MainActivity.kt
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import dajver.com.mvpexample.adapter.PostRecyclerAdapter
import dajver.com.mvpexample.mvc.PostsView
import dajver.com.mvpexample.api.model.PostModel
import dajver.com.mvpexample.mvc.presenter.PostsPresenter
import dajver.com.mvpexample.mvc.presenter.PostsPresenterImpl
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), PostsView {

    private var mPresenter: PostsPresenter? = null

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

        mPresenter = PostsPresenterImpl(this)
    }

    override fun showPosts(posts: List<PostModel>) {
        val adapter = PostRecyclerAdapter(posts)
        adapter.setOnItemClickListener(object: PostRecyclerAdapter.OnItemCLickListener {
            override fun onItemCLick(position: Int) {
                mPresenter!!.onItemWasClicked(position)
            }
        })
        recyclerView.adapter = adapter
    }

    override fun onPostClick(text: PostModel) {
        Toast.makeText(this, text.body, Toast.LENGTH_LONG).show()
    }
}
В onCreate мы создали инстанс нашего Presenter'a и подписались на колбеки, дальше создали два метода которые у нас будут отображать список (showPosts) и делать какие-то действия по клику на айтем (onPostClick). Собственно эти два метода которые мы описали в интерфейсе PostsView. 

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

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

</LinearLayout>
Наша активити будет в себе содержать только список в котором сразу будет прописан леяут менеджер для удобства что бы не требовалось это делать в коде.

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
Так же нужно еще прописать работу с интернетом в манифесте, и собственно на этом можно запускать и смотреть как работает наше идеальное приложение. 

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

Исходники:
GitHub

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

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