Android開發(fā):Kotlin的DSL的應用和干貨分享

Kotlin-first but not kotlin-must

谷歌在 I/O 大會上宣布咸作,Kotlin 編程語言現(xiàn)在是 Android 應用程序開發(fā)人員的首選語言后锨阿,有更多的安卓程序投入Kotlin的懷抱。
Kotlin的語法糖更加提高了開發(fā)的效率记罚,加快了開發(fā)速度墅诡,使開發(fā)工作變得有趣,也讓我們有更多時間寫注釋了(笑)桐智。但是其實對于Kotlin和Java在Android開發(fā)上的選擇末早,個人覺得這個除了開發(fā)人員對語言的喜好的,同時也會應該到各自語言的魅力和特點说庭,甚至項目的需求以及后續(xù)維護等等各個因素然磷,沒有絕對的選擇的。我們要做到的是放大不同語言優(yōu)點并加以拓展刊驴,不是一味只選擇某個語言姿搜,語言不是問題,用的那個人怎么用才是關鍵捆憎。

Kotlin的DSL

一舅柜、從TextWatcher和 TabLayout.OnTabSelectedListener的優(yōu)化開始

先說說語法糖,例如下面的代碼:

infix fun <T:Any> MutableLiveData<T>.post(newValue:T){
    this.postValue(newValue)
}

infix fun <T:Any> MutableLiveData<T>.set(newValue:T){
    this.value = newValue
}

通過 infix 定義 中綴符號 使得操作LiveData的set和post更加好看

//初始化
 private val pagerNumber = MutableLiveData<Int>()
  ......................省略無關內容.............................
//執(zhí)行postValue操作
  pagerNumber post 0

又好像kotlin中的Iterable<T>的forEach有些人發(fā)現(xiàn)這樣用無法break躲惰,源碼如下:

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

那我們不如改裝一下业踢,如下:

//定義
inline fun <T> Iterable<T>.forEachBreak(action: (T) -> Boolean ){
    kotlin.run breaking@{
        for (element in this)
            if(!action(element)){
                return@breaking
            }
    }
}
//應用
members.forEachBreak { call->
         // true or false 控制適當?shù)胤降膔eturn值,跳出循環(huán)
         true
 }

回到今天的主題礁扮,Kotlin 的DSL更加語法糖升華,好像語法糖plus+瞬沦。今天是打算分享一下我開發(fā)時候對應DSL的運用太伊,如何利用DSL使得的開發(fā)變得有趣的。
先看看下面代碼

  edittext.textWatcher {
            afterTextChanged {
                if(!isNullOrEmpty()){
                    button.visibility  = View.VISIBLE
                }else{
                    button.visibility  = View.INVISIBLE
                }
            }
        }

第一眼看上去逛钻,是不是很熟悉呢僚焦。不就一個EditText實現(xiàn)addTextChangedListener的方法,然后afterTextChanged里面執(zhí)行操作button是否出現(xiàn)嗎曙痘?但是你再認真看看芳悲,代碼是不是簡單了很多,好像少了什么边坤,這時候應該有人會留意到那個大括號了吧名扛。這個就是DSL,里面就一個afterTextChanged茧痒?肮韧,其實如果剛剛開始用Kotlin的開發(fā)會這樣想到,那我們建一個XXXX類繼承一下TextWatcher然后

edittext.addTextChangedListener(object:XXXX){
       ....................override相應方法................
}

其實這種寫法有錯嗎?當然沒有弄企,但是不美觀超燃。也不太符合Kotlin的編碼習慣。
那不如我直接POST代碼出來拘领,想讓你們看看內部封裝吧意乓。

fun EditText.textWatcher(textWatch: SimpleTextWatcher.() -> Unit) {
    val simpleTextWatcher = SimpleTextWatcher(this)
    textWatch.invoke(simpleTextWatcher)
}

class SimpleTextWatcher(var view: EditText) {

    private var afterText: (Editable?.() -> Unit)? = null
    fun afterTextChanged(afterText: (Editable?.() -> Unit)) {
        this.afterText = afterText
    }

