Змеиная верстка и «квантовые» частицы в приложениях под Android (Часть 1) +1


image

Статья о библиотеках, позволившие мне обходиться без паттерна 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» в последней странице создании проекта

вот здесь


build.gradle (уровень проекта)
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
}


build.gradle (уровень приложения)
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'
}



MainActivity.kt
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()
    }
}


Добавьте 2 иконки в папку drawable (Image Asset)




res\values\styles.xml
<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
Models.kt
data class Dictionary(var title: String, val items: MutableList<DictionaryItem>)
data class DictionaryItem(val key: String, val value: String)


G.kt
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()
    }
}


Styles.kt
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))
        }
    }
}

Результат:


Пока остановимся здесь. Думаю и здесь хватает чего обсудить. Вот сама библиотека. В следующей статье я напишу про библиотеку с «квантовыми» частицами.

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




К сожалению, не доступен сервер mySQL