Вы находитесь на странице: 1из 14

В этом уроке мы внедрим в проект архитектурный паттерн MVP, будем получать с сервера

и обрабатывать поток данных посредством Rx Kotlin и наконец, реализуем отображение


списка криптовалют в нашем приложении.

MVP
MVP расшифровывается как Model-View-Presenter (модель-представление-презентер). View
— это, например, Activity, которое отображает какие-то данные с сервера, а Model — это
классы по работе с сервером. Напрямую View и Model не взаимодействуют. Для этого
используется посредник — Presenter.

Activity дергает Presenter, который запрашивает данные у Model и передает их в Activity,


которое отображает их на экране.

Таким образом, Presenter — это логика, вынесенная из Activity в отдельный класс. Activity —
только для отображения данных и взаимодействия с пользователем. Если требуется
сделать другое Activity для отображения данных, то не нужно будет переносить логику в
новое Activity, можно будет использовать готовый Presenter. А если требуется поменять
логику, то не нужно будет лезть в Activity и там, среди кода, который отвечает за
отображение данных и взаимодействие с пользователем, искать логику и менять ее.
Достаточно поменять код в Presenter.
Rx Java
Для начала подготовим некоторые библиотеки. Во первых, для загрузки данных с сервера
нам понадобится библиотеки RxJava, RxKotlin, RxAndroid.

Добавьте в build.gradle (App):

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

Все библиотеки используют реализацию библиотеки ReactiveX с открытым исходным


кодом, которая помогает создавать приложения в стиле реактивного программирования.
Хотя RxJava предназначена для обработки синхронных и асинхронных потоков данных, она
не ограничивается «традиционными» типами данных. Определение «данных» RxJava
довольно широко и включает такие вещи, как кеши, переменные, свойства и даже
пользовательские события ввода, такие как щелчки и нажатия.

RxJava расширяет шаблон разработки программного обеспечения Observer, основанный на


концепции наблюдателей и наблюдаемых. Чтобы создать базовый конвейер данных
RxJava, необходимо:

Создать Observable (наблюдаемый).

Дать Observable некоторые данные.

Создать Observer (наблюдатель).

Подписать Observer на Observable.


Как только Observable имеет хотя бы один подписанный Observer, он начнет “излучать”
данные. Каждый раз, когда Observable излучает часть данных, он уведомляет об этом
назначенный Observer, вызывая метод onNext(), и Observer затем обычно выполняет
некоторые действия в ответ на эту эмиссию данных. Как только Observable закончит
выдавать данные, он уведомит Observer, вызвав onComplete(). После этого Observable
завершит работу, и поток данных завершится. Если возникает исключение, тогда
вызывается onError(), и Observable будет немедленно завершен без каких-либо
дополнительных данных или вызова onComplete().

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

Также создайте класс YearValueFormatter.kt:

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

В главном пакете создайте пакеты mvp/contract и в последнем класс BaseContract.kt:

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

Это базовый класс, в нем определен абстрактный презентер и его методы —


подписка/отписка на поток данных, прикрепление/открепление View.
От базового класса наследуем View и Presenter в классе CurrenciesContract.kt:

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

Интерфейс View содержит функции добавления новых валют в список, оповещения


адаптера об изменениях списка, отображении /скрытии прогрессбара, отображения ошибки
и функцию обновления. Презентер содержит функции создания и обновления списка
криптовалют.

Аналогично в 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:

1 fun inject(presenter: CurrenciesPresenter)


2 fun inject(presenter: LatestChartPresenter)
3

И в файл MvpModule:

1 @Provides
2 @Singleton
3 fun provideCurrenciesPresenter(): CurrenciesPresenter = CurrenciesPresenter()
4
5 @Provides
6 @Singleton
7 fun provideLatestChartPresenter(): LatestChartPresenter = LatestChartPresenter()
8

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

В пакете mvp/presenter создаем презентер для списка криптовалют CurrenciesPresenter.kt:

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

Внедряем зависимость — источник данных CoinGeckoApi при помощи аннотации @Inject


Даггера. Инициализируем его компоненты в теле функции init.
Функция makeList() отображает прогрессбар и запускает Rx-цепочку подписки на данные и
обработки их в процессе. Рассмотрим ее более подробно.

Функция subscribe принимает объект CoinGeckoApi, получающий данные от сервера


посредством вызова функции getCoinMarket(), которая возвращает
Observable<List<GeckoCoin>>.

Оператор subscribeOn(Schedulers.io()) определяет отдельный поток для отправки данных.


