DSL In Action

DSL In Action

伴隨著Kotlin的發(fā)展,有一個神奇的框架anko-layout虐秦,一直存在于我們的視野卻又一直因?yàn)楦鞣N原因無法用于生產(chǎn)環(huán)境中胶征。最近在寫項(xiàng)目時尔艇,再次拿出anko這個框架,思考它在UI小組件上的可用性喜庞。

PS: Anko != Anko_Layouts 诀浪,但是為了表述方便,文中一部分Anko是代指這Anko Layouts框架延都,大家自己理解一下~

概述

關(guān)于Anko-Layouts框架的好處和局限性雷猪,網(wǎng)上已經(jīng)有大部分文章在講,它好在用DSL的方式來描述View晰房,而缺點(diǎn)在于無法即時預(yù)覽求摇,在這方面導(dǎo)致Anko DSL的開發(fā)效率不及XML傳統(tǒng)方式。經(jīng)過大家的一些踩坑殊者,以及開發(fā)上的試用与境,一致表示,Anko Layouts無法用在成熟的項(xiàng)目之中猖吴,還是老老實(shí)實(shí)用XML吧…

Anko Layouts的DSL設(shè)計那么棒… 就要這么放棄了嗎

大家眼里的Anko Layouts DSL

受官方文檔的“誘導(dǎo)”摔刁,大家對于Anko Layouts DSL的印象大概是這樣子的:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    verticalLayout {
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }
        editText {
            hint = "Password"
            textSize = 24f
        }
        button("Login") {
            textSize = 26f
        }
    }
}

val name: EditText = with(ankoContext) {
    editText {
        hint = "Name"
    }
}

官方的Demo中,將Activity的布局方式從setContentView()中傳入Layout ID換到了直接的DSL距误,嗯… 看起來還不錯簸搞,官方文檔也提供了一個Anko View 組件化的方案:

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent<MyActivity> {
    override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello, ${name.text}!") }
            }
        }
    }
}

直戳XML的痛點(diǎn),XML作為傳統(tǒng)的View構(gòu)建方式准潭,復(fù)用的方式極其有限(比如說蛋疼的include)趁俊,而Anko可以在編程語言的層面來做View組件的復(fù)用,實(shí)在是棒…

但是它背后做了啥刑然?這些View是怎么被構(gòu)造的寺擂?這些View是怎么被添加進(jìn)去的?如果是復(fù)雜的參數(shù)又應(yīng)該怎么辦泼掠?
這些問題在你計劃把Anko Layouts DSL 作為構(gòu)建View的方式后怔软,逐個浮出水面,然后開始勸退… QAQ

Anko Layout DSL 到底在干什么

為什么我們可以用DSL來寫界面择镇?

Kotlin DSL本身就是語法糖而已挡逼,所以DSL背后就是使用Kotlin代碼來自己初始化View,初始化LayoutParams腻豌,進(jìn)行addView之類…

而其實(shí)LayoutInflater它本身也只是在做相似的事情而已家坎,LayoutInflater是根據(jù)XML文件里面的配置來通過反射初始化View嘱能,根據(jù)其他字段來填充View屬性以及LayoutParams什么的。所以沒有什么神秘的東西…

我們梳理一下虱疏,其實(shí)在非XML代碼中構(gòu)建View的時候惹骂,無非就是 new View(context) -> addView

那么我們瞅瞅Anko的代碼,是不是也有相似的邏輯(不用想也是白龅伞)

inline fun ViewManager.textView(init: (@AnkoViewDslMarker android.widget.TextView).() -> Unit): android.widget.TextView {
    return ankoView(`$$Anko$Factories$Sdk25View`.TEXT_VIEW, theme = 0) { init() }
}

inline fun <T : View> ViewManager.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
    val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)
    val view = factory(ctx)
    view.init()
    AnkoInternals.addView(this, view) // this.addView(view)
    return view
}

