Android开发:Kotlin的DSL的应用和干货分享

Android开发:Kotlin的DSL的应用和干货分享_第1张图片

Kotlin-first but not kotlin-must

谷歌在 I/O 大会上宣布,Kotlin 编程语言现在是 Android 应用程序开发人员的首选语言后,有更多的安卓程序投入Kotlin的怀抱。
Kotlin的语法糖更加提高了开发的效率,加快了开发速度,使开发工作变得有趣,也让我们有更多时间写注释了(笑)。但是其实对于Kotlin和Java在Android开发上的选择,个人觉得这个除了开发人员对语言的喜好的,同时也会应该到各自语言的魅力和特点,甚至项目的需求以及后续维护等等各个因素,没有绝对的选择的。我们要做到的是放大不同语言优点并加以拓展,不是一味只选择某个语言,语言不是问题,用的那个人怎么用才是关键。

Kotlin的DSL

一、从TextWatcher和 TabLayout.OnTabSelectedListener的优化开始

先说说语法糖,例如下面的代码:

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

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

通过 infix 定义 中缀符号 使得操作LiveData的set和post更加好看

//初始化
 private val pagerNumber = MutableLiveData()
  ......................省略无关内容.............................
//执行postValue操作
  pagerNumber post 0

又好像kotlin中的Iterable的forEach有些人发现这样用无法break,源码如下:

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

那我们不如改装一下,如下:

//定义
inline fun  Iterable.forEachBreak(action: (T) -> Boolean ){
    kotlin.run breaking@{
        for (element in this)
            if(!action(element)){
                return@breaking
            }
    }
}
//应用
members.forEachBreak { call->
         // true or false 控制适当地方的return值,跳出循环
         true
 }

回到今天的主题,Kotlin 的DSL更加语法糖升华,好像语法糖plus+。今天是打算分享一下我开发时候对应DSL的运用,如何利用DSL使得的开发变得有趣的。
先看看下面代码

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

第一眼看上去,是不是很熟悉呢。不就一个EditText实现addTextChangedListener的方法,然后afterTextChanged里面执行操作button是否出现吗?但是你再认真看看,代码是不是简单了很多,好像少了什么,这时候应该有人会留意到那个大括号了吧。这个就是DSL,里面就一个afterTextChanged?,其实如果刚刚开始用Kotlin的开发会这样想到,那我们建一个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)
            }
        })
    }
}

代码有点长,当然我知道上面代码还能继续优化,也欢迎大家提供意见。但是通过这样的封装,代码风格更加简便。以此类推,我们也可以对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内进行执行,当然还是那个句:
//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方法就能把这个接口初始化成功了。
先别那么快否定我,这样写实不实用,因为初始化这些接口,对于每个安卓开发都再熟悉不过了,方法一大堆。今天我们是对DSL的进一步学习,把思路拓宽。
这个startInit内部长这样的

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

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

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

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

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

这个reified关键字起到的作用很核心,可以简化模板代码,编译器可以自动推断类型
例如:

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

能直接通过这个reified 拿到泛型的类型,对于Kotlin这种很注重泛型的语言尤其出色,加上inline进一步节省调用开销。通过startInit方法我们现在可以更加优雅处理类的构造函数初始化。

实用大升级

在平时开发中,DSL除了应用在一些普通方法上,我们其实还可以拓展到一些常用类的封装,例如DialogFragment。DialogFragment其实对于安卓的开发人员来说,都不是一个陌生的类。
方法 — 、可以覆写其 onCreateDialog 利用AlertDialog或者Dialog创建出Dialog。
方法 二、 覆写其 onCreateView 使用定义的xml布局文件展示Dialog。
那我们的DSL可以怎么进一步优化使用过程,接下来分享一下我的处理:

一、先提取常用配置,减少重复代码的书写

我们平时在书写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的生命周期,能在适当的地方进行适当操作

 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,进行生命周期的监听。

三、引入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  View.onBindingView(onBindingView :((binding : T?)->Unit)){
        onBindingView.invoke(DataBindingUtil.bind(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 { binding ->
                binding?.apply {
                    //do something
                }
            }
        }
    }

相对的代码量少了,把基本的内容都封装起来,
一、buildDialog 方法,利用onCreate :(()->Int) 这个Int的返回值,即该方法大括号的最后一行,代表返回值的特性,把layoutId设置进去,当然你也可以采用在上面注解的地方,添加也可以。
二、onWindow方法,该方法可以拿到dialog.window的对象,利用该对象,我们可以再进一步进行配置
三、onView方法,该方法能拿到layout的view对象,这里提一个特别的地方
平时开发可以留一下在不使用databinding情况下,Dialog中使用kotlin了
apply plugin: 'kotlin-android-extensions'这个配置可以拿到view的id,但是有一点需要注意引入的时候 xxxxxx.* 和 xxxxx.view.* (xxxxx即你的布局名字)是有区别的,各位可以留意一下。
四、onBindingView是我特意留的一个databinding的方法,方便使用databinding的朋友。

结合协程实现倒计时功能

我们平时实现倒计时功能都会用到RxJava,CountDownTimer,Timer+TimerTask,线程,今天借此利用线程的方案,即Kotlin中的协程,废话不说先放代码:

fun LifecycleOwner.counter(dispatcher: CoroutineContext,start:Int, end:Int, delay:Long, onProgress:((value:Int)->Unit),onFinish: (()->Unit)?= null){
    val out = flow {
        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方法,进一步的优化了采用线程方案的倒计时。

小结

这篇文章也是我第二篇分享文章,因为个人很少写文章和博客,也不是懒不懒的问题,其实就是有个感觉,觉得自己学习知识学了一段时间,是不是应该做个分享,多一些交流,让自己的思路更加拓展。
个人的github地址:https://github.com/ShowMeThe
也分享一下,无聊时候写的一个基于ViewPager2的轮播图:https://github.com/ShowMeThe/BannerView
有问题也可以留个言,交流一下。

你可能感兴趣的:(Android开发:Kotlin的DSL的应用和干货分享)