5.5 掌握 Anko,看这一篇就够了!

平时开发android时,我们的UI布局代码一般都是写在xml中,当然也有少数写在Java代码中,这就导致了这样的局面:xml布局清晰可见,但不能动态改变,Java代码布局比较灵活,但比较难用而且冗余难维护,所以一般都是用xml先编排出布局,然后再用代码进行进一步修改搭配使用。那么有没有更清晰且高效的方式呢?有!Anko就很好的解决了这个问题。

一、Anko是什么?

Anko是JetBrains开发的一个强大的库,它主要的目的是用来替代以前XML的方式来使用代码生成UI布局的,它包含了很多的非常有帮助的函数和属性来避免让你写很多的模版代码。

这是一个很有趣的特性,我推荐你可以尝试下,但是在该系列文章里我们暂时不太多使用它(只在一些地方使用并会做好相关的解释),因为对于现阶段的我们来说使用XML更容易一些,而且Anko其实并不难,看这一篇你就能学会如何使用常用的Anko方法来优化代码了,所以我们会把更多的重点放到Kotlin的学习上来。

如果你想研究Anko源码一探究竟,点击我这里给出的Github传送。

二、开始使用Anko

首先让我们来使用Anko简化一些代码:

val recyclerView: RecyclerView = find(R.id.recyclerView)

以上代码写在MainActivity:onCreate中,这是一段用来简化获取RecyclerView的代码。就像你将要看到的,任何时候你使用了Anko库中的某些东西,它们都会以属性名、方法等方式被导入。

你可能会问:“find()这个方法是哪里来的?”这是因为Anko使用了扩展函数在Android框架中增加了一些新的功能。

而至于什么是扩展函数,暂时我们只要知道:“有了它我们就能在Actvity里面直接调用 verticalLayout( )、relativeLayout( )等方法”就可以了,因为这些都是Anko给Activity加的扩展方法,所以在activity内部可以直接调用到这些方法(比如这里的find())。

我们将会在之后的文章中更加详细介绍给大家一些其它的扩展函数并教会大家怎么去编写自己的扩展函数,所以还请点击绿色的加号保持对我的关注!

(1)我们再来看一个加载布局的例子:

verticalLayout {
   val name = editText()
   button("Say Hello") {
     onClick { toast("Hello, ${name.text}!") 
  } 
}

上面是一个DSL(Domain Specific Language),使用的是 Kotlin语言,作用是创建了一个Button,放在 LinearLayout 内,并为其设置了一个点击监听器onClick。

在这个加载布局的例子中,button 方法接了一个字符串参数,这样的 Helper 方法同样适用于 TextView、EditText、ImageView。

如果我们不需要 View 其它的属性,我们可以省略 “{}” 直接写 “button("Ok")” 或只有 “button()”,如下:

verticalLayout {
    button("Ok")
    button("Cancel")
}
  • 关于控件显示文本问题:
    虽然我们的正式编码中不会存在例程中这样的HardCode,因为我们一般会使用Java的字符串来进行相应的替换,但是大多数时候字符串都是放在 res/values/ 目录下的,并且是运行时调用的,例如:getString(R.string.login)。

幸运的是,Anko中可以使用这样的两个 helper 方法:“button(R.string.login)” 和 “button{textResource = R.string.login}”。

注意,这些属性不是 “text,hint,image”, 而是“textResource,hintResource,imageResource”。

番外:什么是DSL?
Domain Specific Language,即“领域相关语言”,说白了它就是某个行业中的行话。
举个例子:
在构建证券交易系统的过程中,在证券交易活动中存在许多专业的金融术语和过程。现在要为该交易过程创建一个软件解决方案,那么开发者/构建者就必须了解证券交易活动,其中涉及到哪些对象、它们之间的规则以及约束条件是怎么样的。那么就让领域专家(这里就是证券交易专家)来描述证券交易活动中涉及的活动。但是领域专家习惯使用他们熟练使用的行业术语来表达,解决方案的构建者无法理解。如果解决方案的模型构建者要理解交易活动,就必须让领域专家用双方都能理解的自然语言来解释。这种解释的过程中,解决方案的模型构建者就理解了领域知识。这个过程中双方使用的语言就被称为“共同语言”。
在上面的描述,可以看到在需求收集的过程中,如果要成功构建模型,则需要一种领域专家和构建者(也就是通常的领域分析师/业务分析师)都能理解的“共同语言”。但是这种共同语言的创建过程没有保证,不能够保证在收集过程中得到的信息完整的描述了领域活动中所有的业务规则和活动。
如果能够让领域专家通过简单的编程方式描述领域中的所有活动和规则,那么就能在一定程度上保证描述的完整性。DSL 就是为了解决这个问题而提出的。

DSL 的特点:

  • 用于专门领域,不能用于其他领域
  • 表现力有限
  • 不描述解答域,仅描述问题域

DSL 与通用编程语言的区别:

  • DSL 有更高级的抽象,不涉及类似数据结构的细节
  • DSL 表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型

好了,书归正传,我们再举一个设置布局参数的例子。

(2)在“onCreate()”中加载线性布局:

注意:我们不需要继承其它的类,只要标准的Activity、Fragment、FragmentActivity 就好。

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
        }
    }
}