//自定義的View添加DSL支持的話 (ColorCircleView是我的一個自定義View)
//這里的代碼比ViewManager.textView更容易理解
inline fun ViewManager.colorCircleView() = colorCircleView {}

inline fun ViewManager.colorCircleView(init: ColorCircleView.() -> Unit): ColorCircleView {
    return ankoView({ ColorCircleView(it) }, theme = 0, init = init)
}

我們可以大概看到对粪,在AnkoView中構(gòu)造了一個View然后通過ViewManager添加到ViewGroup里面去。
那么装蓬,ViewManager是什么呢著拭?

package android.view;

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

所以ViewManager是管理著View的添加,修改以及刪除的接口矛物,不出意料茫死,ViewGroup就實(shí)現(xiàn)了ViewManager

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

然后我們梳理一下,textView是一個拓展方法履羞,拓展到了ViewManager接口里面峦萎,因此所有實(shí)現(xiàn)ViewManager接口的類都可以調(diào)用這個textView方法,而調(diào)用這個方法的結(jié)果就是把textView加入到此ViewGroup里面忆首,比如說:

val frameLayout = findViewById<FrameLayout>(R.id.fl_container)

val view = frameLayout.textView {
    text = "辣雞辦公網(wǎng)??????????????"
    textColor = Color.BLACK
    textSize = 16f
}.apply {
    layoutParams = FrameLayout.LayoutParams(matchParent, wrapContent)
    visibility = View.GONE
}

效果就是爱榔,在FrameLayout里面添加了一個TextView,Textview擁有著DSL閉包里面的配置糙及。

另外详幽,我們構(gòu)造View的方式還有,傳入一個Context就可以構(gòu)建出一個View浸锨,我們可以瞅瞅相關(guān)的代碼:

inline fun Context.constraintLayout(): android.support.constraint.ConstraintLayout = constraintLayout() {}

inline fun Context.constraintLayout(init: (@AnkoViewDslMarker _ConstraintLayout).() -> Unit): android.support.constraint.ConstraintLayout {
    return ankoView(`$$Anko$Factories$ConstraintLayoutViewGroup`.CONSTRAINT_LAYOUT, theme = 0) { init() }
}

背后的實(shí)現(xiàn)我們不做深究唇聘,大概就是用Context來構(gòu)建出一個View,然后拿到了View柱搜,我們就可以為所欲為了迟郎。

怎么把Anko靈活用起來

簡單回顧一下上面一節(jié)的內(nèi)容: 如果我們擁有一個ViewGroup或者擁有一個Context,就可以用來創(chuàng)建View

因此Anko的用法遠(yuǎn)要比你想象中的靈活 -> 可以拿到Context/ViewGroup的地方就可以使用Anko聪蘸,而Anko的作用也就是簡化初始化View + AddView的流程宪肖。

舉個栗子?
舉個栗子健爬?

比如說我已經(jīng)用XML寫好了頁面的布局控乾,然后我們需要根據(jù)代碼在其中一個FrameLayout中動態(tài)添加一些東西。我們就可以拿到這個FrameLayout的引用娜遵,然后就可以用anko大展拳腳了蜕衡。

val frameLayout = findViewById<FrameLayout>(R.id.fl_container)
val view = frameLayout.textView {
    text = "辣雞辦公網(wǎng)??????????????"
    textColor = Color.BLACK
    textSize = 16f
}.apply {
    layoutParams = FrameLayout.LayoutParams(matchParent, wrapContent)
    visibility = View.GONE
}

frameLayout.verticalLayout { 
    
}

摸著良心說,是不是比自己創(chuàng)建View(不管是從Inflater還是java code方式)都要簡單太多设拟。