    private var beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null
    fun beforeTextChanged(beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)) {
        this.beforeText = beforeText
    }

    private var onTextChanged: ((s: CharSequence?, 
                  start: Int, before: Int, count: Int) -> Unit)? = null

    fun onTextChanged(onTextChanged: ((s: CharSequence?,
                      start: Int, before: Int, count: Int) -> Unit)) {
        this.onTextChanged = onTextChanged
    }

    init {
        view.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                afterText?.invoke(s)
            }
            override fun beforeTextChanged(s: CharSequence?, 
                      start: Int, count: Int, after: Int) {
                beforeText?.invoke(s, start, count, after)
            }
            override fun onTextChanged(s: CharSequence?, 
                       start: Int, before: Int, count: Int) {
                onTextChanged?.invoke(s, start, before, count)
            }
        })
    }
}

代碼有點長,當然我知道上面代碼還能繼續(xù)優(yōu)化约素,也歡迎大家提供意見届良。但是通過這樣的封裝,代碼風格更加簡便业汰。以此類推伙窃,我們也可以對TabLayout的addOnTabSelectedListener進一步封裝

//封裝
fun TabLayout.onTabSelected(tabSelect: TabSelect.() -> Unit) {
    tabSelect.invoke(TabSelect(this))
}

class TabSelect(tab: TabLayout) {
    private var tabReselected: ((tab: TabLayout.Tab) -> Unit)? = null
    private var tabUnselected: ((tab: TabLayout.Tab) -> Unit)? = null
    private var tabSelected: ((tab: TabLayout.Tab) -> Unit)? = null

    fun onTabReselected(tabReselected: (TabLayout.Tab.() -> Unit)) {
        this.tabReselected = tabReselected
    }

    fun onTabUnselected(tabUnselected: (TabLayout.Tab.() -> Unit)) {
        this.tabUnselected = tabUnselected
    }

    fun onTabSelected(tabSelected: (TabLayout.Tab.() -> Unit)) {
        this.tabSelected = tabSelected
    }

    init {
        tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabReselected(tab: TabLayout.Tab?) {
                tab?.apply { tabReselected?.invoke(tab) }
            }
            override fun onTabUnselected(tab: TabLayout.Tab?) {
                tab?.apply { tabUnselected?.invoke(tab) }
            }
            override fun onTabSelected(tab: TabLayout.Tab?) {
                tab?.apply { tabSelected?.invoke(tab) }
            }

        })
    }
}
  //使用
  tab.onTabSelected {
            onTabSelected {
                pos = position
            }
        }

我們其實還有更多這樣的方法可以這樣封裝,達到更加方便样漆。

二为障、DSL運用升級

我們還是先看看代碼:

//在Application內進行執(zhí)行,當然還是那個句:
//Application不要有太多的復雜耗時任務放祟,我只是舉個一個可以運用的地方而已鳍怨,保證初始化成功。
//RetroHttp就是一個Retrofit Client封裝類
//createApi(tClass)還是那個Retrofit.create(tClass)
 startInit {
            modules(Module{
                single{ RetroHttp.createApi(auth::class.java) }
            }
        }

//然后我們在某個Repository內初始化 interface auth 這個接口類
val api : auth by inject()

是不是很簡單呢跪妥,我們只要inject方法就能把這個接口初始化成功了鞋喇。
先別那么快否定我,這樣寫實不實用眉撵,因為初始化這些接口侦香,對于每個安卓開發(fā)都再熟悉不過了,方法一大堆纽疟。今天我們是對DSL的進一步學習罐韩,把思路拓寬。
這個startInit內部長這樣的

fun startInit(component: Components.()->Unit){
    component.invoke(Components.get())
}
class Components {
    companion object{
        private val entry = ArrayMap<String,Any?>()
        private val module = ArrayList<Module>()
        private val instant by lazy { Components() }
        fun get() = instant
        fun getEntry() = entry
    }
    fun modules(vararg modules: Module){
        module.addAll(modules)
    }
}

inline fun <reified T> get(name: String = T::class.java.name) : T{
   return Components.getEntry()[name] as T
}

inline fun <reified T> inject(name: String = T::class.java.name) : Lazy<T> {
    return lazy { Components.getEntry()[name]  as T }
}

class Module(component: Component.() -> Unit){
    init {
        component.invoke(Component())
    }
}

class Component{
    inline fun <reified T>single(noinline single: Component.()->T){
        val name = T::class.java.name
        Components.getEntry()[name] = single()
    }
}

這個reified關鍵字起到的作用很核心污朽,可以簡化模板代碼散吵,編譯器可以自動推斷類型
例如:

//定義
   inline fun <reified T>startActivity(bundle: Bundle? = null) {
        val intent = Intent(this, T::class.java)
        if (bundle != null) {
            intent.putExtras(bundle)
        }
        startActivity(intent)
    }
//利用
  startActivity<CollectActivity>()

能直接通過這個reified 拿到泛型的類型,對于Kotlin這種很注重泛型的語言尤其出色蟆肆,加上inline進一步節(jié)省調用開銷矾睦。通過startInit方法我們現(xiàn)在可以更加優(yōu)雅處理類的構造函數(shù)初始化。

實用大升級

在平時開發(fā)中炎功,DSL除了應用在一些普通方法上枚冗,我們其實還可以拓展到一些常用類的封裝,例如DialogFragment蛇损。DialogFragment其實對于安卓的開發(fā)人員來說官紫,都不是一個陌生的類肛宋。
方法 — 、可以覆寫其 onCreateDialog 利用AlertDialog或者Dialog創(chuàng)建出Dialog束世。
方法 二酝陈、 覆寫其 onCreateView 使用定義的xml布局文件展示Dialog。
那我們的DSL可以怎么進一步優(yōu)化使用過程毁涉,接下來分享一下我的處理:

一沉帮、先提取常用配置,減少重復代碼的書寫

我們平時在書寫DialogFrament時候都會有不少的模板代碼贫堰,幾乎三四個DialogFramgent 里面都有好幾十行是一模一樣的基礎設置穆壕,那我們先定義一個注解

@Target(AnnotationTarget.CLASS)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class  WindowParam(val gravity:Int = Gravity.CENTER,val outSideCanceled:Boolean = true, val noAnim : Boolean = false,
val animRes :Int = -1,val canceled:Boolean = true, 
val dimAmount :Float = -1f)
// gravity 是dialog中的Window的 setGravity(gravity)方法
//outSideCanceled 是 dialog.setCanceledOnTouchOutside(outSideCanceled)
//canceled 是 dialog.setCancelable(canceled)
//noAnim 是指是否使用進場出場動畫
//animRes是我們的進場出場動畫資源
//dimAmount 是window的setDimAmount(dimAmount) 用于控制彈窗后灰色蒙版的透明度

二、引入DefaultLifecycleObserver

引入這個對于DialogFragment是為方便獲取到它依附的Activity的生命周期其屏,能在適當?shù)牡胤竭M行適當操作

 override fun onAttach(context: Context) {
        super.onAttach(context)
        activity = context
        if(context is AppCompatActivity){
            init(context)
        }else if(context is LifecycleOwner){
            init(context)
        }
    }
 private var owner : LifecycleOwner? = null
 private fun init(owner: LifecycleOwner){
        this.owner = owner
        this.owner?.lifecycle?.addObserver(this)
    }

在onAttach方法的地方獲取LifecycleOwner,進行生命周期的監(jiān)聽喇勋。

三、引入DSL語法

在我這個封裝中偎行,我是覆寫onCreateDialog這個方法川背,由于封裝內容很多,我先貼出代碼在慢慢一步步講

abstract class SimpleDialogFragment  : DialogFragment(),DefaultLifecycleObserver {
    lateinit var activity : Context

    private var onCreate :(()->Int)? = null

    private var onWindow :((window:Window)->Unit)? = null

    private var onView :((view:View)->Unit)? = null

