Статья о библиотеках, позволившие мне обходиться без паттерна MVP и XML-разметки в android-приложениях.
Мотивация
Только начиная разрабатывать серьезные приложения под Android (Java) на работе, я унаследовал код ярого php-шника. Первые 2 недели разбора функционального стиля проекта вогнали меня в ад программиста, и я начал сомневаться в своих силах. «Если каждый проект написан таким образом, вряд ли я стану кодером», — думал я. Но спустя некоторое время на работу устроился будущий мой наставник, который и познакомил меня с паттерном MVP. Это было бальзамом для моей души, я полюбил программирование больше всего. Но на одном MVP я долго не продержался.
Я усваивал технологии за технологиями: Kotlin, Retrofit, RxJava, Dagger и т.д. Каждые из них утоляли мой технологический голод, которого хватало ненадолго. Постоянные прыжки между файлами верстки (xml), моделями, презентерами, вьюшками сильно утомляло. Перманентный рендер образа несчастной одной страницы (модуля), описанная в 4-6 файлах вгоняло в меня страх. Я прокрастинировал, работа вошла в стагнацию, такая перспектива меня не устраивала. До тех пор, пока я не познакомился с библиотекой anko. У меня не хватало слов описать восхищение этой технологией. Anko позволяет верстать страницу с помощью DSL, не прибегая к xml. Это позволило мне сократить описание работы модуля (страницы) до 2-3 файлов. Но статья не о ней, а о ее продолжении. Интернет заполнен статьями и примерами работы с anko.
Прошло некоторое время, и в anko я не нашел покой. Меня теперь не устраивало, что на описание одной textView больше 3 строк кода, что для несчастного листа (RecyclerView) мне придется каждый раз создавать адаптер. Я решил написать собственную библиотеку, под названием Koatl. Основная идея в библиотеке: по возможности на одну частицу верстки (textView, button, etc) использовать не больше 1-2 строк. Продемонстрирую это в приложении-словаре для примера.
Практика
Ссылка на github для первой части.
В Android Studio создайте новый проект с поддержкой Kotlin. Нет необходимости в xml файлах, поэтому можете снять галочку с опции «Generate Layout File» в последней странице создании проекта
buildscript {
ext.kotlin_version = '1.2.40'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.brotandos.dictionary"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'com.github.Brotandos:koatl:v0.1.1'
}
import android.annotation.SuppressLint
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import org.jetbrains.anko.frameLayout
class MainActivity : AppCompatActivity() {
private val fragManager = supportFragmentManager
private val container = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
frameLayout { id = container }
// If there are any instances saved, return
if (savedInstanceState != null) return
// else run default fragment
changeFragment(DictionaryFragment())
}
@SuppressLint("PrivateResource")
private fun changeFragment(f: Fragment, needToCleanStack: Boolean = false) {
if (needToCleanStack) clearBackStack()
fragManager.beginTransaction()
.setCustomAnimations(
R.anim.abc_fade_in,
R.anim.abc_fade_out,
R.anim.abc_popup_enter,
R.anim.abc_popup_exit)
.replace(container, f)
.addToBackStack(f::class.simpleName)
.commit()
}
private fun clearBackStack() {
if (fragManager.backStackEntryCount == 0) return
val first = fragManager.getBackStackEntryAt(0)
fragManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
override fun onBackPressed() {
if (fragManager.backStackEntryCount > 1) fragManager.popBackStack()
else finish()
}
}
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Следующие файлы вставьте в ту же папку, где и MainActivity
data class Dictionary(var title: String, val items: MutableList<DictionaryItem>)
data class DictionaryItem(val key: String, val value: String)
object G {
object Color {
const val CARD_SHADOW_1: Int = 0xFFEEEEEE.toInt()
const val CARD_SHADOW_2: Int = 0xFFDDDDDD.toInt()
const val CARD: Int = 0xFFFEFEFE.toInt()
const val PRIMARY: Int = 0xFF3F51B5.toInt()
}
}
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.view.View
import com.brotandos.koatlib.times
import org.jetbrains.anko.dip
fun Context.cardBg(color: Int, radius: Int = dip(2)) = GradientDrawable() * {
shape = GradientDrawable.RECTANGLE
cornerRadius = radius.toFloat()
setColor(color)
}
fun View.layerCard(color: Int = G.Color.CARD) = LayerDrawable(arrayOf(
context.cardBg(G.Color.CARD_SHADOW_1, dip(4)),
context.cardBg(G.Color.CARD_SHADOW_2, dip(3)),
context.cardBg(color)
)) * {
setLayerInset(0, 0, 0, 0, 0)
setLayerInset(1, 0, 0, 0, dip(1))
setLayerInset(2, dip(1), dip(1), dip(1), dip(2))
}
val bgLayerCard: View.() -> Unit = {
background = layerCard()
}
Вот и сам демонстративный код фрагмента словаря:
import android.graphics.Color
import android.support.v7.widget.RecyclerView
import android.widget.TextView
import com.brotandos.koatlib.*
import org.jetbrains.anko.imageResource
import org.jetbrains.anko.matchParent
import org.jetbrains.anko.sdk25.coroutines.onClick
class DictionaryFragment: KoatlFragment() {
private val dictionary: Dictionary
private val icCollapsed = R.drawable.ic_collapsed
private val icExpanded = R.drawable.ic_expanded
private lateinit var vList: RecyclerView
init {
val list = mutableListOf<DictionaryItem>()
for (i in 0 until 20) list += DictionaryItem("key-$i", "value-$i")
dictionary = Dictionary("First dictionary", list)
}
// Все веселье начинается здесь
override fun markup() = KUI {
// Многие view частицы начинаются с маркера 'v'. Ниже LinearLayout с вертикальной ориентацией
vVertical {
// FrameLayout, bg(colorRes: Int) - лямбда-функция, которая изменяет background частицы
vFrame(bg(R.color.colorPrimary)) {
// TextView, здесь функция-расширение Float.sp изменяет размер текста
// функция lp - сокращенное от layoutParams
// submissive означает width = wrapContent и height = wrapContent
// g5 - gravity = Gravity.CENTER.
// Аттрибут гравитации в библиотеке описан по принципу кнопок телефона
// 1 - слева-наверху, 2 - центр-вверх, 5 - середина, 456 - середина вертикали и т.д.
vLabel(dictionary.title, 10f.sp, text(Color.WHITE)).lp(submissive, g5)
}.lparams(matchParent, 50.dp) // Надеюсь здесь Int.dp интуитивно понятно
// Ниже верстается RecyclerView. Моя самая любимая часть библиотеки
// Позволяет отказаться от создания адаптеров
vList = vList(linear).forEachOf(dictionary.items) {
item, i -> // item - текущий объект, i - позиция
// bgLayerCard - описана внутри Styles.kt.
// Это моя попытка внедрить CSS концепцию стилизации view-частиц
// mw - сокращенное от width = matchParent и height = wrapContent
vVertical(bgLayerCard, mw) {
lateinit var vValue: TextView
// content456 то же, что и gravity = Gravity.CENTER_VERTICAL
// Концепция телефонных кнопок удобна тем,
// что благодаря ей легче представлять расположение дочерних view-частиц
vLinear(content456) {
vImage(icCollapsed) {
onClick {
if (this@vImage.resourceId == icCollapsed) {
imageResource = icExpanded
vValue.visible()
} else {
imageResource = icCollapsed
vValue.hidden()
}
}
}.lp(row, 5f()) // row - то же, что и width = matchParent и height = wrapContent
// функция Float.invoke() у дочерних частиц LinearLayout'а означает weight
//
vLabel(item.key).lp(row, 1f())
}.lp(dominant) // dominant - width = matchParent, height = matchParent
// hidden - visibility = View.GONE
vValue = vLabel(item.value, text(G.Color.PRIMARY), hidden).lp(dominant, 1f(), m(2.dp))
}.llp(row, m(2.dp)) // функция m(number: Int) то же, что и margin
}.lp(row, m(5.dp))
}
}
}
Пока остановимся здесь. Думаю и здесь хватает чего обсудить. Вот сама библиотека. В следующей статье я напишу про библиотеку с «квантовыми» частицами.
Так как библиотека писалась под самого себя, то и документации тоже нет, некоторые (а может и многие) фичи остались неописанными. Моим друзьям понравились библиотеки, поэтому я решился на публикацию. Решил заняться документацией, как только библиотеки выйдут в люди.
Комментарии (8)
Brotandos Автор
28.04.2018 23:32Спасибо за непрошеный совет. Ради этого я написал библиотеку и статью.
kapmayn
29.04.2018 12:22>использовать не больше 1-2 строк
>давайте все параметры определять в одной строке, это же так удобно читаетсяBrotandos Автор
29.04.2018 12:25Успокойтесь, никто не запрещал вам прописывать параметры в несколько строк. Такая опция стоит по умолчанию.
Похоже вы не знакомы с CSS. Концепция параметров в одну строку не моя, я подсмотрел у от концепции CSS. Я лишь адаптировал под DSL и android в виде лямбда-функций.kapmayn
29.04.2018 13:05как-то вы резко реагируете, я абсолютно спокоен
1) с css я знаком, но то, что там так можно, не делает ведь этот подход правильным и не означает, что такое нужно в android. в java тоже можно весь код в одну строку написать, но так не делают.
2) какой тогда смысл в вашей библиотеке, если всё равно большинство будет писать в несколько строк? Получается так, что выигрыша в длине кода от вашей либы как минимум мало
3) возможно, пропустил, но чем отличается lp от lparams?
4) аббревиатуры, типа mw, — это плохие сокращения, ибо нечитаемо
5) ладно, mw — сокращенное от width = matchParent и height = wrapContent ещё можно понять, но почему тогда width = wrapContent и height = wrapContent не ww, а submissive? А потом ещё появляется row, который ничем не отличается от mw, рили?
6) чем не угодил gone, что появился hidden? то, что это в css есть, не аргумент, мы всё таки про андроид говорим, а gone — устоявшееся значение
7) опять же, в чём выигрыш от замены margin на m? тут скорее проигрыш, ибо тоже нечитаемо
и это без некоторых (а может и многих) фич, которые остались неописанными
prs123
30.04.2018 00:42Что-то я не понимаю, что такого использовать XML-верстку? Более того, дизайнер может лепить разметку, не вдаваясь в код. Да и по-моему любая надстройка сверху явно не может ускорить то, что под ней. Может ли кто объяснить, зачем все это?
Brotandos Автор
30.04.2018 00:47Я бы посоветовал сперва вам научится читать внимательно. В маленькой главе «Мотивация» я, как бы парадоксально это не звучало, написал о своей мотивации. Нуждаетесь ли вы, чтобы я вырезал текст из статьи и ответил вам комментарием?
abbath0767
Это конечно здорово, но не советую такой подход использовать на реальных проектах. Если вам так нравится такой подход попробуйте Dart&Flutter