再舉一個例子衷咽,在BottomSheetDialogFragment中鸽扁,我們拿到Dialog后,需要通過setContView的方式來給它設(shè)置有個View進(jìn)去镶骗,而我們一般會在XML寫好然后Inflater獲得View加載進(jìn)去,或者自己一個一個new躲雅。有了Anko后鼎姊,你可以隨手寫起DSL。

    override fun setupDialog(dialog: Dialog?, style: Int) {
        if (dialog == null) return
        val context = dialog.context
        
        val view = context.nestedScrollView {
            verticalLayout {
                constraintLayout {
                    backgroundColor = getColorCompat(R.color.colorPrimary)

                    val titleText = textView {
                        text = "課程表設(shè)置"
                        id = View.generateViewId()
                        textSize = 20f
                        textColor = Color.WHITE
                    }.lparams(width = wrapContent, height = wrapContent) {
                        startToStart = ConstraintLayout.LayoutParams.PARENT_ID
                        topToTop = ConstraintLayout.LayoutParams.PARENT_ID
                        bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                        margin = dip(16)
                    }

                }

                indicator("課程表界面設(shè)置")

                constraintLayout {
                    backgroundColor = Color.WHITE

                    textView {
                        text = "自動隱藏周六日"
                        textSize = 14f
                        textColor = Color.BLACK
                    }.lparams(width = wrapContent, height = wrapContent) {
                        startToStart = PARENT_ID
                        topToTop = PARENT_ID
                        bottomToBottom = PARENT_ID
                        leftMargin = dip(16)
                    }

                    switch {
                        isChecked = SchedulePref.autoCollapseSchedule
                        onCheckedChange { _, isChecked ->
                            SchedulePref.autoCollapseSchedule = isChecked
                        }
                    }.lparams {
                        topToTop = PARENT_ID
                        bottomToBottom = PARENT_ID
                        endToEnd = PARENT_ID
                        rightMargin = dip(16)
                    }
                }.lparams(width = matchParent, height = dip(48))

                indicator("主題設(shè)置(課程表試點(diǎn))")

            }
        }

        dialog.setContentView(view)
    }

你甚至可以像函數(shù)一樣去封裝相赁,給LinearLayout做拓展后相寇,就可以包裝添加固定風(fēng)格TextView的操作了(這個封裝是不是就很好寫 就賊tm方便)

fun _LinearLayout.indicator(indicatorText: String) = frameLayout {
        textView {
            text = indicatorText
            textColor = getColorCompat(R.color.colorPrimary)
            textSize = 12f
            typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
        }.lparams(width = matchParent, height = wrapContent) {
            leftMargin = dip(8)
            topMargin = dip(8)
        }
    }.lparams(width = matchParent, height = wrapContent)

    fun lollipop(block: () -> Unit) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            block()
        }
    }

你甚至可以用for循環(huán)來做類似于適配器的事情(當(dāng)然緩存是不會有緩存的 這輩子都沒有做的)

spreadChainLayout {
                    listOf(
                            "佩奇粉" to Color.parseColor("#EDC6CD"),
                            "喬治藍(lán)" to Color.parseColor("#6595D9"),
                            "豬媽黃" to Color.parseColor("#F4B17F"),
                            "豬爸綠" to Color.parseColor("#6FC6C5"),
                            "基佬紫" to Color.parseColor("#9C26B0")
                    ).forEachIndexed { index, (name, color) ->

                        verticalLayout {
                            colorCircleView {
                                this.color = color
                            }.lparams {
                                width = dip(24)
                                height = dip(24)
                                gravity = Gravity.CENTER_HORIZONTAL
                            }
                            textView {
                                text = name
                                textColor = color
                                textSize = 12f
                                typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
                            }.lparams(width = wrapContent, height = wrapContent) {
                                topMargin = dip(6)
                            }
                        }.lparams {
                            width = wrapContent
                            height = wrapContent
                            horizontalPadding = dip(16)
                        }.setOnClickListener {
                            val theme = CustomTheme.themeList[index]
                            Log.e(TAG, "custom theme: $theme")
                            val activity = this@CustomSettingBottomFragment.activity
                            Colorful().edit()
                                    .setPrimaryColor(theme)
                                    .setAccentColor(theme)
                                    .apply(context) {
                                        activity?.recreate()
                                    }
                        }


                    }
                }.lparams(width = matchParent, height = wrapContent) {
                    topMargin = dip(12)
                    bottomMargin = dip(12)
                }