    abstract fun build(savedInstanceState: Bundle?)

    private var owner : LifecycleOwner? = null

    private fun init(owner: LifecycleOwner){
        this.owner = owner
        this.owner?.lifecycle?.addObserver(this)
    }

    override fun onStop(owner: LifecycleOwner) {
        dialog?.apply {
            dismissAllowingStateLoss()
        }
    }
    override fun onDestroy(owner: LifecycleOwner) {
        if(this.owner != null){
            dismissAllowingStateLoss()
            this.owner?.lifecycle?.removeObserver(this)
        }
    }

    fun buildDialog(onCreate :(()->Int)) : SimpleDialogFragment{
        this.onCreate = onCreate
        return this
    }

    fun onWindow(onWindow :((window:Window)->Unit)) : SimpleDialogFragment{
        this.onWindow = onWindow
        return this
    }

    fun <T : ViewDataBinding> View.onBindingView(onBindingView :((binding : T?)->Unit)){
        onBindingView.invoke(DataBindingUtil.bind<T>(this))
    }

    fun onView(onView :((view:View)->Unit)) : SimpleDialogFragment{
        this.onView = onView
        return this
    }
    override fun onAttach(context: Context) {
        super.onAttach(context)
        activity = context
        if(context is AppCompatActivity){
            init(context)
        }else if(context is LifecycleOwner){
            init(context)
        }
    }
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        build(savedInstanceState)
        val viewId = onCreate?.invoke()
        if(viewId!= null){
            val view = View.inflate(activity, viewId, null)
            val param = javaClass.getAnnotation(WindowParam::class.java)!!
            val gravity  = param.gravity
            val outSideCanceled   = param.outSideCanceled
            val canceled  = param.canceled
            val dimAmount = param.dimAmount
            val noAnim  = param.noAnim

            val dialog = Dialog(activity)
            dialog.setContentView(view)
            dialog.setCanceledOnTouchOutside(outSideCanceled)
            dialog.setCancelable(canceled)

            val window = dialog.window
            val dm = DisplayMetrics()
            window?.apply {
                windowManager.defaultDisplay.getMetrics(dm)
                setLayout(dm.widthPixels, window.attributes.height)
                setBackgroundDrawable(ColorDrawable(0x00000000))
                setGravity(gravity)
                if(!noAnim){
                    setWindowAnimations(R.style.LeftRightAnim)
                }
                if(dimAmount!=-1f){
                    setDimAmount(dimAmount)
                }
                onWindow?.invoke(this)
            }
            view?.apply {
                onView?.invoke(this)
            }
            return dialog
        }
        return super.onCreateDialog(savedInstanceState)
    }


    override fun dismiss() {
        dialog?.apply {
            if(isShowing){
                if(getActivity()!=null){
                    super.dismiss()
                }
            }
        }
    }


    override fun show(manager: FragmentManager, tag: String?) {
        try {
            if(!isAdded){
                val transaction = manager.beginTransaction()
                transaction.add(this, tag)
                transaction.commitAllowingStateLoss()
                transaction.show(this)
            }
        }catch (e: Exception){
            Log.e("DialogFragment","${e.message}")
        }
    }

}

用的時候變成了這樣蛤袒,如下:

@WindowParam(gravity = Gravity.BOTTOM,animRes = R.style.BottomTopAnim)
class BottomDialog : SimpleDialogFragment() {
    override fun build(savedInstanceState: Bundle?) {
        buildDialog {
            R.layout.XXXXXXXX
        }
        onView {
            it.onBindingView<XXXXXXXBinding> { binding ->
                binding?.apply {
                    //do something
                }
            }
        }
    }

相對的代碼量少了熄云,把基本的內容都封裝起來,
一妙真、buildDialog 方法缴允,利用onCreate :(()->Int) 這個Int的返回值,即該方法大括號的最后一行珍德,代表返回值的特性练般,把layoutId設置進去,當然你也可以采用在上面注解的地方锈候,添加也可以薄料。
二、onWindow方法晴及,該方法可以拿到dialog.window的對象,利用該對象嫡锌,我們可以再進一步進行配置
三虑稼、onView方法,該方法能拿到layout的view對象势木,這里提一個特別的地方
平時開發(fā)可以留一下在不使用databinding情況下蛛倦,Dialog中使用kotlin了
apply plugin: 'kotlin-android-extensions'這個配置可以拿到view的id,但是有一點需要注意引入的時候 xxxxxx.* 和 xxxxx.view.* (xxxxx即你的布局名字)是有區(qū)別的,各位可以留意一下啦桌。
四溯壶、onBindingView是我特意留的一個databinding的方法及皂,方便使用databinding的朋友。

結合協(xié)程實現(xiàn)倒計時功能

我們平時實現(xiàn)倒計時功能都會用到RxJava,CountDownTimer,Timer+TimerTask,線程且改,今天借此利用線程的方案验烧,即Kotlin中的協(xié)程渴杆,廢話不說先放代碼:

fun LifecycleOwner.counter(dispatcher: CoroutineContext,start:Int, end:Int, delay:Long, onProgress:((value:Int)->Unit),onFinish: (()->Unit)?= null){
    val out = flow<Int> {
        for (i in start..end) {
            emit(i)
            delay(delay)
        }
    }
    lifecycleScope.launchWhenStarted {
        withContext(dispatcher) {
            out.collect {
                onProgress.invoke(it)
            }
            onFinish?.invoke()
        }
    }
}

//使用
counter(Dispatchers.Main,1,3,1000,{
               //倒計時過程

            }){
                //完成倒計時
           }

利用了攜程中的flow方法丈冬,進一步的優(yōu)化了采用線程方案的倒計時。

小結

這篇文章也是我第二篇分享文章居兆,因為個人很少寫文章和博客慨蓝,也不是懶不懶的問題感混,其實就是有個感覺,覺得自己學習知識學了一段時間礼烈,是不是應該做個分享弧满,多一些交流,讓自己的思路更加拓展此熬。
個人的github地址:https://github.com/ShowMeThe
也分享一下庭呜,無聊時候寫的一個基于ViewPager2的輪播圖:https://github.com/ShowMeThe/BannerView
有問題也可以留個言,交流一下摹迷。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末疟赊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子峡碉,更是在濱河造成了極大的恐慌近哟,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鲫寄,死亡現(xiàn)場離奇詭異吉执,居然都是意外死亡,警方通過查閱死者的電腦和手機地来,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門戳玫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人未斑,你說我怎么就攤上這事咕宿。” “怎么了蜡秽?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵府阀,是天一觀的道長。 經(jīng)常有香客問我芽突,道長试浙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任寞蚌,我火速辦了婚禮田巴,結果婚禮上钠糊,老公的妹妹穿的比我還像新娘。我一直安慰自己壹哺,他們只是感情好抄伍,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斗躏,像睡著了一般逝慧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啄糙,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天笛臣,我揣著相機與錄音,去河邊找鬼隧饼。 笑死沈堡,一個胖子當著我的面吹牛,可吹牛的內容都是我干的燕雁。 我是一名探鬼主播诞丽,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拐格!你這毒婦竟也來了僧免?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤捏浊,失蹤者是張志新(化名)和其女友劉穎懂衩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體金踪,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡浊洞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胡岔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片法希。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖靶瘸,靈堂內的尸體忽然破棺而出苫亦,到底是詐尸還是另有隱情,我是刑警寧澤怨咪,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布屋剑,位于F島的核電站,受9級特大地震影響惊暴,放射性物質發(fā)生泄漏饼丘。R本人自食惡果不足惜趁桃,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一辽话、第九天 我趴在偏房一處隱蔽的房頂上張望肄鸽。 院中可真熱鬧,春花似錦油啤、人聲如沸典徘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逮诲。三九已至,卻和暖如春幽告,著一層夾襖步出監(jiān)牢的瞬間梅鹦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工冗锁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留齐唆,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓冻河,卻偏偏與公主長得像箍邮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叨叙,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內容