Создание приложения Kotlin с шаблоном MVP.

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

Контекст нашего приложения

Чтобы показать MVP в реальном приложении, мы создадим приложение-библию. Это будет офлайн-приложение KJV. UI будет 3 простых выпадающих меню (Spinner).

Один для Книги Библии, другой для главы Библии и последний для стиха из Библии.

Затем прокручиваемый вид (Recyclerview) в раскрывающихся меню, в котором отображается книга.

В результате вы получите не самое красивое приложение. Но так как это была спешная работа по объяснению этой концепции, терпите меня.

Настройка приложения

Я нашел в Интернете файл Библии KJV и написал код, чтобы преобразовать его в запрашиваемый формат. Итак, у меня в папке с ресурсами 3 файла - biblebooks.json, bibleChapter.json и bibleverse.json.

Все это 3 массива JSON со следующими объектами в схеме этого типа.

BibleBooks - {
  "abbrev": "gn",
  "bookname": "Genesis",
  "noOfChapter": 50,
  "bookid": 1
}

БиблияГлавы

BibleChapters - {
"chapterid": 1,
"noOfVerses": 31,
"bookid": 1
},

Библейские стихи -

{"verseid":22,"chapterid":1,"versetext":"And God gave them his blessing, saying, Be fertile and have increase, making all the waters of the seas full, and let the birds be increased in the earth.","bookid":1}

Каждый стих содержит bookId и ChapterID. Каждая глава содержит bookId.

Зависимости приложений

Это приложение использует Dagger2 для внедрения зависимостей, Realm для базы данных и ButterKnife для внедрения представления. Знание этих библиотек не требуется для понимания этого проекта.

Теперь к MVP

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

Вот базовый контракт MVP, наследуемый каждым представлением (действием или фрагментом) в нашем приложении для Android.

interface MVPContract {
    interface View 
    interface Presenter<V : View> {
        fun getView(): V?
        fun attachView(view: V)
        fun detachView()
    }
    interface Component<V : View, out P : Presenter<V>> {
        fun presenter(): P
    }
}

У нас есть View (Activity или Fragment). Они могут содержать любое собственное представление (TextView, ViewPager, NavigationView) и т. Д., Нам все равно, что они содержат.

У нас есть докладчик для каждого просмотра. Каждому Presenter создается экземпляр View Object, который он представляет. Он имеет 3 основные функции. Прикрепите вид, отсоедините его и верните ссылку на него.

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

У нас есть компонент. Это используется Dagger 2. Я никогда не пишу никаких приложений без внедрения зависимостей. Он вставляет Presenter в представление (действие / фрагмент). Я хотел обойтись без этого здесь, но эта часть меня не останавливалась.

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

Они здесь.

abstract class MVPDaggerRealmActivity<V : MVPContract.View, out P : MVPContract.Presenter<V>,
        out C : MVPContract.Component<V, P>> : AppCompatActivity(), MVPContract.View {
    protected val presenter: P by lazy { component.presenter() }
    protected val component: C by lazy { createComponent() }
    lateinit var realm: Realm
    protected abstract fun createComponent(): C
    @Suppress("UNCHECKED_CAST")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        presenter.attachView(this as V)
        realm = Realm.getDefaultInstance()
    }
    override fun onDestroy() {
        super.onDestroy()
        presenter.detachView()
        realm.close()
    }
}

Базовый класс Fragment MVP также выглядит как актив, за исключением того, что он расширяет

: Fragment(), MVPContract.View

Вместо AppCompatActivity.

MVPDaggerRealmActivity расширяет AppCompatActivity и реализует MVPContact.View. Представления и взаимодействия представлений будут определены в представлении.

Имеет 2 защищенных поля.

1 ведущий, который будет внедрен компонентом кинжала 2.

2 компонент dagger 2, который будет создан в конкретном классе, расширяющем этот класс, путем предоставления реализации функции CreateComponent.

Наконец, наша бизнес-логика

Пропуск MainActivity и переход к фрагменту Библии.

Вот наш Библейский договор.

interface BibleContract {
    interface View : MVPContract.View {
        fun showInitialView(bookId : Int, chapterId: Int)
        fun getChapterByBook(bookId: Int?, chaptersInBook: RealmResults<BibleChapter>)
        fun getVersesByChapter(bookId: Int?, chapterId: Int?, versesInChapter : RealmResults<BibleVerses>)
    }
    interface Presenter<V : View> : MVPContract.Presenter<V> {
        val bibleBooks: RealmResults<BibleBooks>
        fun getBibleChaptersByBookId(bookId: Int?): RealmResults<BibleChapter>
        fun getBibleVerseByBookIdAndChapterId(bookId: Int, chapterId: Int): RealmResults<BibleVerses>
    }
    interface Component<V : View, out P : Presenter<V>> : MVPContract.Component<V, P>
}

Это определяет договор между нашим приложением BibleView и моделью через Bible Presenter.

Мы видим, что в нашем обзоре Библии будет показано первоначальное представление. Он также может получать главы по bookId и стихи по chapterId. Все они определены здесь, потому что это основные вещи, которые, как ожидается, будет делать представление.

Мы будем использовать спиннеры на 1 фрагменте (одном экране). Завтра мы можем решить создать новый модный пользовательский интерфейс, который будет включать 3 экрана и множество нажатий кнопок и анимации. Независимо от того, что мы делаем, нам все равно понадобятся эти 3 функции в представлении, потому что в конце дня мы должны показать начальное представление, отобразить главы в выбранном представлении и отобразить стихи в конкретном выбранном стихе.

Наш ведущий