По умолчанию Observable отправляет свои данные в поток, в котором была объявлена ​
подписка, т. е. где вызван оператор .subscribe. В Android это, как правило, основной поток
пользовательского интерфейса. Но получение данных в основном потоке приведет к
зависанию интерфейса. Чтобы не блокировать основной поток, можно использовать
оператор subscribeOn() для определения другого Scheduler где Observable должен
выполняться и излучать свои данные.

В свою очередь оператор observeOn() мы используем вместе с планировщиком


AndroidSchedulers.mainThread() для того, чтобы обрабатывать результат в основном потоке
и показать список валют в пользовательском интерфейсе приложения.

Далее оператор flatMap. Функция fromIterable получает список и создает из него Observable
с отдельными элементами списка. Таким образом из Observable<List<GeckoCoin>>  мы
получим Observable<GeckoCoin>. Оператор flatMap раскрывает получившийся
Observable<GeckoCoin> и отправляет его элементы далее в поток.

Оператор doOnNext вызывается каждый раз, когда источник Observable излучает очередной
объект GeckoCoin. В этом операторе мы наполняем поля элемента списка данными из
объекта GeckoCoin и отдаем их адаптеру.

Оператор doOnComplete вызывается при событии onComplete(), которое происходит при


успешном возврате данных, в отличие от события onError(), которое бросает исключение.

Завершает Rx-цепочку оператор subscribe, который, по сути, связывает Observable и


Observer, подписывая наблюдатель на поток данных. Здесь мы скрываем прогрессбар и
оповещаем адаптер об изменениях списка.

Ниже прописана обработка исключения в случае ошибки, скрываем прогрессбар и


отображаем сообщение об ошибке.

Функция refreshList() будет обновлять список, вызывая функцию создания списка снова.

При заполнении элемента списка данными мы используем больше полей, чем будет
отображено в списке. Например, поля market_cap_rank, total_volume,
price_change_percentage_24h не используются для отображения в списке. Но, поскольку мы
получаем эти данные в ответе сервера, удобно их сразу сохранить в объекте элемента
списка, чтобы не запрашивать их потом снова, когда они понадобятся. А понадобятся они
нам в окне детальной информации о каждой криптовалюте, которое мы будем открывать по
щелчку на элементе списка.  Также вы можете самостоятельно модифицировать список,
чтобы отобразить там свой набор данных. Список мы создавали во втором уроке.

Теперь презентер для графика LatestChartPresenter.kt:

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

Здесь все аналогично предыдущему презентеру. Единственное отличие — используется


оператор map вместо flatMap, поскольку функция geckoApi.getCoinMarketChart(id)
возвращает не список, а Observable<GeckoCoinChart>, и нам не нужно его разворачивать.

Отображение списка
Теперь, когда мы подготовили все что нужно, реализуем отображение списка криптовалют в
приложении.
Код 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

Здесь в теле функции onViewCreated, которая выполняется после создания визуального


представления фрагмента, инициализируем адаптер, создаем список и присваиваем
адаптер списку.

Теперь перепишем класс фрагмента CurrenciesListFragment.kt

Не обращайте внимания в видео на функцию showList — она не нужна в приложении и


была удалена в процессе рефакторинга. Сверяйтесь с кодом и исходниками ниже.

Код фрагмента:

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

Для инъекции зависимостей во фрагменте добавьте такую функцию в AppComponent:

1 fun inject(fragment: CurrenciesListFragment)

Класс CurrenciesListFragment наследуем от BaseListFragment() и реализовываем функцию


createAdapterInstance(): BaseAdapter<*>, которая возвращает CurrenciesAdapter().

Также расширим текущий фагмент классом CurrenciesContract.View и реализуем его


функции. В теле функции addCurrency() добаляем новые элементы в список. В
notifyAdapter() оповещаем адаптер об зменении списка. В showProgress() и hideProgress()
отображаем и прячем прогрессбар соответственно. В showErrorMessage() отображаем тост
с ошибкой. И функция refresh() будет вызываться при обновлении списка, здесь очищаем
адаптер и сообщаем ему о необходимости обновления списка.

Чтобы презентер не пересоздавался вместе с фрагментом при повороте экрана,


отсоединяем его в onPause() и присоединяем в onResume().

Осталось заинжектить презентер и переопределить функцию onViewCreated(), где инжектим


AppComponent, присоединяем к фрагменту презентер и вызываем его функцию создания
списка.

Теперь запустите приложение на устройстве, должен открыться список из 100 криптовалют,


отсортированный по капитализации, как мы его получаем с сервера.

Вам также может понравиться