我们会发现,我们不需要显示的调用 setContentView(R.layout.something) , Anko 自动为Activity且只会对Activity进行 “set content view”,这一点我们会在Anko的源码中看到,下文会有解释。

  • 扩展属性
    这里的padding、 hint、textSize 是 扩展属性。大多数 View 都具有这些属性,并且允许使用 text = "Some text" 来代替 setText("Some text")

  • 扩展函数
    上述代码中的verticalLayout是一个竖直方向的 LinearLayout,其中的editText和 button 都是 扩展函数。这些函数存在于 Android 框架中的大部 View 中:Activities、Fragments (android.support包中的) 甚至 Context 也同样适用。所以如果有一个 Context 实例的话,我们可以写出下面的DSL结构:

val name = with(myContext) {
    editText {
        hint = "Name"
    }
}

这里的变量 name 就成为了 EditText 类型。

  • Layouts 和 LayoutParams
    在父布局中,布局控件可能有着这样的xml布局代码:
        

在Anko中,我们可以在 Button 的后面使用 lparams 来实现类似与 xml 中同样的效果。

linearLayout {
    button("Login") {
        textSize = 26f
    }.lparams(width = wrapContent) {
        horizontalMargin = dip(5)
        topMargin = dip(10)
    }
}

如果指定了 lparams 但是没有指定 width 或者 height,那么默认是 “wrapContent”,但是我们可以自己通过使用 named arguments 来指定。下面对该代码中涉及到的属性做出一些解释:

  • horizontalMargin: 同时设置 left 和 right margins
  • verticalMargin: 同时设置 top 和 bottom
  • margin: 同时设置4个方向的 margins

不过这里有一点需要做出说明:lparams的使用在不同的布局中是不同的,例如在RelativeLayout中:

val ID_OK = 1

relativeLayout {
    button("Ok") {
        id = ID_OK
    }.lparams { alignParentTop() }

    button("Cancel").lparams { below(ID_OK) }
}
  • Include tag
    使用 include tag 可以很容易向 DSL 插入 一个 XML layout :
include(R.layout.something) {
    backgroundColor = Color.RED
}.lparams(width = matchParent) { margin = dip(12) }

通常可以使用 lparams,如果类型不是 View,仍然可以用 {}:

include(R.layout.textfield) {
    text = "Hello, world!"
}
  • Styles
    Anko 支持 styling:style 是一个简单的函数,接受一个View,效果作用于这个 View,并且当这个 View 是一个ViewGroup 时,可以递归的作用于这个View的child View:
verticalLayout {
    editText {
        hint = "Name"
    }
    editText {
        hint = "Password"
    }
}.style { view -> when(view) {
    is EditText -> view.textSize = 20f
  }
}
  • Listeners
  • Kotlin中的效果(没有使用Anko):