class BiblePresenter @javax.inject.Inject constructor(var context: Context, var realm: Realm)
    : MVPPresenter<BibleContract.View>(), BibleContract.Presenter<BibleContract.View> {
    override fun getBibleVerseByBookIdAndChapterId(bookId: Int, chapterId: Int): RealmResults<BibleVerses> = realm.where(BibleVerses::class.java)
            .equalTo("bookid", bookId)
            .equalTo("chapterid", chapterId)
            .sort("verseid", Sort.ASCENDING)
            .findAll()
    override fun getBibleChaptersByBookId(bookId: Int?): RealmResults<BibleChapter> = realm.where(BibleChapter::class.java)
            .equalTo("bookid", bookId)
            .sort("chapterid", Sort.ASCENDING)
            .findAll()
    override val bibleBooks: RealmResults<BibleBooks>
        get() = realm.where(BibleBooks::class.java).findAll()
}

Наш BiblePresenter реализует MVPPresenter в классе BibleContract. Он предоставляет список книг Библии, список глав Библии, отфильтрованных по идентификатору книги, и список библейских стихов в главе.

Здесь происходит сбор, преобразование и взаимодействие с базой данных.

Наконец, наш взгляд (фрагмент Библии)

class BibleFragment : MVPDaggerFragment<BibleContract.View, BiblePresenter, BibleComponent>(), BibleContract.View {

Наш BibleFragment реализует MVPDaggerFragment и реализует BibleContract.View.

override fun createComponent(): BibleComponent = DaggerBibleComponent.builder()
        .appComponent(App.component)
        .biblePresenterModule(BiblePresenterModule(activity!!, (activity as MainActivity).realm))
        .build()
@BindView(R.id.bible_book_spinner)
@JvmField
var bibleBooksSpinner: Spinner? = null

Вот кинжал, выполняющий создание экземпляра компонента Библии, и нож ButterKnife, вводящий взгляды во фрагменты.

Как я уже сказал, собственное представление Android определяет, как они принимают свои данные. Спиннер принимает данные через адаптер массива.

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

val bibleBooks = presenter.bibleBooks
val adapter = ArrayAdapter<BibleBooks>(activity, android.R.layout.simple_spinner_item, bibleBooks)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
bibleBooksSpinner?.adapter = adapter

Наш фрагмент реализует интерфейс BibleContract.View и должен реализовывать его функции.

В случае с текущим пользовательским интерфейсом, который мы используем, 3 счетчика и Recyclerview, который отображает текст стиха.

InitialView

override fun showInitialView(bookId : Int, chapterId: Int) {
    val result = presenter.getBibleVerseByBookIdAndChapterId(bookId, chapterId)
    bibleReadAdapter = BibleAdapter(activity as MainActivity, result, 1)
    readBibleRecyclerView!!.adapter = bibleReadAdapter
}

В нашем первоначальном обзоре мы установили для bookId и chapterId значение 1. при первом запуске приложение загружает Бытие 1. Все стихи в книге берутся из функции ведущего getBibleVerseByBookIdAndChapterId.

val result = presenter.getBibleVerseByBookIdAndChapterId(bookId, chapterId)

мы устанавливаем список стихов в библейский адаптер и устанавливаем адаптер recyclerview на библейский адаптер

bibleReadAdapter = BibleAdapter(activity as MainActivity, result, 1)
readBibleRecyclerView!!.adapter = bibleReadAdapter

GetChapterByBookId и GetVersesByChapterId

override fun getChapterByBook(bookId: Int?, chaptersInBook: RealmResults<BibleChapter>) {
    val adapter = ArrayAdapter<BibleChapter>(activity, android.R.layout.simple_spinner_item, chaptersInBook)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
    chapterSpinner!!.adapter = adapter
    chapterSpinner!!.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, l: Long) {
            if (position >= 0) {
                val chapterId = chaptersInBook[position]?.chapterid
                val result = displayVerseAndRespondToClicks(bookId, chapterId, 1) //Start from the first verse
                getVersesByChapter(bookId, chapterId, result)
            }
        }
        override fun onNothingSelected(adapterView: AdapterView<*>){        }
    }
    chapterSpinner!!.setSelection(0)
}

GetChapterByBookId используется прядильщиком библии. ArrayAdapter создается с раскрывающимся списком.

val adapter = ArrayAdapter<BibleChapter>(activity, android.R.layout.simple_spinner_item, chaptersInBook)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)

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

val chapterId = chaptersInBook[position]?.chapterid
val result = displayVerseAndRespondToClicks(bookId, chapterId, 1) //Start from the first verse
getVersesByChapter(bookId, chapterId, result)
chapterSpinner!!.setSelection(0)

Выпадающее меню chapterId похоже на раскрывающееся меню bookId.

Когда щелкают раскрывающийся список verseId, он просто прокручивается до нужной позиции.

readBibleRecyclerView?.scrollToPosition(verseId)

Заключение.

Мы видим, как этот шаблон упрощает нашу разработку и отделяет представления от пользовательского интерфейса. Контракт ничего не знает о нативных представлениях (3 спиннера и recyclerview). Он касается только того, что представление должно получить от модели, и докладчик доступен для доставки данных в него.

Мы можем миллион раз реорганизовать наш пользовательский интерфейс. Единственное, что изменится, - это наш Фрагмент и, возможно, Библейский договор. Просмотр в зависимости от того, какие собственные представления мы используем, и от того, насколько сложно мы проектируем наш поток приложений. Нам не придется переписывать представления каждый раз, когда мы используем новые представления с другими механизмами доступа к данным.

Надеюсь, вам понравилась статья.