在一個Layout的閉包里面寫循環(huán),填充數(shù)據(jù)钮科,然后addView唤衫,有了Kotlin的語法糖 + Anko變得很舒服。

你甚至可以在Recyclerview里面寫Anko


class Item(var text: String, var builder: (TextView.() -> Unit)? = null)

class ViewHolder(itemView: View?, val textView: TextView) : RecyclerView.ViewHolder(itemView)

override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
    lateinit var textView: TextView
    val view = parent.context.constraintLayout {
         textView = textView {
            text = "課程表設(shè)置"
            id = View.generateViewId()
            textSize = 16f
            textColor = Color.BLACK
        }.lparams(width = wrapContent, height = wrapContent) {
            startToStart = PARENT_ID
            topToTop = PARENT_ID
            bottomToBottom = PARENT_ID
            margin = dip(16)
        }
        imageView {
            backgroundColor = getColorCompat(R.color.common_lv4_divider)
        }.lparams(width = matchParent, height = dip(1)) {
            bottomToBottom = PARENT_ID
        }
    }.apply {
        layoutParams = RecyclerView.LayoutParams(matchParent, wrapContent)
    }
    return ViewHolder(view,textView)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) {
    item as SingleTextItem
    holder as ViewHolder
    holder.textView.text = item.text
    item.builder?.invoke(holder.textView)
}

在數(shù)據(jù)里面附著上一個閉包绵脯,便可以實(shí)現(xiàn)TextView的自定義(把邏輯從onBindViewHolder里面抽離出來)佳励,我們的項(xiàng)目中Recyclerview Adapter做了DSL風(fēng)格的二次封裝,目前處于測試階段蛆挫,穩(wěn)定了之后會分享在博客里面赃承。

DSL大概是這樣子的:

        recyclerView.withItems {
            courseInfo(course = course)
            indicatorText("上課信息")

            val week = course.week
            course.arrangeBackup.forEach {
                iconLabel(CourseDetailViewModel(R.drawable.ic_schedule_location, "${week.start}-${week.end}周,${it.week}上課悴侵,每周${getChineseCharacter(it.day)}第${it.start}-${it.end}節(jié)\n${it.room}"))
            }

            indicatorText("其他信息")
            iconLabel(CourseDetailViewModel(R.drawable.ic_schedule_other, "邏輯班號:${course.classid}\n課程編號:${course.courseid}"))
            indicatorText("自定義(開發(fā)中 敬請期待)")
            iconLabel(CourseDetailViewModel(R.drawable.ic_schedule_search, "在蹭課功能中搜索相似課程"))
            iconLabel(CourseDetailViewModel(R.drawable.ic_schedule_event, "添加自定義課程/事件"))
            iconLabel(CourseDetailViewModel(R.drawable.ic_schedule_homework, "添加課程作業(yè)/考試"))
            indicatorText("幫助")
        }

做個總結(jié)瞧剖?

DSL最吸引人的地方就在于,它可以在布局上加入邏輯可免,對于布局過程抓于,它有著編程語言級別的控制,比如說封裝成類浇借,封裝成函數(shù)什么的捉撮。這些東西在XML里面都是無法做到的,因?yàn)閍apt工具的局限性逮刨,XML只能按照固定的格式寫布局 + 代碼控制來提供動態(tài)性呕缭,反正就很蛋疼。