button.setOnClickListener(object : OnClickListener {
    override fun onClick(v: View) {
        login(name, password)
    }
})
  • 使用Anko的代码:
button("Login") {
    onClick {
        login(name, password)
    }
}

当一个Listener有多个方法时,Anko就显得更方便了。

  • Kotlin中的效果(没有使用Anko):
seekBar.setOnSeekBarChangeListener(object: OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        // Do Something
    }
    override fun onStartTrackingTouch(seekBar: SeekBar?) {
        // Just an empty method
    }
    override fun onStopTrackingTouch(seekBar: SeekBar) {
        // Another empty method
    }
})
  • 使用Anko的代码:
seekBar {
    onSeekBarChangeListener {
        onProgressChanged { seekBar, progress, fromUser ->
            // Do Something
        }
    }
}

可以看到Anko的使用让代码大大减少且清晰了很多。如果你同时设置了 onProgressChanged 和 onStartTrackingTouch,对于这种多个相同的方法被合并的情况,最后的一个有效。另外,“onClick{}、onCheckedChange{}、onDateChange{}、onDrawerOpen{}、onItemClick{}、onScrollChange{}”等都有类似用法。

上面的代码简单粗暴,那么这样光鲜亮丽的代码是怎么实现的呢?我们简单说一说

三、Anko的源码解析

前面都是一些关于Anko用法的一些相关知识,也都是表象的一些东西。接下来写点深入的,我们一起去看看Anko的源码,对这些特性的实现一探究竟(对此没有兴趣的同学可以直接跳到第四部分看更多用法的例子)。

(1)首先给出目标代码:

verticalLayout {
   val name = editText()
   button("Say Hello") {
     onClick { toast("Hello, ${name.text}!") 
  } 
}

(2)其次给出代码工作流程

代码工作流程图

1、创建控件:Anko都是定义单例的
2、调用init():这个函数参数是我们传入的
3、addView():它根据传入的上下文对象(如果是acvtivity就setContentView(),如果ViewManager就addView())

(3)最后分析源码

verticalLayout点进去,看到如下代码:

inline fun Activity.verticalLayout(): LinearLayout = verticalLayout({})

inline fun Activity.verticalLayout(init: _LinearLayout.() -> Unit): LinearLayout {
    return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, init)
}

verticalLayout() 其实是个函数,参数也是个函数。这里就涉及到了“闭包”,简单来说就是:“verticalLayout”这个方法的参数(init)也是个函数,这个参数可以理解为在_LinearLayout类中扩展的匿名方法或者代码块。其中_LinearLayout是LinearLayout的子类,这个咱们后面讲的lparam时候再说。这个方法返回是一个LineaerLayout,咱们先来看看他的代码是怎么生成LinearLayout。

  • ankoView($$Anko$Factories$CustomViews.VERTICAL_LAYOUT_FACTORY, init)
object `$$Anko$Factories$CustomViews` {
    val VERTICAL_LAYOUT_FACTORY = {ctx: Context ->
        val view = _LinearLayout(ctx) 
        view.orientation = LinearLayout.VERTICAL
        view
    }}

创建一个单例工厂类,里面有个函数属性:

val VERTICAL_LAYOUT_FACTORY:(Context)-> _LinearLayout

里面的代码很简单,就是把布局的方向设置成“VERTICAL”,这里就不说了。

最关键的是return后面的ankoView是什么,咱们点进去看一下:

inline fun  Activity.ankoView(factory: (ctx: Context) -> T, init: T.() -> Unit): T {
    val view = factory(this)
    view.init()
    AnkoInternals.addView(this, view)
    return view
}

得出结论是:

  • ankoView是Activity扩展的一个方法并且需要两个参数
  • val view = factory(this) :通过工厂类构建这个控件
  • view.init() :控件初始化做一些动作 然后返回
那么AnkoInternals.addView(this, view) 是什么作用呢?咱们接着点进去一探究竟:
fun  addView(activity: Activity, view: T) {
    createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true)
}

