Академический Документы
Профессиональный Документы
Культура Документы
MVP
MVP расшифровывается как Model-View-Presenter (модель-представление-презентер). View
— это, например, Activity, которое отображает какие-то данные с сервера, а Model — это
классы по работе с сервером. Напрямую View и Model не взаимодействуют. Для этого
используется посредник — Presenter.
Таким образом, Presenter — это логика, вынесенная из Activity в отдельный класс. Activity —
только для отображения данных и взаимодействия с пользователем. Если требуется
сделать другое Activity для отображения данных, то не нужно будет переносить логику в
новое Activity, можно будет использовать готовый Presenter. А если требуется поменять
логику, то не нужно будет лезть в Activity и там, среди кода, который отвечает за
отображение данных и взаимодействие с пользователем, искать логику и менять ее.
Достаточно поменять код в Presenter.
Rx Java
Для начала подготовим некоторые библиотеки. Во первых, для загрузки данных с сервера
нам понадобится библиотеки RxJava, RxKotlin, RxAndroid.
1 dependencies {
2 ...
3 //rx
4 implementation 'io.reactivex.rxjava2:rxkotlin:2.1.0'
5 implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
6 implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
7 ...
8 }
9
RxJava — это не просто передача данных из Observable в Observer. RxJava имеет огромную
коллекцию операторов, которую вы можете использовать для фильтрации, слияния и
преобразования этих данных.
MPAndroidChart
Библиотека MPAndroidChart будет использоваться для построения графика курса
криптовалюты и отображения данных на графике. Для добавления ее в проект пропишите
такие строки в build.gradle (Project):
1 allprojects {
2 repositories {
3 ...
4 maven { url 'https://jitpack.io' }
5 }
6 }
7
И в build.gradle (App):
1 dependencies {
2 ...
3
4 //chart
5 implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha'
6 ...
7 }
Вспомогательные функции и классы
Создайте в главном пакете файл Ext.kt:
1 package info.fandroid.top100currencies
2
3 import java.text.SimpleDateFormat
4 import java.util.*
5
6 fun Float.formatThousands() : String {
7 val sb = StringBuilder()
8 val formatter = Formatter(sb, Locale.US)
9 formatter.format("%(,.0f", this)
10 return sb.toString()
11 }
12
13 fun Number.dateToString(pattern: String): String {
14 val calendar = Calendar.getInstance()
15 calendar.timeInMillis = this.toLong()
16 return SimpleDateFormat(pattern).format(calendar.time)
17 }
18
Здесь две внешних функции для форматирования чисел и дат. Первую мы будем
использовать в списке криптовалют для разделения порядков в числах, а вторую для
перевода дат из UNIX в строковый формат, для использования на графике.
1 package info.fandroid.top100currencies.formatters
2
3 import com.github.mikephil.charting.components.AxisBase
4 import com.github.mikephil.charting.formatter.IAxisValueFormatter
5 import info.fandroid.top100currencies.dateToString
6 import java.util.*
7
8 class YearValueFormatter : IAxisValueFormatter {
9
10
11
12 override fun getFormattedValue(value: Float, axis: AxisBase?): String {
13 val calendar = Calendar.getInstance()
14 calendar.timeInMillis = value.toLong()
15 return calendar.toFormatted()
16 }
17
18 fun Calendar.toFormatted(): String {
19 val date = this.timeInMillis
20 return date.dateToString("MMM yyyy")
21 }
22 }
23
Этот класс нужен для преобразования даты с помощью вышеупомянутой внешней функции
в строковый формат для отрисовки легенды оси X графика цены криптовалюты.
MVP классы
Приступим к созданию базовых абстрактных элементов паттерна MVP, которые будут
использоваться через т.н. контракт – базовый элемент каждого компонента приложения,
который сочетает в себе как действия элемента View, так и действия элемента Presenter.
Контракт – достаточно гибкий элемент, состав которого варьируется от компонента к
компоненту, увязывая компоненты в рамках единой архитектуры.
1 package info.fandroid.top100currencies.mvp.contract
2
3 import io.reactivex.disposables.CompositeDisposable
4 import io.reactivex.disposables.Disposable
5
6 class BaseContract {
7
8 interface View
9
10 abstract class Presenter<V: View> {
11 private val subscriptions = CompositeDisposable()
12 protected lateinit var view: V
13
14
15 fun subscribe(subscription: Disposable) {
16 subscriptions.add(subscription)
17 }
18
19 fun unsubscribe() {
20 subscriptions.clear()
21 }
22
23 fun attach(view: V) {
24 this.view = view
25 }
26
27 fun detach() {
28 unsubscribe()
29 }
30
31 }
32 }
33
1 package info.fandroid.top100currencies.mvp.contract
2
3 import info.fandroid.top100currencies.adapter.CurrenciesAdapter
4
5 class CurrenciesContract {
6 interface View : BaseContract.View {
7 fun addCurrency(currency: CurrenciesAdapter.Currency)
8 fun notifyAdapter()
9 fun showProgress()
10 fun hideProgress()
11 fun showErrorMessage(error: String?)
12 fun refresh()
13 }
14
15 abstract class Presenter: BaseContract.Presenter<View>() {
16 abstract fun makeList()
17 abstract fun refreshList()
18 }
19 }
20
Аналогично в LatestChartContract.kt:
1 package info.fandroid.topcrypts.mvp.contract
2
3 import info.fandroid.top100currencies.mvp.contract.BaseContract
4
5 class LatestChartContract {
6 interface View : BaseContract.View {
7 fun addEntryToChart(value: Float, date: String = "")
8 fun addEntryToChart(date: Float, value: Float)
9 fun showProgress()
10 fun hideProgress()
11 fun showErrorMessage(error: String?)
12 fun refresh()
13 }
14
15 abstract class Presenter: BaseContract.Presenter<View>() {
16 abstract fun makeChart(id: String)
17 abstract fun refreshChart()
18 }
19 }
20
В соответствии с концепцией MVP, элемент view должен быть максимально “тупым”, в нем
мы производим только элементарные действия, такие, как, например, добавить элементы в
список/на график, показать/спрятать значок загрузки и т.д. Методы, соответствующие этому
элементу, мы определяем в интерфейсе View контракта. Логикой мы займемся в
презентере – бизнес-логика, манипуляции данными, запуск фоновых задач и т.д. В нем же
мы решаем, когда показать те или иные элементы на экране. Методы, соответствующие
презентеру, мы определяем в абстрактном классе Presenter контракта. При реализации
презентера, манипуляции видом будут производиться через переменную view суперкласса
BaseContract.Presenter.
Presenters
Прежде чем создавать презентеры для списка и графика, добавьте такие строки в файл
AppComponent:
И в файл MvpModule:
1 @Provides
2 @Singleton
3 fun provideCurrenciesPresenter(): CurrenciesPresenter = CurrenciesPresenter()
4
5 @Provides
6 @Singleton
7 fun provideLatestChartPresenter(): LatestChartPresenter = LatestChartPresenter()
8
Среда разработки будет ругаться на эти строки, так как классы презентеров пока
отсутствуют. Дальнейшие действия все исправят.
1 package info.fandroid.top100currencies.mvp.presenter
2
3
4 import info.fandroid.top100currencies.adapter.CurrenciesAdapter
5 import info.fandroid.top100currencies.di.App
6 import info.fandroid.top100currencies.formatThousands
7 import info.fandroid.top100currencies.mvp.contract.CurrenciesContract
8 import info.fandroid.top100currencies.rest.CoinGeckoApi
9 import io.reactivex.Observable
10 import io.reactivex.android.schedulers.AndroidSchedulers
11 import io.reactivex.schedulers.Schedulers
12 import javax.inject.Inject
13
14 class CurrenciesPresenter : CurrenciesContract.Presenter() {
15
16 //внедряем источник данных
17 @Inject
18 lateinit var geckoApi: CoinGeckoApi
19
20 //инициализируем компоненты Даггера
21 init {
22 App.appComponent.inject(this)
23
24 }
25
26 //создаем список, загружая данные с помощью RxJava
27 override fun makeList() {
28 view.showProgress()
29
30 //подписываемся на поток данных
31 subscribe(geckoApi.getCoinMarket()
32
33 //определяем отдельный поток для отправки данных
34 .subscribeOn(Schedulers.io())
35
36 //получаем данные в основном потоке
37 .observeOn(AndroidSchedulers.mainThread())
38
39 //преобразуем List<GeckoCoin> в Observable<GeckoCoin>
40 .flatMap { Observable.fromIterable(it) }
41
42 //наполняем поля элемента списка для адаптера
43 .doOnNext {
44 view.addCurrency(
45 CurrenciesAdapter.Currency(
46 it.id,
47 it.symbol,
48 it.name,
49 it.image,
50 it.current_price,
51 it.market_cap.formatThousands(),
52 it.market_cap_rank,
53 it.total_volume,
54 it.price_change_percentage_24h,
55 it.market_cap_change_percentage_24h,
56 it.circulating_supply,
57 it.total_supply,
58 it.ath,
59 it.ath_change_percentage
60 )
61 )
62 }
63
64 //вызывается при вызове onComplete
65 .doOnComplete {
66 view.hideProgress()
67 }
68
69 //подписывает Observer на Observable
70 .subscribe({
71 view.hideProgress()
72 view.notifyAdapter()
73 }, {
74 view.showErrorMessage(it.message)
75 view.hideProgress()
76 it.printStackTrace()
77 })
78 )
79 }
80
81
82 //обновляем список
83 override fun refreshList() {
84 view.refresh()
85 makeList()
86 }
87 }
88
Далее оператор flatMap. Функция fromIterable получает список и создает из него Observable
с отдельными элементами списка. Таким образом из Observable<List<GeckoCoin>> мы
получим Observable<GeckoCoin>. Оператор flatMap раскрывает получившийся
Observable<GeckoCoin> и отправляет его элементы далее в поток.
Оператор doOnNext вызывается каждый раз, когда источник Observable излучает очередной
объект GeckoCoin. В этом операторе мы наполняем поля элемента списка данными из
объекта GeckoCoin и отдаем их адаптеру.
Функция refreshList() будет обновлять список, вызывая функцию создания списка снова.
При заполнении элемента списка данными мы используем больше полей, чем будет
отображено в списке. Например, поля market_cap_rank, total_volume,
price_change_percentage_24h не используются для отображения в списке. Но, поскольку мы
получаем эти данные в ответе сервера, удобно их сразу сохранить в объекте элемента
списка, чтобы не запрашивать их потом снова, когда они понадобятся. А понадобятся они
нам в окне детальной информации о каждой криптовалюте, которое мы будем открывать по
щелчку на элементе списка. Также вы можете самостоятельно модифицировать список,
чтобы отобразить там свой набор данных. Список мы создавали во втором уроке.
1 package info.fandroid.topcrypts.mvp.presenter
2
3 import info.fandroid.top100currencies.di.App
4 import info.fandroid.top100currencies.rest.CoinGeckoApi
5 import info.fandroid.topcrypts.mvp.contract.LatestChartContract
6 import io.reactivex.Observable
7 import io.reactivex.android.schedulers.AndroidSchedulers
8 import io.reactivex.schedulers.Schedulers
9 import javax.inject.Inject
10
11 class LatestChartPresenter : LatestChartContract.Presenter() {
12
13 @Inject
14 lateinit var geckoApi: CoinGeckoApi
15
16
17 init {
18 App.appComponent.inject(this)
19 }
20
21 override fun makeChart(id: String) {
22
23 subscribe(geckoApi.getCoinMarketChart(id)
24
25
26 .map { it.prices }
27
28 .flatMap { Observable.fromIterable(it) }
29
30 .doOnComplete {
31
32 view.hideProgress()
33 }
34 .subscribeOn(Schedulers.io())
35 .observeOn(AndroidSchedulers.mainThread())
36 .subscribe({
37 view.hideProgress()
38 view.addEntryToChart(it[0], it[1])
39
40 }, {
41 view.hideProgress()
42 view.showErrorMessage(it.message)
43 it.printStackTrace()
44 })
45 )
46
47 }
48
49 override fun refreshChart() {
50 view.refresh()
51
52 }
53
54 }
55
Отображение списка
Теперь, когда мы подготовили все что нужно, реализуем отображение списка криптовалют в
приложении.
Код BaseListFragment.kt:
1 package info.fandroid.top100currencies.fragments
2
3 import android.os.Bundle
4 import android.support.v4.app.Fragment
5 import android.support.v7.widget.LinearLayoutManager
6 import android.support.v7.widget.RecyclerView
7 import android.view.View
8 import info.fandroid.top100currencies.adapter.BaseAdapter
9 import kotlinx.android.synthetic.main.fragment_currencies_list.*
10
11 abstract class BaseListFragment : Fragment() {
12
13 private lateinit var recyclerView: RecyclerView
14 protected lateinit var viewAdapter: BaseAdapter<*>
15 private lateinit var viewManager: RecyclerView.LayoutManager
16
17 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
18 super.onViewCreated(view, savedInstanceState)
19 viewManager = LinearLayoutManager(context)
20 viewAdapter = createAdapterInstance()
21
22 recyclerView = list.apply {
23 setHasFixedSize(true)
24 layoutManager = viewManager
25 adapter = viewAdapter
26 }
27 }
28
29 abstract fun createAdapterInstance(): BaseAdapter<*>
30 }
31
Код фрагмента:
1 package info.fandroid.top100currencies.fragments
2
3
4 import android.os.Bundle
5 import android.view.LayoutInflater
6 import android.view.View
7 import android.view.ViewGroup
8 import android.widget.TextView
9 import android.widget.Toast
10
11 import info.fandroid.top100currencies.R
12 import info.fandroid.top100currencies.adapter.BaseAdapter
13 import info.fandroid.top100currencies.adapter.CurrenciesAdapter
14 import info.fandroid.top100currencies.di.App
15 import info.fandroid.top100currencies.mvp.contract.CurrenciesContract
16 import info.fandroid.top100currencies.mvp.presenter.CurrenciesPresenter
17 import kotlinx.android.synthetic.main.activity_main.*
18 import javax.inject.Inject
19
20
21 class CurrenciesListFragment : BaseListFragment(), CurrenciesContract.View {
22
23 @Inject
24 lateinit var presenter: CurrenciesPresenter
25
26 override fun onCreateView(
27 inflater: LayoutInflater, container: ViewGroup?,
28 savedInstanceState: Bundle?
29 ): View? {
30 // Inflate the layout for this fragment
31 return inflater.inflate(R.layout.fragment_currencies_list, container, false)
32 }
33
34 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
35 super.onViewCreated(view, savedInstanceState)
36 App.appComponent.inject(this)
37 presenter.attach(this)
38 presenter.makeList()
39 }
40
41 override fun createAdapterInstance(): BaseAdapter<*> {
42 return CurrenciesAdapter()
43 }
44
45 override fun addCurrency(currency: CurrenciesAdapter.Currency) {
46 viewAdapter.add(currency)
47 }
48
49 override fun notifyAdapter() {
50 viewAdapter.notifyDataSetChanged()
51 }
52
53 override fun showProgress() {
54 requireActivity().progress.visibility = View.VISIBLE
55 }
56
57 override fun hideProgress() {
58 requireActivity().progress.visibility = View.INVISIBLE
59 }
60
61 override fun showErrorMessage(error: String?) {
62 Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
63 }
64
65 override fun refresh() {
66 viewAdapter.items.clear()
67 viewAdapter.notifyDataSetChanged()
68 }
69
70 override fun onResume() {
71 super.onResume()
72 presenter.attach(this)
73 }
74
75 override fun onPause() {
76 super.onPause()
77 presenter.detach()
78 }
79
80 }
81