而DSL可以解決很多問題修己,比如說用一個for循環(huán)來取代Adapter填充View功能恢总,避免了很多無用的操作。比如說在布局里面加一個if就可以來操作一個控件的布局與否睬愤,而不是在findView之后控制Visibility片仿,可以用Kotlin的閉包來封裝一個View的初始化操作什么的,重復(fù)的操作就可以封裝起來尤辱,再比如XML只能設(shè)置paddingLeft/paddingRight砂豌,在Anko DSL / 自定義DSL里面就可以很輕易的封裝出一個horizontalPadding厢岂。當(dāng)然Anko因?yàn)楸苊饬朔瓷洌岣吡舜罅康男阅堋?/p>

DSL和XML并不是沖突的阳距,DSL用于解決布局中細(xì)碎和動態(tài)的部分塔粒,而XML用于單頁布局,復(fù)雜布局筐摘。同時DSL和XML也可以無縫嵌合在一起卒茬,所以兩者并不是沖突的關(guān)系,也沒有必要去選擇“我到底該用DSL寫還是XML寫”咖熟,兩者各有優(yōu)點(diǎn)圃酵,了解Anko DSL并且與XML活用起來才是最優(yōu)解。XML可以拿到ViewGroup的應(yīng)用然后用DSL做騷操作馍管,DSL也可以動態(tài)添加Inflate出來的XML來實(shí)現(xiàn)復(fù)雜頁面布局的添加

DSL和XML各有所長郭赐,DSL更適合用于頁面模塊的解耦,XML更多用于單頁構(gòu)建 / 復(fù)雜布局确沸,兩者相互結(jié)合相互服務(wù)捌锭。

還想說的

Anko DSL讓人望而卻步的部分就是它不能支持即時預(yù)覽,所以這個局限性也就導(dǎo)致Anko無法構(gòu)建大型復(fù)雜的頁面张惹。而當(dāng)你的設(shè)計圖可以精確到dp的時候舀锨,完全可以用DSL來描述UI的各個小組件,因此DSL在這里不應(yīng)該被一棒子打死宛逗,DSL在目前的項(xiàng)目中坎匿,可以很好的替代手工new View, add view的部分,以及小規(guī)模的View控制雷激。

如果你認(rèn)真看了上面的內(nèi)容替蔬,并且有自己的體會,可以在已有的UI構(gòu)架中很快的用上Anko Layout來解決一些輕量級UI的構(gòu)建屎暇。比如說List中的一個Item承桥,或者一個小Dialog之類。

沒有所謂的“最佳實(shí)踐”根悼,對于業(yè)務(wù)與技術(shù)的一步步探索才是最重要的凶异。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挤巡,隨后出現(xiàn)的幾起案子剩彬,更是在濱河造成了極大的恐慌,老刑警劉巖矿卑,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喉恋,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)轻黑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門糊肤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人氓鄙,你說我怎么就攤上這事馆揉。” “怎么了抖拦?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵把介,是天一觀的道長。 經(jīng)常有香客問我蟋座,道長,這世上最難降的妖魔是什么脚牍? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任向臀,我火速辦了婚禮,結(jié)果婚禮上诸狭,老公的妹妹穿的比我還像新娘券膀。我一直安慰自己,他們只是感情好驯遇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布芹彬。 她就那樣靜靜地躺著,像睡著了一般叉庐。 火紅的嫁衣襯著肌膚如雪舒帮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天陡叠,我揣著相機(jī)與錄音玩郊,去河邊找鬼。 笑死枉阵,一個胖子當(dāng)著我的面吹牛译红,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播兴溜,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼侦厚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拙徽?” 一聲冷哼從身側(cè)響起刨沦,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斋攀,沒想到半個月后已卷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年侧蘸,在試婚紗的時候發(fā)現(xiàn)自己被綠了裁眯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡讳癌,死狀恐怖穿稳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晌坤,我是刑警寧澤逢艘,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站骤菠,受9級特大地震影響它改,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜商乎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一央拖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鹉戚,春花似錦鲜戒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赢底,卻和暖如春失都,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颖系。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工嗅剖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嘁扼。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓信粮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親趁啸。 傳聞我的和親對象是個殘疾皇子强缘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容