fun  addView(manager: ViewManager, view: T) {
    return when (manager) {
        is ViewGroup -> manager.addView(view)
        is AnkoContext<*> -> manager.addView(view, null)
        else -> throw AnkoException("$manager is the wrong parent")
    }
}

inline fun  T.createAnkoContext(
        ctx: Context,
        init: AnkoContext.() -> Unit,
        setContentView: Boolean = false): AnkoContext {
    val dsl = AnkoContextImpl(ctx, this, setContentView)
    dsl.init()
    return dsl
}

可以看到,调用的第一个函数就是调用createAnkoContext(...)方法,而这个方法需要三个参数,主要是第二个参数“init: AnkoContext.() -> Unit”,当然这也是个函数,他是怎么传这个参数的呢?
是这样的:{ AnkoInternals.addView(this, view) } 调用了上述代码的第二个方法,这里面的 this 就是AnkoContext,其实就是 AnkoContextImpl <_LinearLayout>,而这个方法实际上就是调用它们的addView方法。

AnkoContextImpl是什么呢?我们继续看代码:
open class AnkoContextImpl(
        override val ctx: Context,
        override val owner: T,
        private val setContentView: Boolean) : AnkoContext {
        private var myView: View? = null
        override val view: View
        get() = myView ?: throw IllegalStateException("View was not set previously")

      override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
        if (view == null)return

        if (myView != null) {
            alreadyHasView()
        }

        this.myView = view

        if (setContentView) {
            doAddView(ctx, view)
        }
    }

    private fun doAddView(context: Context, view: View) {
        when (context) {
            is Activity -> context.setContentView(view)
            is ContextWrapper -> doAddView(context.baseContext, view)
            else -> throw IllegalStateException("Context is not an Activity, can't set content view") 
       }
    }

    open protected fun alreadyHasView(): Unit = throw IllegalStateException("View is already set: $myView")
}

这么多代码其实就干了一件事情,就是: is Activity -> context.setContentView(view),那么到此为止我们就知道了目标代码到底是怎样一步步加载布局并“自动setContentView”的了。对于其它控件,代码逻辑和 verticalLayout 都是差不多的。

最后我们来分析下 lparams 是怎么实现的,先给出目标代码:
textView {    

}.lparams(WRAP_CONTENT, WRAP_CONTENT) {
    centerHorizontally()
    below(circleImgView)
    topMargin = kIntHeight(0.02f)
}
然后电击进入到 lparams 代码:
fun  T.lparams( 
    width: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
    height: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
    init: RelativeLayout.LayoutParams.() -> Unit = {}): T {
    val layoutParams = RelativeLayout.LayoutParams(width, height)
    layoutParams.init()
    [email protected] = layoutParams
    return this
}

看到这里我想大家应该都明白了,Anko的写法大致就是这样实现的。

四、Anko 的一些高级功能

(1)Intent Helpers

传统的 Kotlin 启动新的 Activity 的方式是创建一个 Intent,同时可能传递一些参数,最后将创建的 Intent 通过 Context 的 startActivity() 方法传递,就像这样:

val intent = Intent(this, javaClass())
intent.putExtra("id", 5)
intent.putExtra("name", "John")
startActivity(intent)

而通过 Anoko 我们只需要一行代码来实现:

startActivity("id" to 5, "name" to "ActivityName")

startActivity 方法接收一个键值对,并且把这些键值作为 Intent 的 parameters 传递。而另一个我们熟知的方法 startActivityForResult() 也支持相同的语法。

(2)Popular Intent Shorthands

几乎所有的应用程序都会有调用默认浏览器或者打开系统邮件的代码,Anko 也提供了辅助方法:

browse("http://somewebsite.org (http://somewebsite.org/)")
email("[email protected] (mailto:[email protected])", "Here I am!", "Message text")

(3)AlertDialogs

Anko 提供了一个创建含有文本、列表、进度条甚至你自己的 DLS 布局的声明方式创建一个简单的文本对话框。

  • 有两个底部按钮的对话框
alert("Order", "Do you want to order this item?") {
    positiveButton("Yes") { processAnOrder() }
    negativeButton("No") { }
}.show()
  • 单选列表的对话框
val flowers = listOf("Chrysanthemum", "Rose", "Hyacinth")selector("What is your favorite flower?", flowers) {
      i -> toast("So your favorite flower is ${flowers[i]}, right?")
}
  • 不显示进度的 Loading Dialg
gressDialog("Please wait a minute.", "Downloading…")
indeterminateProgressDialog("Fetching the data…")
  • 通过 Anko 的 DSL 来创建一个自定义布局
alert {
    customView {
        verticalLayout {
            val familyName = editText {
                hint = "Family name"
            }
            val firstName = editText {
                hint = "First name"
             }
             positiveButton("Register") { register(familyName.text, firstName.text) }
         }
    }
}.show()

(3)Services

Android 系统的服务,比如 WifiManager , LocationManager 或者 Vibrator,Anko 都可以通过给 Context 添加扩展属性来实现:

if (!wifiManager.isWifiEnabled()) {
    vibrator.vibrate(200)
    toast("Wifi is disabled. Please turn on!")
}

(4)Asynchronous Tasks

在 Android 中异步任务使用最多的可能就是 AsyncTask 了,尽管它很流行,但是使用起来有诸多不便,比如:在使用 AsyncTask 时,当运行到 postExecute 的时候,如果 Activity 被销毁,会出现一些异常。Anko 提供了几种方式来实现相同的效果。

async(someExecutor) { // Omit the parameter to use the default executor
// This code will be executed in background
}

async() {...} 方法通过 ThreadExecutor 来执行 {} 中的代码,我们可以选择使用默认的或者自定义的,而如果你想在 async() 中回到 “UI线程” 来操作视图,可以使用 uiThread() 方法:

async {
    // Do some work ... 
    uiThread {
        toast("The work is done!")
    }
}
uiThread() 在 async() 中有特殊的语义:

async() 没有持有一个 Context 的实例,只有一个 WeakReference(弱引用) 来持有 Context 的实例,所以即使 lambda 表达式一直都不结束, Context 的实例也是不会泄露的。(UIThread 依赖于调用者,如果它被 Activity 调用,如果 activity.isFinishing() 返回 true ,那么 uiThread 不会执行,这就避免了刚才提及的那个问题。)

async {
    uiThread {
        /* Safe version. This code won't be executed
            if the underlying Context is gone. */
    }
    ctx.uiThread {
        /* Here we are calling the `uiThread`
            extension function for Context directly,
            so we are holding a reference to it. */
    }
}

(5)Logging

Android SDK 提供 android.util.Log 类来提供一些 logging 方法,并且我们在开头也教过大家如何定制自己的LogUtil,这些方法都很实用,但是我们每次必须传递一个 Tag 参数,同时这个 Tag 信息必须是 String 类型的,这就略显麻烦。不过现在我们可以通过 AnkoLogger 类摆脱这些恼人的问题:

class SomeActivity : Activity(), AnkoLogger {
    fun someMethod() {
        info("Info message")
        debug(42) // .toString() method will be called automatically
    }
}

默认的 Tag 名是当前的类名( 本例中的是SomeActivity),但是通过重写 AnkoLogger 的 loggerTag 属性我们是可以来更改的,而且每个方法有两个版本,这意味着我们还可以这样写:

info("String " + "concatenation")
info { "String " + "concatenation" }

结语:

到此为止,我们学会了使用 Anko 库中很多东西,发现 Anko 确实能帮助我们很大程度上地简化代码,但是我们现在还没有使用库中更多的东西,比如:实例化Intent、Fragment的创建、数据库的访问等等。

我们将会在之后学习中了解到更多有趣的例子,有志共同进步的同学请头像右侧点击“(+)”保持关注,后续好文第一时间推送给你。

你可能感兴趣的:(5.5 掌握 Anko,看这一篇就够了!)